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.js
CHANGED
|
@@ -1224,6 +1224,31 @@ function detectFinderCandidates(binary, width, height, options = {}) {
|
|
|
1224
1224
|
return clusterFinderCandidates(raw, minConfirmations);
|
|
1225
1225
|
}
|
|
1226
1226
|
|
|
1227
|
+
function recoverFinderSetFromTwo(binary, width, height, strongFinders, detector = {}) {
|
|
1228
|
+
if (strongFinders.length !== 2) return strongFinders;
|
|
1229
|
+
const moduleMean = (strongFinders[0].moduleSize + strongFinders[1].moduleSize) / 2;
|
|
1230
|
+
const loose = detectFinderCandidates(binary, width, height, {
|
|
1231
|
+
toleranceScale: Math.max(1.45, (detector.toleranceScale ?? 1) * 1.35),
|
|
1232
|
+
moduleSpreadLimit: Math.max(0.72, detector.moduleSpreadLimit ?? 0.45),
|
|
1233
|
+
minConfirmations: 1
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
const extra = loose.filter((candidate) => {
|
|
1237
|
+
const moduleRatio = Math.abs(candidate.moduleSize - moduleMean) / Math.max(candidate.moduleSize, moduleMean);
|
|
1238
|
+
if (moduleRatio > 0.62) return false;
|
|
1239
|
+
return strongFinders.every((known) =>
|
|
1240
|
+
Math.hypot(candidate.x - known.x, candidate.y - known.y) > Math.max(moduleMean, known.moduleSize) * 5
|
|
1241
|
+
);
|
|
1242
|
+
}).slice(0, 12);
|
|
1243
|
+
|
|
1244
|
+
if (!extra.length) return strongFinders;
|
|
1245
|
+
return strongFinders.concat(extra).sort((a, b) =>
|
|
1246
|
+
(b.confirmations - a.confirmations) ||
|
|
1247
|
+
(a.score - b.score) ||
|
|
1248
|
+
(b.moduleSize - a.moduleSize)
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1227
1252
|
function dot(a, b) {
|
|
1228
1253
|
return a.x * b.x + a.y * b.y;
|
|
1229
1254
|
}
|
|
@@ -1306,6 +1331,14 @@ function alignmentTemplateValue(pattern, r, c) {
|
|
|
1306
1331
|
return alignmentPatternIsBlack(pattern, r, c) ? 1 : 0;
|
|
1307
1332
|
}
|
|
1308
1333
|
|
|
1334
|
+
const ALIGNMENT_SUBCELL_PROBES = Object.freeze([
|
|
1335
|
+
Object.freeze([0, 0]),
|
|
1336
|
+
Object.freeze([-0.32, 0]),
|
|
1337
|
+
Object.freeze([0.32, 0]),
|
|
1338
|
+
Object.freeze([0, -0.32]),
|
|
1339
|
+
Object.freeze([0, 0.32])
|
|
1340
|
+
]);
|
|
1341
|
+
|
|
1309
1342
|
function alignmentScore(binary, width, height, center, basisU, basisV, scale, pattern) {
|
|
1310
1343
|
let matches = 0;
|
|
1311
1344
|
let total = 0;
|
|
@@ -1323,7 +1356,31 @@ function alignmentScore(binary, width, height, center, basisU, basisV, scale, pa
|
|
|
1323
1356
|
return total ? matches / total : 0;
|
|
1324
1357
|
}
|
|
1325
1358
|
|
|
1326
|
-
function
|
|
1359
|
+
function preciseAlignmentScore(binary, width, height, center, basisU, basisV, scale, pattern) {
|
|
1360
|
+
let matches = 0;
|
|
1361
|
+
let total = 0;
|
|
1362
|
+
const radius = alignmentPatternRadius(pattern);
|
|
1363
|
+
for (let r = -radius; r <= radius; r++) {
|
|
1364
|
+
for (let c = -radius; c <= radius; c++) {
|
|
1365
|
+
const expected = alignmentTemplateValue(pattern, r, c);
|
|
1366
|
+
// Centre-only alignment scoring has a broad plateau: a candidate can be
|
|
1367
|
+
// wrong by almost half a module while every centre still lands in the
|
|
1368
|
+
// correct solid tile. This denser score is reserved for the slow recovery
|
|
1369
|
+
// path, where Triangle16 needs sub-module geometry precision.
|
|
1370
|
+
for (const [du, dv] of ALIGNMENT_SUBCELL_PROBES) {
|
|
1371
|
+
const x = center.x + basisU.x * (c + du) * scale + basisV.x * (r + dv) * scale;
|
|
1372
|
+
const y = center.y + basisU.y * (c + du) * scale + basisV.y * (r + dv) * scale;
|
|
1373
|
+
const value = sampleBinaryAt(binary, width, height, x, y);
|
|
1374
|
+
if (value === null) continue;
|
|
1375
|
+
total++;
|
|
1376
|
+
if (value === expected) matches++;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return total ? matches / total : 0;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function searchAlignment(binary, width, height, triple, version, options = {}) {
|
|
1327
1384
|
const size = sizeForVersion(version);
|
|
1328
1385
|
const separation = size - 7;
|
|
1329
1386
|
const basisU = {
|
|
@@ -1343,8 +1400,12 @@ function searchAlignment(binary, width, height, triple, version) {
|
|
|
1343
1400
|
};
|
|
1344
1401
|
|
|
1345
1402
|
let best = { score: 0, center: predicted, scale: 1 };
|
|
1346
|
-
const
|
|
1403
|
+
const precise = options.preciseAlignment === true;
|
|
1404
|
+
const offsets = precise
|
|
1405
|
+
? [-2.5, -1.5, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1.5, 2.5]
|
|
1406
|
+
: [-2.5, -1.5, -0.75, 0, 0.75, 1.5, 2.5];
|
|
1347
1407
|
const scales = [0.72, 0.85, 1, 1.15, 1.3];
|
|
1408
|
+
const scoreAlignment = precise ? preciseAlignmentScore : alignmentScore;
|
|
1348
1409
|
|
|
1349
1410
|
for (const ou of offsets) {
|
|
1350
1411
|
for (const ov of offsets) {
|
|
@@ -1353,7 +1414,7 @@ function searchAlignment(binary, width, height, triple, version) {
|
|
|
1353
1414
|
y: predicted.y + basisU.y * ou + basisV.y * ov
|
|
1354
1415
|
};
|
|
1355
1416
|
for (const scale of scales) {
|
|
1356
|
-
const score =
|
|
1417
|
+
const score = scoreAlignment(binary, width, height, center, basisU, basisV, scale, target);
|
|
1357
1418
|
if (score > best.score) best = { score, center, scale };
|
|
1358
1419
|
}
|
|
1359
1420
|
}
|
|
@@ -1396,6 +1457,183 @@ function scoreAlignmentGrid(binary, width, height, homography, version) {
|
|
|
1396
1457
|
return { score, patternScores };
|
|
1397
1458
|
}
|
|
1398
1459
|
|
|
1460
|
+
function localProjectiveBasis(homography, moduleX, moduleY) {
|
|
1461
|
+
const center = projectPoint(homography, moduleX, moduleY);
|
|
1462
|
+
const alongU = projectPoint(homography, moduleX + 1, moduleY);
|
|
1463
|
+
const alongV = projectPoint(homography, moduleX, moduleY + 1);
|
|
1464
|
+
return {
|
|
1465
|
+
center,
|
|
1466
|
+
basisU: { x: alongU.x - center.x, y: alongU.y - center.y },
|
|
1467
|
+
basisV: { x: alongV.x - center.x, y: alongV.y - center.y }
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function searchProjectedAlignment(binary, width, height, homography, pattern) {
|
|
1472
|
+
const moduleX = pattern.col + 0.5;
|
|
1473
|
+
const moduleY = pattern.row + 0.5;
|
|
1474
|
+
const local = localProjectiveBasis(homography, moduleX, moduleY);
|
|
1475
|
+
const moduleScale = Math.max(
|
|
1476
|
+
0.5,
|
|
1477
|
+
(Math.hypot(local.basisU.x, local.basisU.y) + Math.hypot(local.basisV.x, local.basisV.y)) / 2
|
|
1478
|
+
);
|
|
1479
|
+
const offsets = [-1, -0.5, 0, 0.5, 1];
|
|
1480
|
+
const scales = [0.88, 1, 1.12];
|
|
1481
|
+
let best = {
|
|
1482
|
+
score: projectedAlignmentScore(binary, width, height, homography, pattern),
|
|
1483
|
+
center: local.center,
|
|
1484
|
+
scale: 1,
|
|
1485
|
+
offsetU: 0,
|
|
1486
|
+
offsetV: 0,
|
|
1487
|
+
displacementModules: 0
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
for (const offsetU of offsets) {
|
|
1491
|
+
for (const offsetV of offsets) {
|
|
1492
|
+
const center = {
|
|
1493
|
+
x: local.center.x + local.basisU.x * offsetU + local.basisV.x * offsetV,
|
|
1494
|
+
y: local.center.y + local.basisU.y * offsetU + local.basisV.y * offsetV
|
|
1495
|
+
};
|
|
1496
|
+
for (const scale of scales) {
|
|
1497
|
+
const score = alignmentScore(
|
|
1498
|
+
binary,
|
|
1499
|
+
width,
|
|
1500
|
+
height,
|
|
1501
|
+
center,
|
|
1502
|
+
local.basisU,
|
|
1503
|
+
local.basisV,
|
|
1504
|
+
scale,
|
|
1505
|
+
pattern
|
|
1506
|
+
);
|
|
1507
|
+
if (score > best.score) {
|
|
1508
|
+
best = {
|
|
1509
|
+
score,
|
|
1510
|
+
center,
|
|
1511
|
+
scale,
|
|
1512
|
+
offsetU,
|
|
1513
|
+
offsetV,
|
|
1514
|
+
displacementModules: Math.hypot(offsetU, offsetV),
|
|
1515
|
+
displacementPixels: Math.hypot(offsetU, offsetV) * moduleScale
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
return best;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function refineHomographyWithAlignmentGrid(
|
|
1526
|
+
binary,
|
|
1527
|
+
width,
|
|
1528
|
+
height,
|
|
1529
|
+
initialHomography,
|
|
1530
|
+
version,
|
|
1531
|
+
sourcePoints,
|
|
1532
|
+
destinationPoints,
|
|
1533
|
+
initialGrid,
|
|
1534
|
+
options = {}
|
|
1535
|
+
) {
|
|
1536
|
+
const patterns = alignmentPatternCentersForVersion(version);
|
|
1537
|
+
if (patterns.length <= 1 || options.alignmentRefinement === false) {
|
|
1538
|
+
return {
|
|
1539
|
+
homography: initialHomography,
|
|
1540
|
+
grid: initialGrid,
|
|
1541
|
+
refined: false,
|
|
1542
|
+
points: []
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const primary = patterns.find((pattern) => pattern.primary);
|
|
1547
|
+
const secondaries = patterns.filter((pattern) => !pattern.primary);
|
|
1548
|
+
const minimumScore = options.alignmentRefinePatternThreshold ?? 0.84;
|
|
1549
|
+
const maxPoints = Math.max(1, Math.min(18, Math.round(options.alignmentRefineMaxPoints ?? 12)));
|
|
1550
|
+
const candidates = [];
|
|
1551
|
+
|
|
1552
|
+
for (const pattern of secondaries) {
|
|
1553
|
+
const found = searchProjectedAlignment(binary, width, height, initialHomography, pattern);
|
|
1554
|
+
if (found.score < minimumScore) continue;
|
|
1555
|
+
if (found.displacementModules > (options.alignmentRefineMaxDisplacement ?? 1.45)) continue;
|
|
1556
|
+
candidates.push({ pattern, ...found });
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
if (!candidates.length) {
|
|
1560
|
+
return { homography: initialHomography, grid: initialGrid, refined: false, points: [] };
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Prefer confident references, but retain spatial distribution so dense
|
|
1564
|
+
// versions use alignment information across the entire matrix rather than
|
|
1565
|
+
// allowing a cluster of nearby markers to dominate the fit.
|
|
1566
|
+
candidates.sort((a, b) =>
|
|
1567
|
+
(b.score - a.score) ||
|
|
1568
|
+
((b.pattern.row + b.pattern.col) - (a.pattern.row + a.pattern.col))
|
|
1569
|
+
);
|
|
1570
|
+
const selected = [];
|
|
1571
|
+
for (const candidate of candidates) {
|
|
1572
|
+
if (selected.length >= maxPoints) break;
|
|
1573
|
+
const tooClose = selected.some((item) =>
|
|
1574
|
+
Math.hypot(item.pattern.col - candidate.pattern.col, item.pattern.row - candidate.pattern.row) < 6
|
|
1575
|
+
);
|
|
1576
|
+
if (!tooClose || selected.length < 3) selected.push(candidate);
|
|
1577
|
+
}
|
|
1578
|
+
if (!selected.length) return { homography: initialHomography, grid: initialGrid, refined: false, points: [] };
|
|
1579
|
+
|
|
1580
|
+
const refinedSource = sourcePoints.slice();
|
|
1581
|
+
const refinedDestination = destinationPoints.slice();
|
|
1582
|
+
const weights = [4, 4, 4, 5];
|
|
1583
|
+
const pointInfo = [];
|
|
1584
|
+
|
|
1585
|
+
for (const candidate of selected) {
|
|
1586
|
+
refinedSource.push(candidate.center);
|
|
1587
|
+
refinedDestination.push({
|
|
1588
|
+
x: candidate.pattern.col + 0.5,
|
|
1589
|
+
y: candidate.pattern.row + 0.5
|
|
1590
|
+
});
|
|
1591
|
+
// A 3x3 marker carries less evidence than the 5x5 primary and the three
|
|
1592
|
+
// finder patterns, so it helps average geometry without overpowering them.
|
|
1593
|
+
weights.push(0.8 + candidate.score * 1.7);
|
|
1594
|
+
pointInfo.push({
|
|
1595
|
+
row: candidate.pattern.row,
|
|
1596
|
+
col: candidate.pattern.col,
|
|
1597
|
+
score: candidate.score,
|
|
1598
|
+
x: candidate.center.x,
|
|
1599
|
+
y: candidate.center.y,
|
|
1600
|
+
displacementModules: candidate.displacementModules
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
let refinedHomography;
|
|
1605
|
+
try {
|
|
1606
|
+
refinedHomography = computeHomographyLeastSquares(refinedSource, refinedDestination, weights);
|
|
1607
|
+
} catch {
|
|
1608
|
+
return { homography: initialHomography, grid: initialGrid, refined: false, points: [] };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const refinedGrid = scoreAlignmentGrid(binary, width, height, refinedHomography, version);
|
|
1612
|
+
const primaryBefore = primary
|
|
1613
|
+
? projectedAlignmentScore(binary, width, height, initialHomography, primary)
|
|
1614
|
+
: 1;
|
|
1615
|
+
const primaryAfter = primary
|
|
1616
|
+
? projectedAlignmentScore(binary, width, height, refinedHomography, primary)
|
|
1617
|
+
: 1;
|
|
1618
|
+
const allowedGridDrop = options.alignmentRefineAllowedGridDrop ?? 0.006;
|
|
1619
|
+
const allowedPrimaryDrop = options.alignmentRefineAllowedPrimaryDrop ?? 0.04;
|
|
1620
|
+
|
|
1621
|
+
if (
|
|
1622
|
+
refinedGrid.score + allowedGridDrop < initialGrid.score ||
|
|
1623
|
+
primaryAfter + allowedPrimaryDrop < primaryBefore
|
|
1624
|
+
) {
|
|
1625
|
+
return { homography: initialHomography, grid: initialGrid, refined: false, points: [] };
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return {
|
|
1629
|
+
homography: refinedHomography,
|
|
1630
|
+
grid: refinedGrid,
|
|
1631
|
+
refined: true,
|
|
1632
|
+
points: pointInfo,
|
|
1633
|
+
initialGridScore: initialGrid.score
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1399
1637
|
function solveLinearSystemFloat(matrix, vector) {
|
|
1400
1638
|
const n = vector.length;
|
|
1401
1639
|
const a = matrix.map((row, index) => row.slice().concat([vector[index]]));
|
|
@@ -1437,6 +1675,43 @@ function computeHomography(sourcePoints, destinationPoints) {
|
|
|
1437
1675
|
return [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], 1];
|
|
1438
1676
|
}
|
|
1439
1677
|
|
|
1678
|
+
function computeHomographyLeastSquares(sourcePoints, destinationPoints, weights = null) {
|
|
1679
|
+
// Same destination -> source convention as computeHomography(), but accepts
|
|
1680
|
+
// more than four correspondences and averages locator/alignment measurement
|
|
1681
|
+
// noise. This is used only as a refinement after a valid four-point projective
|
|
1682
|
+
// solution already exists.
|
|
1683
|
+
assert(
|
|
1684
|
+
sourcePoints.length === destinationPoints.length && sourcePoints.length >= 4,
|
|
1685
|
+
"At least four matching point pairs are required."
|
|
1686
|
+
);
|
|
1687
|
+
|
|
1688
|
+
const normal = Array.from({ length: 8 }, () => Array(8).fill(0));
|
|
1689
|
+
const rhs = Array(8).fill(0);
|
|
1690
|
+
|
|
1691
|
+
const addEquation = (row, value, weight) => {
|
|
1692
|
+
for (let i = 0; i < 8; i++) {
|
|
1693
|
+
rhs[i] += row[i] * value * weight;
|
|
1694
|
+
for (let j = 0; j < 8; j++) normal[i][j] += row[i] * row[j] * weight;
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
for (let i = 0; i < sourcePoints.length; i++) {
|
|
1699
|
+
const u = destinationPoints[i].x;
|
|
1700
|
+
const v = destinationPoints[i].y;
|
|
1701
|
+
const x = sourcePoints[i].x;
|
|
1702
|
+
const y = sourcePoints[i].y;
|
|
1703
|
+
const weight = Math.max(0.05, Number(weights?.[i] ?? 1));
|
|
1704
|
+
addEquation([u, v, 1, 0, 0, 0, -u * x, -v * x], x, weight);
|
|
1705
|
+
addEquation([0, 0, 0, u, v, 1, -u * y, -v * y], y, weight);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// Tiny Tikhonov regularisation keeps near-degenerate noisy fits numerically
|
|
1709
|
+
// stable without moving a normal QR-sized solution in any meaningful way.
|
|
1710
|
+
for (let i = 0; i < 8; i++) normal[i][i] += 1e-9;
|
|
1711
|
+
const h = solveLinearSystemFloat(normal, rhs);
|
|
1712
|
+
return [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], 1];
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1440
1715
|
function projectPoint(h, u, v) {
|
|
1441
1716
|
const denominator = h[6] * u + h[7] * v + h[8];
|
|
1442
1717
|
return {
|
|
@@ -1462,7 +1737,7 @@ function geometryCandidatesFromBinary(binary, width, height, finders, threshold,
|
|
|
1462
1737
|
for (const item of versions) {
|
|
1463
1738
|
const version = item.version;
|
|
1464
1739
|
const size = sizeForVersion(version);
|
|
1465
|
-
const alignment = searchAlignment(binary, width, height, triple, version);
|
|
1740
|
+
const alignment = searchAlignment(binary, width, height, triple, version, options);
|
|
1466
1741
|
if (alignment.score < (options.alignmentThreshold ?? 0.72)) continue;
|
|
1467
1742
|
|
|
1468
1743
|
const alignmentTarget = alignment.target;
|
|
@@ -1486,8 +1761,34 @@ function geometryCandidatesFromBinary(binary, width, height, finders, threshold,
|
|
|
1486
1761
|
continue;
|
|
1487
1762
|
}
|
|
1488
1763
|
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1764
|
+
const initialAlignmentGrid = scoreAlignmentGrid(binary, width, height, homography, version);
|
|
1765
|
+
const alignmentGridThreshold = options.alignmentGridThreshold ?? 0.68;
|
|
1766
|
+
const refinementFloor = Math.max(0, alignmentGridThreshold - (options.alignmentRefineCandidateMargin ?? 0.10));
|
|
1767
|
+
const skipRefinementScore = options.alignmentRefineSkipScore ?? 0.985;
|
|
1768
|
+
const refinement = (
|
|
1769
|
+
initialAlignmentGrid.score >= refinementFloor &&
|
|
1770
|
+
initialAlignmentGrid.score < skipRefinementScore
|
|
1771
|
+
)
|
|
1772
|
+
? refineHomographyWithAlignmentGrid(
|
|
1773
|
+
binary,
|
|
1774
|
+
width,
|
|
1775
|
+
height,
|
|
1776
|
+
homography,
|
|
1777
|
+
version,
|
|
1778
|
+
src,
|
|
1779
|
+
dest,
|
|
1780
|
+
initialAlignmentGrid,
|
|
1781
|
+
options
|
|
1782
|
+
)
|
|
1783
|
+
: {
|
|
1784
|
+
homography,
|
|
1785
|
+
grid: initialAlignmentGrid,
|
|
1786
|
+
refined: false,
|
|
1787
|
+
points: []
|
|
1788
|
+
};
|
|
1789
|
+
homography = refinement.homography;
|
|
1790
|
+
const alignmentGrid = refinement.grid;
|
|
1791
|
+
if (alignmentGrid.score < alignmentGridThreshold) continue;
|
|
1491
1792
|
|
|
1492
1793
|
const versionPenalty = item.error / 4;
|
|
1493
1794
|
const alignmentConfidence = 0.55 * alignment.score + 0.45 * alignmentGrid.score;
|
|
@@ -1506,6 +1807,9 @@ function geometryCandidatesFromBinary(binary, width, height, finders, threshold,
|
|
|
1506
1807
|
target: { row: alignmentTarget.row, col: alignmentTarget.col },
|
|
1507
1808
|
patterns: alignmentGrid.patternScores.length,
|
|
1508
1809
|
gridScore: alignmentGrid.score,
|
|
1810
|
+
initialGridScore: refinement.initialGridScore ?? initialAlignmentGrid.score,
|
|
1811
|
+
refined: refinement.refined,
|
|
1812
|
+
refinementPoints: refinement.points,
|
|
1509
1813
|
patternScores: alignmentGrid.patternScores
|
|
1510
1814
|
},
|
|
1511
1815
|
threshold,
|
|
@@ -1577,19 +1881,21 @@ function detectCodeGeometry(imageData, options = {}) {
|
|
|
1577
1881
|
const maxCandidates = options.maxCandidates ?? 8;
|
|
1578
1882
|
|
|
1579
1883
|
const evaluatePass = (pass, recovery = false) => {
|
|
1580
|
-
const
|
|
1581
|
-
const
|
|
1884
|
+
const detector = pass.detector ?? {};
|
|
1885
|
+
const finders = detectFinderCandidates(pass.binary, width, height, detector);
|
|
1886
|
+
const geometryOptions = {
|
|
1887
|
+
...options,
|
|
1888
|
+
finderMethod: pass.finderMethod,
|
|
1889
|
+
alignmentThreshold: recovery ? 0.68 : 0.72,
|
|
1890
|
+
alignmentGridThreshold: recovery ? 0.64 : 0.68
|
|
1891
|
+
};
|
|
1892
|
+
let geometries = geometryCandidatesFromBinary(
|
|
1582
1893
|
pass.binary,
|
|
1583
1894
|
width,
|
|
1584
1895
|
height,
|
|
1585
1896
|
finders,
|
|
1586
1897
|
pass.threshold,
|
|
1587
|
-
|
|
1588
|
-
...options,
|
|
1589
|
-
finderMethod: pass.finderMethod,
|
|
1590
|
-
alignmentThreshold: recovery ? 0.68 : 0.72,
|
|
1591
|
-
alignmentGridThreshold: recovery ? 0.64 : 0.68
|
|
1592
|
-
}
|
|
1898
|
+
geometryOptions
|
|
1593
1899
|
);
|
|
1594
1900
|
|
|
1595
1901
|
pushFinderDiagnostic(
|
|
@@ -1599,6 +1905,35 @@ function detectCodeGeometry(imageData, options = {}) {
|
|
|
1599
1905
|
finders,
|
|
1600
1906
|
geometries
|
|
1601
1907
|
);
|
|
1908
|
+
|
|
1909
|
+
// Dense codes under projective distortion can leave two finder patterns
|
|
1910
|
+
// perfectly strong while the third is stretched enough to miss the normal
|
|
1911
|
+
// 1:1:3:1:1 tolerance. Only in that very specific recovery state, run one
|
|
1912
|
+
// bounded looser detector and let the normal three-finder geometry checks
|
|
1913
|
+
// reject false data-cell lookalikes. The clean fast path is unchanged.
|
|
1914
|
+
if (!geometries.length && recovery && finders.length === 2) {
|
|
1915
|
+
const recoveredFinders = recoverFinderSetFromTwo(pass.binary, width, height, finders, detector);
|
|
1916
|
+
if (recoveredFinders.length > 2) {
|
|
1917
|
+
const finderMethod = `${pass.finderMethod}-two-finder-recovery`;
|
|
1918
|
+
geometries = geometryCandidatesFromBinary(
|
|
1919
|
+
pass.binary,
|
|
1920
|
+
width,
|
|
1921
|
+
height,
|
|
1922
|
+
recoveredFinders,
|
|
1923
|
+
pass.threshold,
|
|
1924
|
+
{ ...geometryOptions, finderMethod }
|
|
1925
|
+
);
|
|
1926
|
+
pushFinderDiagnostic(
|
|
1927
|
+
{ ...options, width, height, diagnosticLabel: `${options.diagnosticLabel ?? "perspective"}-two-finder` },
|
|
1928
|
+
finderMethod,
|
|
1929
|
+
pass.threshold,
|
|
1930
|
+
recoveredFinders,
|
|
1931
|
+
geometries
|
|
1932
|
+
);
|
|
1933
|
+
if (geometries.length) return { finders: recoveredFinders, geometries };
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1602
1937
|
return { finders, geometries };
|
|
1603
1938
|
};
|
|
1604
1939
|
|
|
@@ -1614,6 +1949,38 @@ function detectCodeGeometry(imageData, options = {}) {
|
|
|
1614
1949
|
if (fast.geometries.length) return fast.geometries.slice(0, maxCandidates);
|
|
1615
1950
|
if (options.finderRecovery === false) return [];
|
|
1616
1951
|
|
|
1952
|
+
// If the clean threshold already found exactly two strong locators, try the
|
|
1953
|
+
// bounded perspective-tolerant third-finder pass before any color processing.
|
|
1954
|
+
// This is substantially cheaper than Auto Color and targets the dense-code
|
|
1955
|
+
// projective failure mode directly.
|
|
1956
|
+
if (fast.finders.length === 2) {
|
|
1957
|
+
const recoveredFinders = recoverFinderSetFromTwo(valueInfo.binary, width, height, fast.finders, {});
|
|
1958
|
+
if (recoveredFinders.length > 2) {
|
|
1959
|
+
const finderMethod = "rgb-value-otsu-two-finder-recovery";
|
|
1960
|
+
const recoveredGeometries = geometryCandidatesFromBinary(
|
|
1961
|
+
valueInfo.binary,
|
|
1962
|
+
width,
|
|
1963
|
+
height,
|
|
1964
|
+
recoveredFinders,
|
|
1965
|
+
valueInfo.threshold,
|
|
1966
|
+
{
|
|
1967
|
+
...options,
|
|
1968
|
+
finderMethod,
|
|
1969
|
+
alignmentThreshold: 0.68,
|
|
1970
|
+
alignmentGridThreshold: 0.64
|
|
1971
|
+
}
|
|
1972
|
+
);
|
|
1973
|
+
pushFinderDiagnostic(
|
|
1974
|
+
{ ...options, width, height, diagnosticLabel: `${options.diagnosticLabel ?? "perspective"}-two-finder` },
|
|
1975
|
+
finderMethod,
|
|
1976
|
+
valueInfo.threshold,
|
|
1977
|
+
recoveredFinders,
|
|
1978
|
+
recoveredGeometries
|
|
1979
|
+
);
|
|
1980
|
+
if (recoveredGeometries.length) return recoveredGeometries.slice(0, maxCandidates);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1617
1984
|
// Camera recovery #1: Photoshop Auto Color-style per-channel levels before
|
|
1618
1985
|
// finder thresholding. Live camera frames are usually much larger than the
|
|
1619
1986
|
// QR itself, so a single global histogram can be dominated by dark room/UI
|
|
@@ -2020,6 +2387,49 @@ function samplePerspectiveMatrix(imageData, homography, size, options = {}) {
|
|
|
2020
2387
|
return { rgbGrid };
|
|
2021
2388
|
}
|
|
2022
2389
|
|
|
2390
|
+
/**
|
|
2391
|
+
* Sample the two protected regions of every Triangle16 module.
|
|
2392
|
+
*
|
|
2393
|
+
* Triangle16 uses a fixed "/" diagonal. Samples are intentionally placed well
|
|
2394
|
+
* inside the upper-left and lower-right triangles, away from the diagonal and
|
|
2395
|
+
* module borders. This makes the decoder much less sensitive to antialiasing,
|
|
2396
|
+
* blur, resampling and small homography errors than center sampling.
|
|
2397
|
+
*/
|
|
2398
|
+
function samplePerspectiveTriangleMatrix(imageData, homography, size, options = {}) {
|
|
2399
|
+
const triangleGrid = Array.from({ length: size }, () => new Array(size));
|
|
2400
|
+
const mode = options.sampleMode ?? "cross";
|
|
2401
|
+
const radius = options.highDensitySampleRadius ?? options.triangleSampleRadius ??
|
|
2402
|
+
(mode === "median" ? 0.075 : 0.085);
|
|
2403
|
+
const inset = Math.max(0.18, Math.min(0.36, Number(options.highDensitySampleInset ?? options.triangleSampleInset ?? 0.28)));
|
|
2404
|
+
const firstOffset = inset;
|
|
2405
|
+
const secondOffset = 1 - inset;
|
|
2406
|
+
const offsetX = Number.isFinite(options.sampleOffsetX) ? options.sampleOffsetX : 0;
|
|
2407
|
+
const offsetY = Number.isFinite(options.sampleOffsetY) ? options.sampleOffsetY : 0;
|
|
2408
|
+
const sampler = mode === "median" ? robustProjectedSample : averageProjectedSample;
|
|
2409
|
+
|
|
2410
|
+
for (let r = 0; r < size; r++) {
|
|
2411
|
+
for (let c = 0; c < size; c++) {
|
|
2412
|
+
triangleGrid[r][c] = {
|
|
2413
|
+
first: sampler(
|
|
2414
|
+
imageData,
|
|
2415
|
+
homography,
|
|
2416
|
+
c + firstOffset + offsetX,
|
|
2417
|
+
r + firstOffset + offsetY,
|
|
2418
|
+
radius
|
|
2419
|
+
),
|
|
2420
|
+
second: sampler(
|
|
2421
|
+
imageData,
|
|
2422
|
+
homography,
|
|
2423
|
+
c + secondOffset + offsetX,
|
|
2424
|
+
r + secondOffset + offsetY,
|
|
2425
|
+
radius
|
|
2426
|
+
)
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
return { triangleGrid };
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2023
2433
|
function meanRgb(values) {
|
|
2024
2434
|
if (!values.length) throw new Error("No calibration samples available.");
|
|
2025
2435
|
return values.reduce(
|
|
@@ -2216,6 +2626,34 @@ function sampleAxisAlignedGrid(imageData, bounds, size, radiusRatio = 0.18) {
|
|
|
2216
2626
|
return { rgbGrid, moduleWidth: moduleW, moduleHeight: moduleH };
|
|
2217
2627
|
}
|
|
2218
2628
|
|
|
2629
|
+
function sampleAxisAlignedTriangleGrid(imageData, bounds, size, radiusRatio = 0.085, insetRatio = 0.28) {
|
|
2630
|
+
const moduleW = bounds.width / size;
|
|
2631
|
+
const moduleH = bounds.height / size;
|
|
2632
|
+
const inset = Math.max(0.18, Math.min(0.36, insetRatio));
|
|
2633
|
+
const radius = Math.max(0, Math.min(moduleW, moduleH) * radiusRatio);
|
|
2634
|
+
const triangleGrid = Array.from({ length: size }, () => new Array(size));
|
|
2635
|
+
|
|
2636
|
+
for (let r = 0; r < size; r++) {
|
|
2637
|
+
for (let c = 0; c < size; c++) {
|
|
2638
|
+
triangleGrid[r][c] = {
|
|
2639
|
+
first: sampleAverageAxis(
|
|
2640
|
+
imageData,
|
|
2641
|
+
bounds.x + (c + inset) * moduleW,
|
|
2642
|
+
bounds.y + (r + inset) * moduleH,
|
|
2643
|
+
radius
|
|
2644
|
+
),
|
|
2645
|
+
second: sampleAverageAxis(
|
|
2646
|
+
imageData,
|
|
2647
|
+
bounds.x + (c + 1 - inset) * moduleW,
|
|
2648
|
+
bounds.y + (r + 1 - inset) * moduleH,
|
|
2649
|
+
radius
|
|
2650
|
+
)
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
return { triangleGrid, moduleWidth: moduleW, moduleHeight: moduleH };
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2219
2657
|
const visionInternals = Object.freeze({
|
|
2220
2658
|
buildGray,
|
|
2221
2659
|
otsuThreshold,
|
|
@@ -2228,21 +2666,21 @@ const visionInternals = Object.freeze({
|
|
|
2228
2666
|
luminance
|
|
2229
2667
|
});
|
|
2230
2668
|
|
|
2231
|
-
return { computeHomography, projectPoint, detectCodeGeometry, autoColorImageData, autoToneContrastColorRgbGrid, autoToneContrastColorImageData, samplePerspectiveMatrix, sampleObservedPalette, spatiallyNormalizeRgbGrid, rectifyImageData, findActiveBounds, sampleAxisAlignedGrid, visionInternals };
|
|
2669
|
+
return { computeHomography, projectPoint, detectCodeGeometry, autoColorImageData, autoToneContrastColorRgbGrid, autoToneContrastColorImageData, samplePerspectiveMatrix, samplePerspectiveTriangleMatrix, sampleObservedPalette, spatiallyNormalizeRgbGrid, rectifyImageData, findActiveBounds, sampleAxisAlignedGrid, sampleAxisAlignedTriangleGrid, visionInternals };
|
|
2232
2670
|
})();
|
|
2233
2671
|
const __qqr_quadqr_js = (function () {
|
|
2234
2672
|
const { MAX_CODEWORD_SYMBOLS, rsEncode, rsDecode } = __qqr_reed_solomon_js;
|
|
2235
2673
|
const { alignmentPatternCentersForVersion, alignmentPatternIsBlack, alignmentPatternRadius, sizeForVersion, versionFromSize } = __qqr_geometry_js;
|
|
2236
|
-
const { detectCodeGeometry, computeHomography, projectPoint, samplePerspectiveMatrix, rectifyImageData, sampleObservedPalette, spatiallyNormalizeRgbGrid, autoToneContrastColorRgbGrid, autoToneContrastColorImageData, autoColorImageData, findActiveBounds, sampleAxisAlignedGrid } = __qqr_vision_js;
|
|
2674
|
+
const { detectCodeGeometry, computeHomography, projectPoint, samplePerspectiveMatrix, samplePerspectiveTriangleMatrix, rectifyImageData, sampleObservedPalette, spatiallyNormalizeRgbGrid, autoToneContrastColorRgbGrid, autoToneContrastColorImageData, autoColorImageData, findActiveBounds, sampleAxisAlignedGrid, sampleAxisAlignedTriangleGrid } = __qqr_vision_js;
|
|
2237
2675
|
const { decryptSecurePayload, encryptSecurePayload, inspectSecureEnvelope, SECURITY_MODES, SECURITY_ALGORITHMS, SECURE_PAYLOAD_VERSION, DEFAULT_PBKDF2_ITERATIONS, generateRaw256Key, normalizeRaw256Key, bytesToHex } = __qqr_security_js;
|
|
2238
2676
|
/**
|
|
2239
2677
|
* QuadQR
|
|
2240
2678
|
*
|
|
2241
|
-
* Experimental
|
|
2679
|
+
* Experimental RGBW / Triangle16 matrix code written in pure JavaScript.
|
|
2242
2680
|
*
|
|
2243
2681
|
* Core format:
|
|
2244
|
-
* - Red / Green / Blue / White data
|
|
2245
|
-
* -
|
|
2682
|
+
* - RGBW profile: Red / Green / Blue / White, exactly 2 bits per data cell
|
|
2683
|
+
* - Triangle16 profile: two fixed RGBW triangle regions, 16 states / 4 bits per body data cell
|
|
2246
2684
|
* - GF(256) Reed-Solomon ECC over byte symbols
|
|
2247
2685
|
* - interleaved body ECC blocks
|
|
2248
2686
|
* - zero-overhead spectral-spatial cell placement
|
|
@@ -2262,6 +2700,12 @@ const RENDER_MODES = Object.freeze({
|
|
|
2262
2700
|
PRINT: "print"
|
|
2263
2701
|
});
|
|
2264
2702
|
|
|
2703
|
+
const CELL_ENCODINGS = Object.freeze({
|
|
2704
|
+
RGBW: "rgbw",
|
|
2705
|
+
TRIANGLE16: "triangle16"
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2708
|
+
|
|
2265
2709
|
// Print-safe defaults deliberately use darker, more ink-tolerant primaries and
|
|
2266
2710
|
// a true white background. Applications can still override individual colors.
|
|
2267
2711
|
const PRINT_PALETTE = Object.freeze({
|
|
@@ -2319,8 +2763,10 @@ const MAGIC = new Uint8Array([0x51, 0x51, 0x52, 0x57]); // QQRW (QuadQR RGBW)
|
|
|
2319
2763
|
const HEADER_BYTES = 10;
|
|
2320
2764
|
const HEADER_RS_PARITY = 8;
|
|
2321
2765
|
const HEADER_CODEWORD_BYTES = HEADER_BYTES + HEADER_RS_PARITY;
|
|
2322
|
-
const
|
|
2323
|
-
const
|
|
2766
|
+
const RGBW_CELLS_PER_BYTE = 4;
|
|
2767
|
+
const TRIANGLE16_CELLS_PER_BYTE = 2;
|
|
2768
|
+
const CELLS_PER_BYTE = RGBW_CELLS_PER_BYTE; // legacy internal alias
|
|
2769
|
+
const HEADER_CODEWORD_CELLS = HEADER_CODEWORD_BYTES * RGBW_CELLS_PER_BYTE;
|
|
2324
2770
|
|
|
2325
2771
|
// Version 1 is a deliberately compact small-symbol profile. A 21x21 matrix
|
|
2326
2772
|
// cannot afford the normal 18-byte protected header plus a 24-byte M body
|
|
@@ -2332,7 +2778,7 @@ const COMPACT_HEADER_MAGIC = 0xc3;
|
|
|
2332
2778
|
const COMPACT_HEADER_BYTES = 4;
|
|
2333
2779
|
const COMPACT_HEADER_RS_PARITY = 4;
|
|
2334
2780
|
const COMPACT_HEADER_CODEWORD_BYTES = COMPACT_HEADER_BYTES + COMPACT_HEADER_RS_PARITY;
|
|
2335
|
-
const COMPACT_HEADER_CODEWORD_CELLS = COMPACT_HEADER_CODEWORD_BYTES *
|
|
2781
|
+
const COMPACT_HEADER_CODEWORD_CELLS = COMPACT_HEADER_CODEWORD_BYTES * RGBW_CELLS_PER_BYTE;
|
|
2336
2782
|
const COMPACT_ECC_LEVELS = Object.freeze({
|
|
2337
2783
|
L: Object.freeze({ paritySymbols: 4, correctableSymbolsPerBlock: 2 }),
|
|
2338
2784
|
M: Object.freeze({ paritySymbols: 8, correctableSymbolsPerBlock: 4 }),
|
|
@@ -2345,6 +2791,7 @@ const TEXT_FLAG = 1;
|
|
|
2345
2791
|
const SECURE_FLAG = 1 << 3;
|
|
2346
2792
|
const EXTENDED_PAYLOAD_FLAG = 1 << 4;
|
|
2347
2793
|
const SIGNED_FLAG = 1 << 5;
|
|
2794
|
+
const TRIANGLE16_FLAG = 1 << 6;
|
|
2348
2795
|
|
|
2349
2796
|
// Internal payload extension envelope. This is deliberately not a public
|
|
2350
2797
|
// payload mode or content-type system. It exists only when compression or
|
|
@@ -2407,6 +2854,45 @@ function normalizeEccLevel(level = DEFAULT_ECC_LEVEL) {
|
|
|
2407
2854
|
return value;
|
|
2408
2855
|
}
|
|
2409
2856
|
|
|
2857
|
+
function normalizeCellEncoding(value = CELL_ENCODINGS.RGBW) {
|
|
2858
|
+
const key = String(value).toLowerCase();
|
|
2859
|
+
assert(
|
|
2860
|
+
key === CELL_ENCODINGS.RGBW || key === CELL_ENCODINGS.TRIANGLE16,
|
|
2861
|
+
`Internal cell encoding must be ${CELL_ENCODINGS.RGBW} or ${CELL_ENCODINGS.TRIANGLE16}.`
|
|
2862
|
+
);
|
|
2863
|
+
return key;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// Public API presents Triangle16 as one experimental High Density Mode toggle.
|
|
2867
|
+
// cellEncoding remains an internal/backward-compatible detail so existing
|
|
2868
|
+
// experimental branch symbols and renderer metadata continue to work.
|
|
2869
|
+
function cellEncodingFromOptions(options = {}) {
|
|
2870
|
+
if (typeof options.highDensity === "boolean") {
|
|
2871
|
+
return options.highDensity ? CELL_ENCODINGS.TRIANGLE16 : CELL_ENCODINGS.RGBW;
|
|
2872
|
+
}
|
|
2873
|
+
return normalizeCellEncoding(options.cellEncoding ?? CELL_ENCODINGS.RGBW);
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
function cellsPerByteForEncoding(cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
2877
|
+
return normalizeCellEncoding(cellEncoding) === CELL_ENCODINGS.TRIANGLE16
|
|
2878
|
+
? TRIANGLE16_CELLS_PER_BYTE
|
|
2879
|
+
: RGBW_CELLS_PER_BYTE;
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
function packTriangleCell(first, second) {
|
|
2883
|
+
assert(first >= 0 && first <= 3 && second >= 0 && second <= 3, "Invalid Triangle16 color.");
|
|
2884
|
+
return ((first & 3) << 2) | (second & 3);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
function unpackTriangleCell(value) {
|
|
2888
|
+
assert(Number.isInteger(value) && value >= 0 && value <= 15, "Invalid Triangle16 data cell.");
|
|
2889
|
+
return { first: (value >>> 2) & 3, second: value & 3 };
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
function solidTriangleCell(color) {
|
|
2893
|
+
return packTriangleCell(color, color);
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2410
2896
|
function concatBytes(...arrays) {
|
|
2411
2897
|
const total = arrays.reduce((sum, item) => sum + item.length, 0);
|
|
2412
2898
|
const out = new Uint8Array(total);
|
|
@@ -2834,8 +3320,19 @@ function crc32(bytes) {
|
|
|
2834
3320
|
return (crc ^ 0xffffffff) >>> 0;
|
|
2835
3321
|
}
|
|
2836
3322
|
|
|
2837
|
-
function bytesToCells(bytes) {
|
|
2838
|
-
const
|
|
3323
|
+
function bytesToCells(bytes, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3324
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3325
|
+
if (encoding === CELL_ENCODINGS.TRIANGLE16) {
|
|
3326
|
+
const out = new Array(bytes.length * TRIANGLE16_CELLS_PER_BYTE);
|
|
3327
|
+
let cursor = 0;
|
|
3328
|
+
for (const byte of bytes) {
|
|
3329
|
+
out[cursor++] = (byte >>> 4) & 0x0f;
|
|
3330
|
+
out[cursor++] = byte & 0x0f;
|
|
3331
|
+
}
|
|
3332
|
+
return out;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
const out = new Array(bytes.length * RGBW_CELLS_PER_BYTE);
|
|
2839
3336
|
let cursor = 0;
|
|
2840
3337
|
for (const byte of bytes) {
|
|
2841
3338
|
out[cursor++] = (byte >>> 6) & 0b11;
|
|
@@ -2846,11 +3343,36 @@ function bytesToCells(bytes) {
|
|
|
2846
3343
|
return out;
|
|
2847
3344
|
}
|
|
2848
3345
|
|
|
2849
|
-
function
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
3346
|
+
function bytesToProtectedHeaderCells(bytes, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3347
|
+
const base = bytesToCells(bytes, CELL_ENCODINGS.RGBW);
|
|
3348
|
+
if (normalizeCellEncoding(cellEncoding) !== CELL_ENCODINGS.TRIANGLE16) return base;
|
|
3349
|
+
// Triangle16 deliberately keeps the protected bootstrap/header as solid-color
|
|
3350
|
+
// modules. This costs a few cells but makes mode detection and damaged-camera
|
|
3351
|
+
// recovery substantially more reliable.
|
|
3352
|
+
return base.map(solidTriangleCell);
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
function cellsToBytes(cells, byteCount, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3356
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3357
|
+
const cellsPerByte = cellsPerByteForEncoding(encoding);
|
|
3358
|
+
const count = byteCount ?? Math.floor(cells.length / cellsPerByte);
|
|
3359
|
+
assert(cells.length >= count * cellsPerByte, "Not enough data cells to rebuild bytes.");
|
|
3360
|
+
const out = new Uint8Array(count);
|
|
3361
|
+
|
|
3362
|
+
if (encoding === CELL_ENCODINGS.TRIANGLE16) {
|
|
3363
|
+
for (let i = 0; i < count; i++) {
|
|
3364
|
+
const offset = i * TRIANGLE16_CELLS_PER_BYTE;
|
|
3365
|
+
const a = cells[offset];
|
|
3366
|
+
const b = cells[offset + 1];
|
|
3367
|
+
assert(Number.isInteger(a) && a >= 0 && a <= 15, "Invalid Triangle16 data cell.");
|
|
3368
|
+
assert(Number.isInteger(b) && b >= 0 && b <= 15, "Invalid Triangle16 data cell.");
|
|
3369
|
+
out[i] = (a << 4) | b;
|
|
3370
|
+
}
|
|
3371
|
+
return out;
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
for (let i = 0; i < count; i++) {
|
|
3375
|
+
const offset = i * RGBW_CELLS_PER_BYTE;
|
|
2854
3376
|
const a = cells[offset];
|
|
2855
3377
|
const b = cells[offset + 1];
|
|
2856
3378
|
const c = cells[offset + 2];
|
|
@@ -2863,14 +3385,31 @@ function cellsToBytes(cells, byteCount = Math.floor(cells.length / CELLS_PER_BYT
|
|
|
2863
3385
|
return out;
|
|
2864
3386
|
}
|
|
2865
3387
|
|
|
2866
|
-
function
|
|
3388
|
+
function protectedHeaderCellsToBytes(cells, byteCount, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3389
|
+
if (normalizeCellEncoding(cellEncoding) !== CELL_ENCODINGS.TRIANGLE16) {
|
|
3390
|
+
return cellsToBytes(cells, byteCount, CELL_ENCODINGS.RGBW);
|
|
3391
|
+
}
|
|
3392
|
+
const colors = cells.map((value) => {
|
|
3393
|
+
const pair = unpackTriangleCell(value);
|
|
3394
|
+
if (pair.first !== pair.second) throw new Error("Triangle16 protected header lost solid-color integrity.");
|
|
3395
|
+
return pair.first;
|
|
3396
|
+
});
|
|
3397
|
+
return cellsToBytes(colors, byteCount, CELL_ENCODINGS.RGBW);
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
function flagsFor(text, eccLevel, secure = false, extended = false, signed = false, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
2867
3401
|
return (text ? TEXT_FLAG : 0) |
|
|
2868
3402
|
(secure ? SECURE_FLAG : 0) |
|
|
2869
3403
|
(extended ? EXTENDED_PAYLOAD_FLAG : 0) |
|
|
2870
3404
|
(signed ? SIGNED_FLAG : 0) |
|
|
3405
|
+
(normalizeCellEncoding(cellEncoding) === CELL_ENCODINGS.TRIANGLE16 ? TRIANGLE16_FLAG : 0) |
|
|
2871
3406
|
((ECC_LEVELS[eccLevel].id << ECC_SHIFT) & ECC_MASK);
|
|
2872
3407
|
}
|
|
2873
3408
|
|
|
3409
|
+
function cellEncodingFromFlags(flags) {
|
|
3410
|
+
return (flags & TRIANGLE16_FLAG) !== 0 ? CELL_ENCODINGS.TRIANGLE16 : CELL_ENCODINGS.RGBW;
|
|
3411
|
+
}
|
|
3412
|
+
|
|
2874
3413
|
function eccFromFlags(flags) {
|
|
2875
3414
|
const id = (flags & ECC_MASK) >> ECC_SHIFT;
|
|
2876
3415
|
const name = ECC_BY_ID[id];
|
|
@@ -3120,7 +3659,8 @@ function maskValue(row, col, maskId) {
|
|
|
3120
3659
|
}
|
|
3121
3660
|
}
|
|
3122
3661
|
|
|
3123
|
-
function makePaddingCells(count, seed) {
|
|
3662
|
+
function makePaddingCells(count, seed, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3663
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3124
3664
|
let state = (seed >>> 0) || 0x6d2b79f5;
|
|
3125
3665
|
const out = new Array(count);
|
|
3126
3666
|
for (let i = 0; i < count; i++) {
|
|
@@ -3128,11 +3668,27 @@ function makePaddingCells(count, seed) {
|
|
|
3128
3668
|
state ^= state >>> 17;
|
|
3129
3669
|
state ^= state << 5;
|
|
3130
3670
|
state >>>= 0;
|
|
3131
|
-
out[i] = state & 3;
|
|
3671
|
+
out[i] = encoding === CELL_ENCODINGS.TRIANGLE16 ? (state & 0x0f) : (state & 3);
|
|
3132
3672
|
}
|
|
3133
3673
|
return out;
|
|
3134
3674
|
}
|
|
3135
3675
|
|
|
3676
|
+
function triangleMaskValue(row, col, maskId) {
|
|
3677
|
+
const first = maskValue(row, col, maskId);
|
|
3678
|
+
const second = maskValue(row + 1, col + 2, (maskId + 1) & 3);
|
|
3679
|
+
return packTriangleCell(first, second);
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
function applyCellMask(value, row, col, maskId, cellEncoding, protectedHeader = false) {
|
|
3683
|
+
if (cellEncoding !== CELL_ENCODINGS.TRIANGLE16) return value ^ maskValue(row, col, maskId);
|
|
3684
|
+
if (protectedHeader) {
|
|
3685
|
+
const pair = unpackTriangleCell(value);
|
|
3686
|
+
if (pair.first !== pair.second) throw new Error("Triangle16 protected header must use solid-color cells.");
|
|
3687
|
+
return solidTriangleCell(pair.first ^ maskValue(row, col, maskId));
|
|
3688
|
+
}
|
|
3689
|
+
return value ^ triangleMaskValue(row, col, maskId);
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3136
3692
|
const SPECTRAL_PERMUTATION_CACHE = new Map();
|
|
3137
3693
|
|
|
3138
3694
|
function spectralPermutation(length, version) {
|
|
@@ -3161,7 +3717,15 @@ function spectralPermutation(length, version) {
|
|
|
3161
3717
|
return permutation;
|
|
3162
3718
|
}
|
|
3163
3719
|
|
|
3164
|
-
function applyData(
|
|
3720
|
+
function applyData(
|
|
3721
|
+
layout,
|
|
3722
|
+
rawCells,
|
|
3723
|
+
maskId,
|
|
3724
|
+
spectralInterleaving = true,
|
|
3725
|
+
cellEncoding = CELL_ENCODINGS.RGBW,
|
|
3726
|
+
protectedHeaderCells = getHeaderPlan(layout.version).codewordCells
|
|
3727
|
+
) {
|
|
3728
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3165
3729
|
const matrix = cloneMatrix(layout.matrix);
|
|
3166
3730
|
const permutation = spectralInterleaving
|
|
3167
3731
|
? spectralPermutation(layout.dataPositions.length, layout.version)
|
|
@@ -3170,16 +3734,37 @@ function applyData(layout, rawCells, maskId, spectralInterleaving = true) {
|
|
|
3170
3734
|
for (let logicalIndex = 0; logicalIndex < layout.dataPositions.length; logicalIndex++) {
|
|
3171
3735
|
const physicalIndex = permutation ? permutation[logicalIndex] : logicalIndex;
|
|
3172
3736
|
const [row, col] = layout.dataPositions[physicalIndex];
|
|
3173
|
-
matrix[row][col] =
|
|
3737
|
+
matrix[row][col] = applyCellMask(
|
|
3738
|
+
rawCells[logicalIndex],
|
|
3739
|
+
row,
|
|
3740
|
+
col,
|
|
3741
|
+
maskId,
|
|
3742
|
+
encoding,
|
|
3743
|
+
encoding === CELL_ENCODINGS.TRIANGLE16 && logicalIndex < protectedHeaderCells
|
|
3744
|
+
);
|
|
3174
3745
|
}
|
|
3175
3746
|
return matrix;
|
|
3176
3747
|
}
|
|
3177
3748
|
|
|
3178
|
-
function
|
|
3749
|
+
function dataCellPenalty(matrix, reserved, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3750
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3179
3751
|
const size = matrix.length;
|
|
3180
3752
|
let penalty = 0;
|
|
3181
|
-
const
|
|
3182
|
-
let
|
|
3753
|
+
const colorCounts = [0, 0, 0, 0];
|
|
3754
|
+
let colorCount = 0;
|
|
3755
|
+
|
|
3756
|
+
const symbolAt = (r, c) => matrix[r][c];
|
|
3757
|
+
const addColors = (value) => {
|
|
3758
|
+
if (encoding === CELL_ENCODINGS.TRIANGLE16) {
|
|
3759
|
+
const pair = unpackTriangleCell(value);
|
|
3760
|
+
colorCounts[pair.first]++;
|
|
3761
|
+
colorCounts[pair.second]++;
|
|
3762
|
+
colorCount += 2;
|
|
3763
|
+
} else {
|
|
3764
|
+
colorCounts[value]++;
|
|
3765
|
+
colorCount++;
|
|
3766
|
+
}
|
|
3767
|
+
};
|
|
3183
3768
|
|
|
3184
3769
|
for (let r = 0; r < size; r++) {
|
|
3185
3770
|
let previous = null;
|
|
@@ -3190,9 +3775,8 @@ function quaternaryPenalty(matrix, reserved) {
|
|
|
3190
3775
|
run = 0;
|
|
3191
3776
|
continue;
|
|
3192
3777
|
}
|
|
3193
|
-
const value =
|
|
3194
|
-
|
|
3195
|
-
dataCount++;
|
|
3778
|
+
const value = symbolAt(r, c);
|
|
3779
|
+
addColors(value);
|
|
3196
3780
|
if (value === previous) {
|
|
3197
3781
|
run++;
|
|
3198
3782
|
if (run >= 4) penalty += 2;
|
|
@@ -3212,7 +3796,7 @@ function quaternaryPenalty(matrix, reserved) {
|
|
|
3212
3796
|
run = 0;
|
|
3213
3797
|
continue;
|
|
3214
3798
|
}
|
|
3215
|
-
const value =
|
|
3799
|
+
const value = symbolAt(r, c);
|
|
3216
3800
|
if (value === previous) {
|
|
3217
3801
|
run++;
|
|
3218
3802
|
if (run >= 4) penalty += 2;
|
|
@@ -3223,9 +3807,9 @@ function quaternaryPenalty(matrix, reserved) {
|
|
|
3223
3807
|
}
|
|
3224
3808
|
}
|
|
3225
3809
|
|
|
3226
|
-
if (
|
|
3227
|
-
const ideal =
|
|
3228
|
-
penalty +=
|
|
3810
|
+
if (colorCount > 0) {
|
|
3811
|
+
const ideal = colorCount / 4;
|
|
3812
|
+
penalty += colorCounts.reduce((sum, count) => sum + Math.abs(count - ideal), 0) / 2;
|
|
3229
3813
|
}
|
|
3230
3814
|
return penalty;
|
|
3231
3815
|
}
|
|
@@ -3254,7 +3838,7 @@ function deinterleaveBlocks(stream, blockLengths) {
|
|
|
3254
3838
|
return blocks;
|
|
3255
3839
|
}
|
|
3256
3840
|
|
|
3257
|
-
function getBodyRsPlan(payloadLength, eccLevel, version = 2) {
|
|
3841
|
+
function getBodyRsPlan(payloadLength, eccLevel, version = 2, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3258
3842
|
const ecc = getEffectiveEcc(version, eccLevel);
|
|
3259
3843
|
const bodyByteCount = payloadLength + CRC_BYTES;
|
|
3260
3844
|
const dataSymbols = bodyByteCount;
|
|
@@ -3276,25 +3860,27 @@ function getBodyRsPlan(payloadLength, eccLevel, version = 2) {
|
|
|
3276
3860
|
dataBlockLengths,
|
|
3277
3861
|
codewordBlockLengths,
|
|
3278
3862
|
encodedSymbols,
|
|
3279
|
-
encodedCells: encodedSymbols *
|
|
3863
|
+
encodedCells: encodedSymbols * cellsPerByteForEncoding(cellEncoding)
|
|
3280
3864
|
};
|
|
3281
3865
|
}
|
|
3282
3866
|
|
|
3283
|
-
function streamCellCount(payloadLength, eccLevel, version) {
|
|
3284
|
-
return getHeaderPlan(version).codewordCells +
|
|
3867
|
+
function streamCellCount(payloadLength, eccLevel, version, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3868
|
+
return getHeaderPlan(version).codewordCells +
|
|
3869
|
+
getBodyRsPlan(payloadLength, eccLevel, version, cellEncoding).encodedCells;
|
|
3285
3870
|
}
|
|
3286
3871
|
|
|
3287
|
-
function streamFitsLayout(layout, eccLevel, payloadLength, version) {
|
|
3288
|
-
return streamCellCount(payloadLength, eccLevel, version) <= layout.dataPositions.length;
|
|
3872
|
+
function streamFitsLayout(layout, eccLevel, payloadLength, version, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3873
|
+
return streamCellCount(payloadLength, eccLevel, version, cellEncoding) <= layout.dataPositions.length;
|
|
3289
3874
|
}
|
|
3290
3875
|
|
|
3291
|
-
function getCapacityForLayout(layout, eccLevel, version) {
|
|
3292
|
-
|
|
3876
|
+
function getCapacityForLayout(layout, eccLevel, version, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3877
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3878
|
+
if (!streamFitsLayout(layout, eccLevel, 0, version, encoding)) return 0;
|
|
3293
3879
|
let low = 0;
|
|
3294
|
-
let high = Math.floor(layout.dataPositions.length /
|
|
3880
|
+
let high = Math.floor(layout.dataPositions.length / cellsPerByteForEncoding(encoding));
|
|
3295
3881
|
while (low < high) {
|
|
3296
3882
|
const mid = Math.ceil((low + high) / 2);
|
|
3297
|
-
if (streamFitsLayout(layout, eccLevel, mid, version)) low = mid;
|
|
3883
|
+
if (streamFitsLayout(layout, eccLevel, mid, version, encoding)) low = mid;
|
|
3298
3884
|
else high = mid - 1;
|
|
3299
3885
|
}
|
|
3300
3886
|
return low;
|
|
@@ -3303,6 +3889,7 @@ function getCapacityForLayout(layout, eccLevel, version) {
|
|
|
3303
3889
|
function getVersionInfo(version, options = {}) {
|
|
3304
3890
|
validateVersion(version);
|
|
3305
3891
|
const eccLevel = normalizeEccLevel(options.ecc ?? DEFAULT_ECC_LEVEL);
|
|
3892
|
+
const cellEncoding = cellEncodingFromOptions(options);
|
|
3306
3893
|
const layout = createLayout(version);
|
|
3307
3894
|
const headerPlan = getHeaderPlan(version);
|
|
3308
3895
|
const effectiveEcc = getEffectiveEcc(version, eccLevel);
|
|
@@ -3312,8 +3899,8 @@ function getVersionInfo(version, options = {}) {
|
|
|
3312
3899
|
eccLevel,
|
|
3313
3900
|
size: layout.size,
|
|
3314
3901
|
dataCells: layout.dataPositions.length,
|
|
3315
|
-
theoreticalBits: layout.dataPositions.length * 2,
|
|
3316
|
-
capacityBytes: getCapacityForLayout(layout, eccLevel, version),
|
|
3902
|
+
theoreticalBits: layout.dataPositions.length * (cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 4 : 2),
|
|
3903
|
+
capacityBytes: getCapacityForLayout(layout, eccLevel, version, cellEncoding),
|
|
3317
3904
|
headerCells: headerPlan.codewordCells,
|
|
3318
3905
|
headerBytes: headerPlan.headerBytes,
|
|
3319
3906
|
headerParitySymbols: headerPlan.paritySymbols,
|
|
@@ -3325,8 +3912,11 @@ function getVersionInfo(version, options = {}) {
|
|
|
3325
3912
|
hasAlignmentMarker: true,
|
|
3326
3913
|
alignmentPatterns: layout.alignments.length,
|
|
3327
3914
|
alignmentCenters: layout.alignments.map(({ row, col }) => [row, col]),
|
|
3328
|
-
bitsPerDataCell: 2,
|
|
3915
|
+
bitsPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 4 : 2,
|
|
3916
|
+
highDensity: cellEncoding === CELL_ENCODINGS.TRIANGLE16,
|
|
3917
|
+
cellEncoding,
|
|
3329
3918
|
colors: 4,
|
|
3919
|
+
statesPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 16 : 4,
|
|
3330
3920
|
spectralInterleaving: true,
|
|
3331
3921
|
confidenceAwareEcc: true
|
|
3332
3922
|
};
|
|
@@ -3337,6 +3927,7 @@ function chooseVersion(payloadLength, options = {}) {
|
|
|
3337
3927
|
const minVersion = options.minVersion ?? MIN_VERSION;
|
|
3338
3928
|
const maxVersion = options.maxVersion ?? MAX_VERSION;
|
|
3339
3929
|
const ecc = normalizeEccLevel(options.ecc ?? DEFAULT_ECC_LEVEL);
|
|
3930
|
+
const cellEncoding = cellEncodingFromOptions(options);
|
|
3340
3931
|
|
|
3341
3932
|
validateVersion(minVersion);
|
|
3342
3933
|
validateVersion(maxVersion);
|
|
@@ -3345,10 +3936,10 @@ function chooseVersion(payloadLength, options = {}) {
|
|
|
3345
3936
|
if (requested !== "auto") {
|
|
3346
3937
|
validateVersion(requested);
|
|
3347
3938
|
assert(requested >= minVersion && requested <= maxVersion, "Requested version is outside selected bounds.");
|
|
3348
|
-
const info = getVersionInfo(requested, { ecc });
|
|
3939
|
+
const info = getVersionInfo(requested, { ecc, cellEncoding });
|
|
3349
3940
|
const layout = createLayout(requested);
|
|
3350
3941
|
assert(
|
|
3351
|
-
payloadLength <= info.capacityBytes && streamFitsLayout(layout, ecc, payloadLength, requested),
|
|
3942
|
+
payloadLength <= info.capacityBytes && streamFitsLayout(layout, ecc, payloadLength, requested, cellEncoding),
|
|
3352
3943
|
`Payload does not fit version ${requested} with ${ecc} ECC. Maximum is ${info.capacityBytes} bytes.`
|
|
3353
3944
|
);
|
|
3354
3945
|
return requested;
|
|
@@ -3356,20 +3947,20 @@ function chooseVersion(payloadLength, options = {}) {
|
|
|
3356
3947
|
|
|
3357
3948
|
for (let version = minVersion; version <= maxVersion; version++) {
|
|
3358
3949
|
const layout = createLayout(version);
|
|
3359
|
-
if (streamFitsLayout(layout, ecc, payloadLength, version)) return version;
|
|
3950
|
+
if (streamFitsLayout(layout, ecc, payloadLength, version, cellEncoding)) return version;
|
|
3360
3951
|
}
|
|
3361
3952
|
throw new Error(`Payload is too large for versions ${minVersion}..${maxVersion}.`);
|
|
3362
3953
|
}
|
|
3363
3954
|
|
|
3364
|
-
function encodeProtectedHeader(header, version) {
|
|
3955
|
+
function encodeProtectedHeader(header, version, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3365
3956
|
const plan = getHeaderPlan(version);
|
|
3366
3957
|
const codeword = rsEncode(Array.from(header), plan.paritySymbols);
|
|
3367
3958
|
assert(codeword.length === plan.codewordBytes, "Header RS symbol calculation mismatch.");
|
|
3368
|
-
return
|
|
3959
|
+
return bytesToProtectedHeaderCells(codeword, cellEncoding);
|
|
3369
3960
|
}
|
|
3370
3961
|
|
|
3371
|
-
function encodeProtectedBody(bodyBytes, eccLevel, version) {
|
|
3372
|
-
const plan = getBodyRsPlan(bodyBytes.length - CRC_BYTES, eccLevel, version);
|
|
3962
|
+
function encodeProtectedBody(bodyBytes, eccLevel, version, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3963
|
+
const plan = getBodyRsPlan(bodyBytes.length - CRC_BYTES, eccLevel, version, cellEncoding);
|
|
3373
3964
|
assert(bodyBytes.length === plan.dataSymbols, "Body RS symbol calculation mismatch.");
|
|
3374
3965
|
|
|
3375
3966
|
const blocks = [];
|
|
@@ -3378,7 +3969,7 @@ function encodeProtectedBody(bodyBytes, eccLevel, version) {
|
|
|
3378
3969
|
blocks.push(rsEncode(Array.from(bodyBytes.slice(offset, offset + length)), plan.paritySymbols));
|
|
3379
3970
|
offset += length;
|
|
3380
3971
|
}
|
|
3381
|
-
return { cells: bytesToCells(interleaveBlocks(blocks)), plan };
|
|
3972
|
+
return { cells: bytesToCells(interleaveBlocks(blocks), cellEncoding), plan };
|
|
3382
3973
|
}
|
|
3383
3974
|
|
|
3384
3975
|
function finalizeMatrix(layout, rawCells, meta) {
|
|
@@ -3386,9 +3977,10 @@ function finalizeMatrix(layout, rawCells, meta) {
|
|
|
3386
3977
|
let bestMatrix = null;
|
|
3387
3978
|
let bestPenalty = Infinity;
|
|
3388
3979
|
|
|
3980
|
+
const cellEncoding = normalizeCellEncoding(meta.cellEncoding ?? CELL_ENCODINGS.RGBW);
|
|
3389
3981
|
for (let maskId = 0; maskId < 4; maskId++) {
|
|
3390
|
-
const candidate = applyData(layout, rawCells, maskId, true);
|
|
3391
|
-
const penalty =
|
|
3982
|
+
const candidate = applyData(layout, rawCells, maskId, true, cellEncoding);
|
|
3983
|
+
const penalty = dataCellPenalty(candidate, layout.reserved, cellEncoding);
|
|
3392
3984
|
if (penalty < bestPenalty) {
|
|
3393
3985
|
bestPenalty = penalty;
|
|
3394
3986
|
bestMaskId = maskId;
|
|
@@ -3396,7 +3988,7 @@ function finalizeMatrix(layout, rawCells, meta) {
|
|
|
3396
3988
|
}
|
|
3397
3989
|
}
|
|
3398
3990
|
|
|
3399
|
-
const info = getVersionInfo(meta.version, { ecc: meta.eccLevel });
|
|
3991
|
+
const info = getVersionInfo(meta.version, { ecc: meta.eccLevel, cellEncoding });
|
|
3400
3992
|
return {
|
|
3401
3993
|
format: "QuadQR",
|
|
3402
3994
|
formatVersion: FORMAT_VERSION,
|
|
@@ -3419,7 +4011,10 @@ function finalizeMatrix(layout, rawCells, meta) {
|
|
|
3419
4011
|
capacityBytes: info.capacityBytes,
|
|
3420
4012
|
alignmentPatterns: layout.alignments.length,
|
|
3421
4013
|
utilization: meta.meaningfulCells / layout.dataPositions.length,
|
|
3422
|
-
bitsPerDataCell: 2,
|
|
4014
|
+
bitsPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 4 : 2,
|
|
4015
|
+
highDensity: cellEncoding === CELL_ENCODINGS.TRIANGLE16,
|
|
4016
|
+
cellEncoding,
|
|
4017
|
+
statesPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 16 : 4,
|
|
3423
4018
|
eccLevel: meta.eccLevel,
|
|
3424
4019
|
eccParitySymbols: meta.eccParitySymbols,
|
|
3425
4020
|
eccBlocks: meta.eccBlocks,
|
|
@@ -3440,23 +4035,26 @@ function encodePreparedBytes(input, options = {}) {
|
|
|
3440
4035
|
const secure = Boolean(options.secure);
|
|
3441
4036
|
const extended = Boolean(options.extended);
|
|
3442
4037
|
const signed = Boolean(options.signed);
|
|
3443
|
-
const
|
|
3444
|
-
const
|
|
4038
|
+
const cellEncoding = cellEncodingFromOptions(options);
|
|
4039
|
+
const flags = flagsFor(Boolean(options.text), eccLevel, secure, extended, signed, cellEncoding);
|
|
4040
|
+
const version = chooseVersion(payload.length, { ...options, ecc: eccLevel, cellEncoding });
|
|
3445
4041
|
const layout = createLayout(version);
|
|
3446
4042
|
const header = makeHeader(payload.length, flags, version);
|
|
3447
4043
|
const crc = crc32(concatBytes(header, payload));
|
|
3448
|
-
const headerCells = encodeProtectedHeader(header, version);
|
|
3449
|
-
const bodyEncoded = encodeProtectedBody(concatBytes(payload, u32be(crc)), eccLevel, version);
|
|
4044
|
+
const headerCells = encodeProtectedHeader(header, version, cellEncoding);
|
|
4045
|
+
const bodyEncoded = encodeProtectedBody(concatBytes(payload, u32be(crc)), eccLevel, version, cellEncoding);
|
|
3450
4046
|
const meaningfulCells = headerCells.concat(bodyEncoded.cells);
|
|
3451
4047
|
assert(meaningfulCells.length <= layout.dataPositions.length, "Internal QuadQR capacity calculation error.");
|
|
3452
4048
|
|
|
3453
4049
|
const padding = makePaddingCells(
|
|
3454
4050
|
layout.dataPositions.length - meaningfulCells.length,
|
|
3455
|
-
crc ^ payload.length ^ (version << 24) ^ (ECC_LEVELS[eccLevel].id << 16)
|
|
4051
|
+
crc ^ payload.length ^ (version << 24) ^ (ECC_LEVELS[eccLevel].id << 16),
|
|
4052
|
+
cellEncoding
|
|
3456
4053
|
);
|
|
3457
4054
|
|
|
3458
4055
|
return finalizeMatrix(layout, meaningfulCells.concat(padding), {
|
|
3459
4056
|
version,
|
|
4057
|
+
cellEncoding,
|
|
3460
4058
|
payloadBytes: payload.length,
|
|
3461
4059
|
sourcePayloadBytes: options.sourcePayloadBytes ?? payload.length,
|
|
3462
4060
|
secure,
|
|
@@ -3689,12 +4287,14 @@ function rotate90(matrix) {
|
|
|
3689
4287
|
return out;
|
|
3690
4288
|
}
|
|
3691
4289
|
|
|
3692
|
-
function extractVisibleCells(matrix, layout) {
|
|
4290
|
+
function extractVisibleCells(matrix, layout, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
4291
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
4292
|
+
const maxValue = encoding === CELL_ENCODINGS.TRIANGLE16 ? 15 : 3;
|
|
3693
4293
|
const out = [];
|
|
3694
4294
|
for (const [row, col] of layout.dataPositions) {
|
|
3695
4295
|
const value = matrix[row][col];
|
|
3696
|
-
if (!Number.isInteger(value) || value < 0 || value >
|
|
3697
|
-
throw new Error(
|
|
4296
|
+
if (!Number.isInteger(value) || value < 0 || value > maxValue) {
|
|
4297
|
+
throw new Error(`Data region contains an invalid ${encoding === CELL_ENCODINGS.TRIANGLE16 ? "Triangle16" : "RGBW"} data cell.`);
|
|
3698
4298
|
}
|
|
3699
4299
|
out.push(value);
|
|
3700
4300
|
}
|
|
@@ -3708,6 +4308,34 @@ function unmaskCells(visibleCells, positions, maskId) {
|
|
|
3708
4308
|
});
|
|
3709
4309
|
}
|
|
3710
4310
|
|
|
4311
|
+
function unmaskAndRestoreLogicalCells(
|
|
4312
|
+
visibleCells,
|
|
4313
|
+
positions,
|
|
4314
|
+
maskId,
|
|
4315
|
+
version,
|
|
4316
|
+
spectralInterleaving,
|
|
4317
|
+
cellEncoding = CELL_ENCODINGS.RGBW
|
|
4318
|
+
) {
|
|
4319
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
4320
|
+
const permutation = spectralInterleaving ? spectralPermutation(visibleCells.length, version) : null;
|
|
4321
|
+
const logical = new Array(visibleCells.length);
|
|
4322
|
+
const headerCells = getHeaderPlan(version).codewordCells;
|
|
4323
|
+
|
|
4324
|
+
for (let logicalIndex = 0; logicalIndex < visibleCells.length; logicalIndex++) {
|
|
4325
|
+
const physicalIndex = permutation ? permutation[logicalIndex] : logicalIndex;
|
|
4326
|
+
const [row, col] = positions[physicalIndex];
|
|
4327
|
+
logical[logicalIndex] = applyCellMask(
|
|
4328
|
+
visibleCells[physicalIndex],
|
|
4329
|
+
row,
|
|
4330
|
+
col,
|
|
4331
|
+
maskId,
|
|
4332
|
+
encoding,
|
|
4333
|
+
encoding === CELL_ENCODINGS.TRIANGLE16 && logicalIndex < headerCells
|
|
4334
|
+
);
|
|
4335
|
+
}
|
|
4336
|
+
return logical;
|
|
4337
|
+
}
|
|
4338
|
+
|
|
3711
4339
|
function restoreLogicalOrder(physicalValues, version, spectralInterleaving) {
|
|
3712
4340
|
if (!spectralInterleaving) return physicalValues.slice();
|
|
3713
4341
|
const permutation = spectralPermutation(physicalValues.length, version);
|
|
@@ -3718,20 +4346,21 @@ function restoreLogicalOrder(physicalValues, version, spectralInterleaving) {
|
|
|
3718
4346
|
return logical;
|
|
3719
4347
|
}
|
|
3720
4348
|
|
|
3721
|
-
function cellsToSymbolConfidences(
|
|
4349
|
+
function cellsToSymbolConfidences(
|
|
4350
|
+
confidences,
|
|
4351
|
+
byteCount,
|
|
4352
|
+
cellsPerByte = RGBW_CELLS_PER_BYTE
|
|
4353
|
+
) {
|
|
3722
4354
|
if (!confidences) return null;
|
|
3723
|
-
if (confidences.length < byteCount *
|
|
4355
|
+
if (confidences.length < byteCount * cellsPerByte) return null;
|
|
3724
4356
|
const out = new Array(byteCount);
|
|
3725
4357
|
for (let symbolIndex = 0; symbolIndex < byteCount; symbolIndex++) {
|
|
3726
|
-
const offset = symbolIndex *
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
confidences[offset + 2] ?? 1,
|
|
3733
|
-
confidences[offset + 3] ?? 1
|
|
3734
|
-
);
|
|
4358
|
+
const offset = symbolIndex * cellsPerByte;
|
|
4359
|
+
let value = 1;
|
|
4360
|
+
for (let cellIndex = 0; cellIndex < cellsPerByte; cellIndex++) {
|
|
4361
|
+
value = Math.min(value, confidences[offset + cellIndex] ?? 1);
|
|
4362
|
+
}
|
|
4363
|
+
out[symbolIndex] = value;
|
|
3735
4364
|
}
|
|
3736
4365
|
return out;
|
|
3737
4366
|
}
|
|
@@ -3768,11 +4397,11 @@ function decodeRsAdaptive(codeword, paritySymbols, symbolConfidences, options =
|
|
|
3768
4397
|
}
|
|
3769
4398
|
}
|
|
3770
4399
|
|
|
3771
|
-
function decodeProtectedHeader(rawCells, version, rawConfidences = null, options = {}) {
|
|
4400
|
+
function decodeProtectedHeader(rawCells, version, rawConfidences = null, options = {}, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3772
4401
|
const plan = getHeaderPlan(version);
|
|
3773
4402
|
const headerCells = rawCells.slice(0, plan.codewordCells);
|
|
3774
|
-
const codeword = Array.from(
|
|
3775
|
-
const confidences = cellsToSymbolConfidences(rawConfidences?.slice(0, plan.codewordCells), plan.codewordBytes);
|
|
4403
|
+
const codeword = Array.from(protectedHeaderCellsToBytes(headerCells, plan.codewordBytes, cellEncoding));
|
|
4404
|
+
const confidences = cellsToSymbolConfidences(rawConfidences?.slice(0, plan.codewordCells), plan.codewordBytes, RGBW_CELLS_PER_BYTE);
|
|
3776
4405
|
const decoded = decodeRsAdaptive(codeword, plan.paritySymbols, confidences, options);
|
|
3777
4406
|
const header = new Uint8Array(decoded.data);
|
|
3778
4407
|
const parsed = parseHeader(header, version);
|
|
@@ -3787,16 +4416,17 @@ function decodeProtectedHeader(rawCells, version, rawConfidences = null, options
|
|
|
3787
4416
|
};
|
|
3788
4417
|
}
|
|
3789
4418
|
|
|
3790
|
-
function decodeProtectedBody(rawCells, payloadLength, eccLevel, version, rawConfidences = null, options = {}) {
|
|
3791
|
-
const plan = getBodyRsPlan(payloadLength, eccLevel, version);
|
|
4419
|
+
function decodeProtectedBody(rawCells, payloadLength, eccLevel, version, rawConfidences = null, options = {}, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
4420
|
+
const plan = getBodyRsPlan(payloadLength, eccLevel, version, cellEncoding);
|
|
3792
4421
|
const bodyStart = getHeaderPlan(version).codewordCells;
|
|
3793
4422
|
const encodedCells = rawCells.slice(bodyStart, bodyStart + plan.encodedCells);
|
|
3794
4423
|
if (encodedCells.length !== plan.encodedCells) throw new Error("Protected body is incomplete.");
|
|
3795
4424
|
|
|
3796
|
-
const encodedSymbols = Array.from(cellsToBytes(encodedCells, plan.encodedSymbols));
|
|
4425
|
+
const encodedSymbols = Array.from(cellsToBytes(encodedCells, plan.encodedSymbols, cellEncoding));
|
|
3797
4426
|
const symbolConfidences = cellsToSymbolConfidences(
|
|
3798
4427
|
rawConfidences?.slice(bodyStart, bodyStart + plan.encodedCells),
|
|
3799
|
-
plan.encodedSymbols
|
|
4428
|
+
plan.encodedSymbols,
|
|
4429
|
+
cellsPerByteForEncoding(cellEncoding)
|
|
3800
4430
|
);
|
|
3801
4431
|
const blocks = deinterleaveBlocks(encodedSymbols, plan.codewordBlockLengths);
|
|
3802
4432
|
const confidenceBlocks = symbolConfidences
|
|
@@ -3839,96 +4469,136 @@ function decodeCanonical(matrix, rotation, tolerance = 0, confidenceMatrix = nul
|
|
|
3839
4469
|
if (!validateStructure(matrix, tolerance)) throw new Error("QuadQR finder/alignment structure does not match.");
|
|
3840
4470
|
|
|
3841
4471
|
const layout = createLayout(version);
|
|
3842
|
-
const visible = extractVisibleCells(matrix, layout);
|
|
3843
|
-
const visibleConfidences = confidenceMatrix
|
|
3844
|
-
? layout.dataPositions.map(([row, col]) => confidenceMatrix[row]?.[col] ?? 1)
|
|
3845
|
-
: null;
|
|
3846
4472
|
const errors = [];
|
|
4473
|
+
const hintedEncoding = options.cellEncodingHint != null
|
|
4474
|
+
? normalizeCellEncoding(options.cellEncodingHint)
|
|
4475
|
+
: null;
|
|
4476
|
+
const hasExtendedCells = layout.dataPositions.some(([row, col]) => (matrix[row]?.[col] ?? 0) > 3);
|
|
4477
|
+
const encodingAttempts = hintedEncoding
|
|
4478
|
+
? [hintedEncoding]
|
|
4479
|
+
: hasExtendedCells
|
|
4480
|
+
? [CELL_ENCODINGS.TRIANGLE16, CELL_ENCODINGS.RGBW]
|
|
4481
|
+
: [CELL_ENCODINGS.RGBW, CELL_ENCODINGS.TRIANGLE16];
|
|
4482
|
+
|
|
4483
|
+
for (const cellEncoding of encodingAttempts) {
|
|
4484
|
+
let visible;
|
|
4485
|
+
try {
|
|
4486
|
+
visible = extractVisibleCells(matrix, layout, cellEncoding);
|
|
4487
|
+
} catch (error) {
|
|
4488
|
+
errors.push(`${cellEncoding}: ${error.message}`);
|
|
4489
|
+
continue;
|
|
4490
|
+
}
|
|
3847
4491
|
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
for (let maskId = 0; maskId < 4; maskId++) {
|
|
3852
|
-
try {
|
|
3853
|
-
const physicalRaw = unmaskCells(visible, layout.dataPositions, maskId);
|
|
3854
|
-
const raw = restoreLogicalOrder(physicalRaw, version, spectralInterleaving);
|
|
3855
|
-
const rawConfidences = visibleConfidences
|
|
3856
|
-
? restoreLogicalOrder(visibleConfidences, version, spectralInterleaving)
|
|
3857
|
-
: null;
|
|
3858
|
-
const headerDecoded = decodeProtectedHeader(raw, version, rawConfidences, options);
|
|
3859
|
-
const header = headerDecoded.header;
|
|
3860
|
-
const flags = headerDecoded.flags;
|
|
3861
|
-
const eccLevel = eccFromFlags(flags);
|
|
3862
|
-
const payloadLength = headerDecoded.payloadLength;
|
|
3863
|
-
if (streamCellCount(payloadLength, eccLevel, version) > raw.length) {
|
|
3864
|
-
throw new Error("Declared payload exceeds matrix capacity.");
|
|
3865
|
-
}
|
|
4492
|
+
const visibleConfidences = confidenceMatrix
|
|
4493
|
+
? layout.dataPositions.map(([row, col]) => confidenceMatrix[row]?.[col] ?? 1)
|
|
4494
|
+
: null;
|
|
3866
4495
|
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
4496
|
+
// New symbols use spectral-spatial interleaving. Legacy order remains a
|
|
4497
|
+
// decode fallback so existing RGBW QuadQR images do not become unreadable.
|
|
4498
|
+
for (const spectralInterleaving of [true, false]) {
|
|
4499
|
+
for (let maskId = 0; maskId < 4; maskId++) {
|
|
4500
|
+
try {
|
|
4501
|
+
const raw = unmaskAndRestoreLogicalCells(
|
|
4502
|
+
visible,
|
|
4503
|
+
layout.dataPositions,
|
|
4504
|
+
maskId,
|
|
4505
|
+
version,
|
|
4506
|
+
spectralInterleaving,
|
|
4507
|
+
cellEncoding
|
|
4508
|
+
);
|
|
4509
|
+
const rawConfidences = visibleConfidences
|
|
4510
|
+
? restoreLogicalOrder(visibleConfidences, version, spectralInterleaving)
|
|
4511
|
+
: null;
|
|
4512
|
+
const headerDecoded = decodeProtectedHeader(
|
|
4513
|
+
raw,
|
|
4514
|
+
version,
|
|
4515
|
+
rawConfidences,
|
|
4516
|
+
options,
|
|
4517
|
+
cellEncoding
|
|
4518
|
+
);
|
|
4519
|
+
const header = headerDecoded.header;
|
|
4520
|
+
const flags = headerDecoded.flags;
|
|
4521
|
+
const declaredCellEncoding = cellEncodingFromFlags(flags);
|
|
4522
|
+
if (declaredCellEncoding !== cellEncoding) {
|
|
4523
|
+
throw new Error(`Header declares ${declaredCellEncoding}, not ${cellEncoding}.`);
|
|
4524
|
+
}
|
|
4525
|
+
const eccLevel = eccFromFlags(flags);
|
|
4526
|
+
const payloadLength = headerDecoded.payloadLength;
|
|
4527
|
+
if (streamCellCount(payloadLength, eccLevel, version, cellEncoding) > raw.length) {
|
|
4528
|
+
throw new Error("Declared payload exceeds matrix capacity.");
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
const bodyDecoded = decodeProtectedBody(
|
|
4532
|
+
raw,
|
|
4533
|
+
payloadLength,
|
|
4534
|
+
eccLevel,
|
|
4535
|
+
version,
|
|
4536
|
+
rawConfidences,
|
|
4537
|
+
options,
|
|
4538
|
+
cellEncoding
|
|
4539
|
+
);
|
|
4540
|
+
const body = bodyDecoded.body;
|
|
4541
|
+
const payload = body.slice(0, payloadLength);
|
|
4542
|
+
const expectedCrc = readU32be(body, payloadLength);
|
|
4543
|
+
const actualCrc = crc32(concatBytes(header, payload));
|
|
4544
|
+
if (expectedCrc !== actualCrc) throw new Error("CRC mismatch after ECC.");
|
|
4545
|
+
|
|
4546
|
+
const isText = (flags & TEXT_FLAG) !== 0;
|
|
4547
|
+
const secure = (flags & SECURE_FLAG) !== 0;
|
|
4548
|
+
const extendedFlag = (flags & EXTENDED_PAYLOAD_FLAG) !== 0;
|
|
4549
|
+
const signedFlag = (flags & SIGNED_FLAG) !== 0;
|
|
4550
|
+
const security = secure ? inspectSecureEnvelope(payload) : null;
|
|
4551
|
+
const envelope = extendedFlag && !secure ? parsePayloadEnvelope(payload) : null;
|
|
4552
|
+
const applicationPayload = envelope ? envelope.payload : payload;
|
|
4553
|
+
const erasureSymbols = headerDecoded.erasureSymbols + bodyDecoded.erasureSymbols;
|
|
4554
|
+
const unknownErrorSymbols = headerDecoded.unknownErrorSymbols + bodyDecoded.unknownErrorSymbols;
|
|
4555
|
+
return {
|
|
4556
|
+
ok: true,
|
|
4557
|
+
format: "QuadQR",
|
|
4558
|
+
formatVersion: FORMAT_VERSION,
|
|
4559
|
+
version,
|
|
4560
|
+
size,
|
|
4561
|
+
alignmentPatterns: layout.alignments.length,
|
|
4562
|
+
maskId,
|
|
4563
|
+
rotation,
|
|
4564
|
+
flags,
|
|
4565
|
+
highDensity: cellEncoding === CELL_ENCODINGS.TRIANGLE16,
|
|
4566
|
+
cellEncoding,
|
|
4567
|
+
bitsPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 4 : 2,
|
|
4568
|
+
statesPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 16 : 4,
|
|
4569
|
+
eccLevel,
|
|
4570
|
+
eccParitySymbols: bodyDecoded.plan.paritySymbols,
|
|
4571
|
+
eccBlocks: bodyDecoded.plan.dataBlockLengths.length,
|
|
4572
|
+
correctableSymbolsPerBlock: bodyDecoded.plan.correctableSymbolsPerBlock,
|
|
4573
|
+
spectralInterleaving,
|
|
4574
|
+
confidenceAwareEcc: Boolean(rawConfidences),
|
|
4575
|
+
confidenceAssisted: headerDecoded.confidenceAssisted || bodyDecoded.confidenceAssisted,
|
|
4576
|
+
erasureSymbols,
|
|
4577
|
+
unknownErrorSymbols,
|
|
4578
|
+
correctedHeaderSymbols: headerDecoded.correctedSymbols,
|
|
4579
|
+
correctedBodySymbols: bodyDecoded.correctedSymbols,
|
|
4580
|
+
correctedSymbols: headerDecoded.correctedSymbols + bodyDecoded.correctedSymbols,
|
|
4581
|
+
protectedPayload: envelope ? payload : undefined,
|
|
4582
|
+
payload: secure ? payload : applicationPayload,
|
|
4583
|
+
text: secure ? null : (isText ? getTextDecoder().decode(applicationPayload) : null),
|
|
4584
|
+
compressed: envelope?.compressed ?? false,
|
|
4585
|
+
compression: envelope?.compression ?? "none",
|
|
4586
|
+
signed: envelope?.signed ?? signedFlag,
|
|
4587
|
+
signatureVerified: envelope?.signatureVerified,
|
|
4588
|
+
signatureTrusted: envelope?.signatureTrusted,
|
|
4589
|
+
signingKeyId: envelope?.keyId ?? null,
|
|
4590
|
+
hasEmbeddedPublicKey: envelope?.hasEmbeddedPublicKey ?? false,
|
|
4591
|
+
signer: envelope?.signer ?? null,
|
|
4592
|
+
secure,
|
|
4593
|
+
encrypted: secure,
|
|
4594
|
+
decrypted: false,
|
|
4595
|
+
requiresDecryption: secure,
|
|
4596
|
+
security,
|
|
4597
|
+
crc32: actualCrc >>> 0
|
|
4598
|
+
};
|
|
4599
|
+
} catch (error) {
|
|
4600
|
+
errors.push(`${cellEncoding} ${spectralInterleaving ? "spectral" : "legacy"} mask ${maskId}: ${error.message}`);
|
|
4601
|
+
}
|
|
3932
4602
|
}
|
|
3933
4603
|
}
|
|
3934
4604
|
}
|
|
@@ -4045,6 +4715,80 @@ function cellRgb(cell, palette) {
|
|
|
4045
4715
|
}
|
|
4046
4716
|
|
|
4047
4717
|
|
|
4718
|
+
function inferCellEncoding(codeOrMatrix, matrix, layout = null) {
|
|
4719
|
+
if (!Array.isArray(codeOrMatrix) && codeOrMatrix?.cellEncoding) {
|
|
4720
|
+
return normalizeCellEncoding(codeOrMatrix.cellEncoding);
|
|
4721
|
+
}
|
|
4722
|
+
const activeLayout = layout ?? renderLayoutForMatrix(matrix);
|
|
4723
|
+
if (activeLayout) {
|
|
4724
|
+
for (const [row, col] of activeLayout.dataPositions) {
|
|
4725
|
+
if ((matrix[row]?.[col] ?? 0) > 3) return CELL_ENCODINGS.TRIANGLE16;
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
return CELL_ENCODINGS.RGBW;
|
|
4729
|
+
}
|
|
4730
|
+
|
|
4731
|
+
function triangleCellColors(cell) {
|
|
4732
|
+
const { first, second } = unpackTriangleCell(cell);
|
|
4733
|
+
return { first, second };
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4736
|
+
function drawTriangleCellCanvas(ctx, x, y, width, height, cell, palette) {
|
|
4737
|
+
const pair = triangleCellColors(cell);
|
|
4738
|
+
if (pair.first === pair.second) {
|
|
4739
|
+
ctx.fillStyle = cellColor(pair.first, palette);
|
|
4740
|
+
ctx.fillRect(x, y, width, height);
|
|
4741
|
+
return;
|
|
4742
|
+
}
|
|
4743
|
+
|
|
4744
|
+
// Fixed "/" split. first = upper-left triangle, second = lower-right.
|
|
4745
|
+
ctx.beginPath();
|
|
4746
|
+
ctx.moveTo(x, y);
|
|
4747
|
+
ctx.lineTo(x + width, y);
|
|
4748
|
+
ctx.lineTo(x, y + height);
|
|
4749
|
+
ctx.closePath();
|
|
4750
|
+
ctx.fillStyle = cellColor(pair.first, palette);
|
|
4751
|
+
ctx.fill();
|
|
4752
|
+
|
|
4753
|
+
ctx.beginPath();
|
|
4754
|
+
ctx.moveTo(x + width, y);
|
|
4755
|
+
ctx.lineTo(x + width, y + height);
|
|
4756
|
+
ctx.lineTo(x, y + height);
|
|
4757
|
+
ctx.closePath();
|
|
4758
|
+
ctx.fillStyle = cellColor(pair.second, palette);
|
|
4759
|
+
ctx.fill();
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
function fillImageTriangleCell(data, imageWidth, x, y, width, height, cell, palette) {
|
|
4763
|
+
const pair = triangleCellColors(cell);
|
|
4764
|
+
const firstRgb = cellRgb(pair.first, palette);
|
|
4765
|
+
const secondRgb = cellRgb(pair.second, palette);
|
|
4766
|
+
for (let yy = 0; yy < height; yy++) {
|
|
4767
|
+
for (let xx = 0; xx < width; xx++) {
|
|
4768
|
+
const normalizedX = (xx + 0.5) / Math.max(1, width);
|
|
4769
|
+
const normalizedY = (yy + 0.5) / Math.max(1, height);
|
|
4770
|
+
const rgb = normalizedX + normalizedY <= 1 ? firstRgb : secondRgb;
|
|
4771
|
+
const p = ((y + yy) * imageWidth + (x + xx)) * 4;
|
|
4772
|
+
data[p] = rgb.r;
|
|
4773
|
+
data[p + 1] = rgb.g;
|
|
4774
|
+
data[p + 2] = rgb.b;
|
|
4775
|
+
data[p + 3] = 255;
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
|
|
4780
|
+
function svgTriangleCell(x, y, size, cell, palette) {
|
|
4781
|
+
const pair = triangleCellColors(cell);
|
|
4782
|
+
if (pair.first === pair.second) return svgRect(x, y, size, size, cellColor(pair.first, palette));
|
|
4783
|
+
const first = cellColor(pair.first, palette);
|
|
4784
|
+
const second = cellColor(pair.second, palette);
|
|
4785
|
+
return [
|
|
4786
|
+
`<polygon points="${x},${y} ${x + size},${y} ${x},${y + size}" fill="${first}"/>`,
|
|
4787
|
+
`<polygon points="${x + size},${y} ${x + size},${y + size} ${x},${y + size}" fill="${second}"/>`
|
|
4788
|
+
].join("");
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
|
|
4048
4792
|
function normalizeRenderStyle(style = RENDER_STYLES.CLASSIC) {
|
|
4049
4793
|
const value = String(style).toLowerCase();
|
|
4050
4794
|
const allowed = Object.values(RENDER_STYLES);
|
|
@@ -4514,6 +5258,7 @@ function renderToCanvas(codeOrMatrix, canvas, options = {}) {
|
|
|
4514
5258
|
const mode = normalizeRenderMode(options.mode ?? options.renderMode ?? RENDER_MODES.SCREEN);
|
|
4515
5259
|
const style = normalizeRenderStyle(mode === RENDER_MODES.PRINT && options.allowStyledPrint !== true ? RENDER_STYLES.CLASSIC : options.style);
|
|
4516
5260
|
const layout = renderLayoutForMatrix(matrix);
|
|
5261
|
+
const cellEncoding = inferCellEncoding(codeOrMatrix, matrix, layout);
|
|
4517
5262
|
const size = matrix.length;
|
|
4518
5263
|
const logo = resolveLogoRenderOptions(options, size, moduleSize, quietZone, palette, codeOrMatrix);
|
|
4519
5264
|
|
|
@@ -4532,6 +5277,14 @@ function renderToCanvas(codeOrMatrix, canvas, options = {}) {
|
|
|
4532
5277
|
const { x, y, width: cellWidth, height: cellHeight } = rect;
|
|
4533
5278
|
const cellSize = Math.min(cellWidth, cellHeight);
|
|
4534
5279
|
|
|
5280
|
+
if (cellEncoding === CELL_ENCODINGS.TRIANGLE16 && !structural) {
|
|
5281
|
+
// Experimental Triangle16 always renders payload cells with exact,
|
|
5282
|
+
// hard-edged geometry. Decorative per-cell styles would contaminate the
|
|
5283
|
+
// two independent color samples and reduce scan reliability.
|
|
5284
|
+
drawTriangleCellCanvas(ctx, x, y, cellWidth, cellHeight, cell, palette);
|
|
5285
|
+
continue;
|
|
5286
|
+
}
|
|
5287
|
+
|
|
4535
5288
|
if (style === RENDER_STYLES.CLASSIC || structural) {
|
|
4536
5289
|
ctx.fillStyle = cellColor(cell, palette);
|
|
4537
5290
|
ctx.fillRect(x, y, cellWidth, cellHeight);
|
|
@@ -4605,6 +5358,7 @@ function renderToImageData(codeOrMatrix, options = {}) {
|
|
|
4605
5358
|
const mode = normalizeRenderMode(options.mode ?? options.renderMode ?? RENDER_MODES.SCREEN);
|
|
4606
5359
|
const style = normalizeRenderStyle(mode === RENDER_MODES.PRINT && options.allowStyledPrint !== true ? RENDER_STYLES.CLASSIC : options.style);
|
|
4607
5360
|
const layout = renderLayoutForMatrix(matrix);
|
|
5361
|
+
const cellEncoding = inferCellEncoding(codeOrMatrix, matrix, layout);
|
|
4608
5362
|
const size = matrix.length;
|
|
4609
5363
|
const data = new Uint8ClampedArray(pixelSize * pixelSize * 4);
|
|
4610
5364
|
const white = palette.white;
|
|
@@ -4621,7 +5375,6 @@ function renderToImageData(codeOrMatrix, options = {}) {
|
|
|
4621
5375
|
for (let r = 0; r < size; r++) {
|
|
4622
5376
|
for (let c = 0; c < size; c++) {
|
|
4623
5377
|
const cell = matrix[r][c];
|
|
4624
|
-
const rgb = cellRgb(cell, palette);
|
|
4625
5378
|
const structural = isStructuralRenderCell(layout, r, c, cell);
|
|
4626
5379
|
const rect = rasterModuleRect(r, c, quietZone, moduleSize);
|
|
4627
5380
|
const x0 = rect.x;
|
|
@@ -4630,6 +5383,13 @@ function renderToImageData(codeOrMatrix, options = {}) {
|
|
|
4630
5383
|
const cellHeight = rect.height;
|
|
4631
5384
|
const cellSize = Math.min(cellWidth, cellHeight);
|
|
4632
5385
|
|
|
5386
|
+
if (cellEncoding === CELL_ENCODINGS.TRIANGLE16 && !structural) {
|
|
5387
|
+
fillImageTriangleCell(data, pixelSize, x0, y0, cellWidth, cellHeight, cell, palette);
|
|
5388
|
+
continue;
|
|
5389
|
+
}
|
|
5390
|
+
|
|
5391
|
+
const rgb = cellRgb(cell, palette);
|
|
5392
|
+
|
|
4633
5393
|
if (style === RENDER_STYLES.CLASSIC || structural) {
|
|
4634
5394
|
fillImageRect(data, pixelSize, x0, y0, cellWidth, cellHeight, rgb);
|
|
4635
5395
|
continue;
|
|
@@ -4691,6 +5451,7 @@ function renderToSVG(codeOrMatrix, options = {}) {
|
|
|
4691
5451
|
const mode = normalizeRenderMode(options.mode ?? options.renderMode ?? RENDER_MODES.SCREEN);
|
|
4692
5452
|
const style = normalizeRenderStyle(mode === RENDER_MODES.PRINT && options.allowStyledPrint !== true ? RENDER_STYLES.CLASSIC : options.style);
|
|
4693
5453
|
const layout = renderLayoutForMatrix(matrix);
|
|
5454
|
+
const cellEncoding = inferCellEncoding(codeOrMatrix, matrix, layout);
|
|
4694
5455
|
const size = matrix.length;
|
|
4695
5456
|
const logo = resolveLogoRenderOptions(options, size, moduleSize, quietZone, palette, codeOrMatrix);
|
|
4696
5457
|
const body = [svgRect(0, 0, pixelSize, pixelSize, palette.white)];
|
|
@@ -4701,7 +5462,12 @@ function renderToSVG(codeOrMatrix, options = {}) {
|
|
|
4701
5462
|
const structural = isStructuralRenderCell(layout, r, c, cell);
|
|
4702
5463
|
const x = (c + quietZone) * moduleSize;
|
|
4703
5464
|
const y = (r + quietZone) * moduleSize;
|
|
4704
|
-
const fill = cellColor(cell, palette);
|
|
5465
|
+
const fill = cellEncoding === CELL_ENCODINGS.TRIANGLE16 && !structural ? null : cellColor(cell, palette);
|
|
5466
|
+
|
|
5467
|
+
if (cellEncoding === CELL_ENCODINGS.TRIANGLE16 && !structural) {
|
|
5468
|
+
body.push(svgTriangleCell(x, y, moduleSize, cell, palette));
|
|
5469
|
+
continue;
|
|
5470
|
+
}
|
|
4705
5471
|
|
|
4706
5472
|
if (style === RENDER_STYLES.CLASSIC || structural) {
|
|
4707
5473
|
body.push(svgRect(x, y, moduleSize, moduleSize, fill));
|
|
@@ -4928,6 +5694,60 @@ function classifySampledRgbGrid(rgbGrid, classifier, layout = null) {
|
|
|
4928
5694
|
};
|
|
4929
5695
|
}
|
|
4930
5696
|
|
|
5697
|
+
function classifyTriangleSampledRgbGrid(rgbGrid, triangleGrid, classifier, layout) {
|
|
5698
|
+
const size = rgbGrid.length;
|
|
5699
|
+
const matrix = make2D(size, CELL.WHITE);
|
|
5700
|
+
const confidence = make2D(size, 1);
|
|
5701
|
+
const dataClassifier = {
|
|
5702
|
+
...classifier,
|
|
5703
|
+
entries: classifier.entries.filter(({ cell }) => cell !== CELL.BLACK)
|
|
5704
|
+
};
|
|
5705
|
+
let distanceSum = 0;
|
|
5706
|
+
let confidenceSum = 0;
|
|
5707
|
+
let minimumConfidence = 1;
|
|
5708
|
+
let lowConfidenceCells = 0;
|
|
5709
|
+
|
|
5710
|
+
for (let r = 0; r < size; r++) {
|
|
5711
|
+
for (let c = 0; c < size; c++) {
|
|
5712
|
+
const isDataCell = layout && !layout.reserved[r][c];
|
|
5713
|
+
if (!isDataCell) {
|
|
5714
|
+
const classified = classifyRgb(rgbGrid[r][c], classifier);
|
|
5715
|
+
matrix[r][c] = classified.cell;
|
|
5716
|
+
confidence[r][c] = classified.confidence;
|
|
5717
|
+
distanceSum += classified.distance;
|
|
5718
|
+
confidenceSum += classified.confidence;
|
|
5719
|
+
minimumConfidence = Math.min(minimumConfidence, classified.confidence);
|
|
5720
|
+
if (classified.confidence < 0.4) lowConfidenceCells++;
|
|
5721
|
+
continue;
|
|
5722
|
+
}
|
|
5723
|
+
|
|
5724
|
+
const samples = triangleGrid?.[r]?.[c];
|
|
5725
|
+
if (!samples) throw new Error("Triangle16 sampling grid is incomplete.");
|
|
5726
|
+
const classifyData = (rgb) => classifier.dataMode === "hue"
|
|
5727
|
+
? classifyDataHue(rgb, classifier)
|
|
5728
|
+
: classifyRgb(rgb, dataClassifier);
|
|
5729
|
+
const first = classifyData(samples.first);
|
|
5730
|
+
const second = classifyData(samples.second);
|
|
5731
|
+
const cellConfidence = Math.min(first.confidence, second.confidence);
|
|
5732
|
+
matrix[r][c] = packTriangleCell(first.cell, second.cell);
|
|
5733
|
+
confidence[r][c] = cellConfidence;
|
|
5734
|
+
distanceSum += (first.distance + second.distance) / 2;
|
|
5735
|
+
confidenceSum += cellConfidence;
|
|
5736
|
+
minimumConfidence = Math.min(minimumConfidence, cellConfidence);
|
|
5737
|
+
if (cellConfidence < 0.4) lowConfidenceCells++;
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
|
|
5741
|
+
return {
|
|
5742
|
+
matrix,
|
|
5743
|
+
confidence,
|
|
5744
|
+
averageColorDistance: distanceSum / (size * size),
|
|
5745
|
+
averageCellConfidence: confidenceSum / (size * size),
|
|
5746
|
+
minimumCellConfidence: minimumConfidence,
|
|
5747
|
+
lowConfidenceCells
|
|
5748
|
+
};
|
|
5749
|
+
}
|
|
5750
|
+
|
|
4931
5751
|
function structuralAccuracy(matrix, layout) {
|
|
4932
5752
|
let matches = 0;
|
|
4933
5753
|
let total = 0;
|
|
@@ -4967,6 +5787,7 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
4967
5787
|
finderAutoColorAnalysisInset: options.finderAutoColorAnalysisInset,
|
|
4968
5788
|
finderAutoColorMinimumInputRange: options.finderAutoColorMinimumInputRange,
|
|
4969
5789
|
finderAutoColorTargetSamples: options.finderAutoColorTargetSamples,
|
|
5790
|
+
preciseAlignment: options.preciseAlignment,
|
|
4970
5791
|
diagnostics: options._visionDiagnostics,
|
|
4971
5792
|
diagnosticLabel: options._diagnosticLabel ?? "normal"
|
|
4972
5793
|
});
|
|
@@ -4992,9 +5813,18 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
4992
5813
|
let bestStructureScore = 0;
|
|
4993
5814
|
for (const profile of sampleProfiles) {
|
|
4994
5815
|
let sampled;
|
|
5816
|
+
let triangleSampled = null;
|
|
4995
5817
|
let observedPalette;
|
|
4996
5818
|
try {
|
|
4997
5819
|
sampled = samplePerspectiveMatrix(imageData, geometry.homography, layout.size, profile);
|
|
5820
|
+
if (options.highDensitySampling !== false && options.triangle16 !== false) {
|
|
5821
|
+
triangleSampled = samplePerspectiveTriangleMatrix(
|
|
5822
|
+
imageData,
|
|
5823
|
+
geometry.homography,
|
|
5824
|
+
layout.size,
|
|
5825
|
+
profile
|
|
5826
|
+
);
|
|
5827
|
+
}
|
|
4998
5828
|
observedPalette = sampleObservedPalette(sampled.rgbGrid, layout.calibration, {
|
|
4999
5829
|
robust: profile.robustCalibration
|
|
5000
5830
|
});
|
|
@@ -5053,11 +5883,67 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
5053
5883
|
}
|
|
5054
5884
|
};
|
|
5055
5885
|
|
|
5886
|
+
const tryTriangleAttempt = (attempt, metadata = {}) => {
|
|
5887
|
+
if (!triangleSampled?.triangleGrid) return false;
|
|
5888
|
+
const activeObservedPalette = metadata.observedPalette ?? observedPalette;
|
|
5889
|
+
const activeSamplingMode = metadata.samplingMode ?? `${profile.sampleMode}-triangle16`;
|
|
5890
|
+
const classified = classifyTriangleSampledRgbGrid(
|
|
5891
|
+
sampled.rgbGrid,
|
|
5892
|
+
triangleSampled.triangleGrid,
|
|
5893
|
+
attempt.classifier,
|
|
5894
|
+
layout
|
|
5895
|
+
);
|
|
5896
|
+
const structureScore = structuralAccuracy(classified.matrix, layout);
|
|
5897
|
+
bestStructureScore = Math.max(bestStructureScore, structureScore);
|
|
5898
|
+
pushObservation(options, {
|
|
5899
|
+
version: geometry.version,
|
|
5900
|
+
matrix: classified.matrix,
|
|
5901
|
+
confidence: classified.confidence,
|
|
5902
|
+
geometry,
|
|
5903
|
+
observedPalette: activeObservedPalette,
|
|
5904
|
+
samplingMode: activeSamplingMode,
|
|
5905
|
+
colorNormalization: attempt.colorNormalization,
|
|
5906
|
+
cellEncoding: CELL_ENCODINGS.TRIANGLE16,
|
|
5907
|
+
structureScore,
|
|
5908
|
+
averageCellConfidence: classified.averageCellConfidence,
|
|
5909
|
+
lowConfidenceCells: classified.lowConfidenceCells
|
|
5910
|
+
});
|
|
5911
|
+
|
|
5912
|
+
try {
|
|
5913
|
+
const decoded = decodeMatrix(classified.matrix, {
|
|
5914
|
+
structureTolerance: options.structureTolerance ?? 0.18,
|
|
5915
|
+
cellConfidence: classified.confidence,
|
|
5916
|
+
cellEncodingHint: CELL_ENCODINGS.TRIANGLE16,
|
|
5917
|
+
maxErasureConfidence: options.maxErasureConfidence
|
|
5918
|
+
});
|
|
5919
|
+
if (decoded.version !== geometry.version) return false;
|
|
5920
|
+
results.push({
|
|
5921
|
+
...decoded,
|
|
5922
|
+
perspectiveCorrected: true,
|
|
5923
|
+
colorCalibrated: true,
|
|
5924
|
+
colorNormalization: attempt.colorNormalization,
|
|
5925
|
+
samplingMode: activeSamplingMode,
|
|
5926
|
+
geometry,
|
|
5927
|
+
observedPalette: activeObservedPalette,
|
|
5928
|
+
averageColorDistance: classified.averageColorDistance,
|
|
5929
|
+
averageCellConfidence: classified.averageCellConfidence,
|
|
5930
|
+
minimumCellConfidence: classified.minimumCellConfidence,
|
|
5931
|
+
lowConfidenceCells: classified.lowConfidenceCells,
|
|
5932
|
+
rectified: options.includeRectified
|
|
5933
|
+
? rectifyImageData(imageData, geometry.homography, layout.size, options.rectifiedModuleSize ?? 8)
|
|
5934
|
+
: undefined
|
|
5935
|
+
});
|
|
5936
|
+
return true;
|
|
5937
|
+
} catch {
|
|
5938
|
+
return false;
|
|
5939
|
+
}
|
|
5940
|
+
};
|
|
5941
|
+
|
|
5056
5942
|
// Fast path: preserve the original observed-RGB classifier first, then
|
|
5057
5943
|
// try per-channel white balancing. Most clean frames stop here without
|
|
5058
5944
|
// paying for the more expensive spatial normalization fallback.
|
|
5059
5945
|
for (const attempt of paletteClassifierAttempts(observedPalette)) {
|
|
5060
|
-
if (tryAttempt(attempt, sampled.rgbGrid)) {
|
|
5946
|
+
if (tryAttempt(attempt, sampled.rgbGrid) || tryTriangleAttempt(attempt)) {
|
|
5061
5947
|
geometryDecoded = true;
|
|
5062
5948
|
break;
|
|
5063
5949
|
}
|
|
@@ -5184,17 +6070,30 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
5184
6070
|
[step, -step], [step, step], [-step, -step], [-step, step]
|
|
5185
6071
|
];
|
|
5186
6072
|
const refinementCandidates = [];
|
|
6073
|
+
const triangleRefinementCandidates = [];
|
|
5187
6074
|
|
|
5188
6075
|
for (const [offsetX, offsetY] of refinementOffsets) {
|
|
5189
6076
|
let sampled;
|
|
6077
|
+
let triangleSampled = null;
|
|
5190
6078
|
let observedPalette;
|
|
5191
6079
|
try {
|
|
5192
|
-
|
|
6080
|
+
const samplingOptions = {
|
|
5193
6081
|
sampleMode: "cross",
|
|
5194
6082
|
sampleRadius: 0,
|
|
5195
6083
|
sampleOffsetX: offsetX,
|
|
5196
|
-
sampleOffsetY: offsetY
|
|
5197
|
-
|
|
6084
|
+
sampleOffsetY: offsetY,
|
|
6085
|
+
triangleSampleInset: options.highDensitySampleInset ?? options.triangleSampleInset,
|
|
6086
|
+
triangleSampleRadius: options.highDensitySampleRadius ?? options.triangleSampleRadius
|
|
6087
|
+
};
|
|
6088
|
+
sampled = samplePerspectiveMatrix(imageData, geometry.homography, layout.size, samplingOptions);
|
|
6089
|
+
if (options.highDensitySampling !== false && options.triangle16 !== false) {
|
|
6090
|
+
triangleSampled = samplePerspectiveTriangleMatrix(
|
|
6091
|
+
imageData,
|
|
6092
|
+
geometry.homography,
|
|
6093
|
+
layout.size,
|
|
6094
|
+
samplingOptions
|
|
6095
|
+
);
|
|
6096
|
+
}
|
|
5198
6097
|
observedPalette = sampleObservedPalette(sampled.rgbGrid, layout.calibration, { robust: true });
|
|
5199
6098
|
} catch {
|
|
5200
6099
|
continue;
|
|
@@ -5224,30 +6123,80 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
5224
6123
|
|
|
5225
6124
|
// Do not spend RS work on a refinement that made the known finder /
|
|
5226
6125
|
// alignment structure worse. This keeps the recovery path bounded.
|
|
5227
|
-
if (structureScore
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
6126
|
+
if (structureScore >= (options.refinementDecodeThreshold ?? 0.95)) {
|
|
6127
|
+
refinementCandidates.push({
|
|
6128
|
+
attempt,
|
|
6129
|
+
classified,
|
|
6130
|
+
observedPalette,
|
|
6131
|
+
offsetX,
|
|
6132
|
+
offsetY,
|
|
6133
|
+
structureScore,
|
|
6134
|
+
cellEncoding: CELL_ENCODINGS.RGBW,
|
|
6135
|
+
samplingMode: "refined-center"
|
|
6136
|
+
});
|
|
6137
|
+
}
|
|
6138
|
+
|
|
6139
|
+
if (triangleSampled?.triangleGrid) {
|
|
6140
|
+
const triangleClassified = classifyTriangleSampledRgbGrid(
|
|
6141
|
+
sampled.rgbGrid,
|
|
6142
|
+
triangleSampled.triangleGrid,
|
|
6143
|
+
attempt.classifier,
|
|
6144
|
+
layout
|
|
6145
|
+
);
|
|
6146
|
+
const triangleStructureScore = structuralAccuracy(triangleClassified.matrix, layout);
|
|
6147
|
+
bestStructureScore = Math.max(bestStructureScore, triangleStructureScore);
|
|
6148
|
+
pushObservation(options, {
|
|
6149
|
+
version: geometry.version,
|
|
6150
|
+
matrix: triangleClassified.matrix,
|
|
6151
|
+
confidence: triangleClassified.confidence,
|
|
6152
|
+
geometry,
|
|
6153
|
+
observedPalette,
|
|
6154
|
+
samplingMode: "refined-triangle16",
|
|
6155
|
+
samplingOffset: { x: offsetX, y: offsetY },
|
|
6156
|
+
colorNormalization: attempt.colorNormalization,
|
|
6157
|
+
cellEncoding: CELL_ENCODINGS.TRIANGLE16,
|
|
6158
|
+
structureScore: triangleStructureScore,
|
|
6159
|
+
averageCellConfidence: triangleClassified.averageCellConfidence,
|
|
6160
|
+
lowConfidenceCells: triangleClassified.lowConfidenceCells
|
|
6161
|
+
});
|
|
6162
|
+
if (triangleStructureScore >= (options.refinementDecodeThreshold ?? 0.95)) {
|
|
6163
|
+
triangleRefinementCandidates.push({
|
|
6164
|
+
attempt,
|
|
6165
|
+
classified: triangleClassified,
|
|
6166
|
+
observedPalette,
|
|
6167
|
+
offsetX,
|
|
6168
|
+
offsetY,
|
|
6169
|
+
structureScore: triangleStructureScore,
|
|
6170
|
+
cellEncoding: CELL_ENCODINGS.TRIANGLE16,
|
|
6171
|
+
samplingMode: "refined-triangle16"
|
|
6172
|
+
});
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
5236
6175
|
}
|
|
5237
6176
|
}
|
|
5238
6177
|
|
|
5239
|
-
|
|
6178
|
+
const sortRefinementCandidates = (items) => items.sort((a, b) =>
|
|
5240
6179
|
(b.structureScore - a.structureScore) ||
|
|
5241
6180
|
(b.classified.averageCellConfidence - a.classified.averageCellConfidence) ||
|
|
5242
6181
|
(a.classified.averageColorDistance - b.classified.averageColorDistance)
|
|
5243
6182
|
);
|
|
6183
|
+
sortRefinementCandidates(refinementCandidates);
|
|
6184
|
+
sortRefinementCandidates(triangleRefinementCandidates);
|
|
5244
6185
|
|
|
5245
6186
|
const decodeLimit = Math.max(1, Math.min(8, Math.round(options.refinementDecodeCandidates ?? 4)));
|
|
5246
|
-
|
|
6187
|
+
// Keep separate per-encoding limits. A Triangle16 image can produce very
|
|
6188
|
+
// confident but meaningless centre samples, and those must not crowd out
|
|
6189
|
+
// the dual-region candidates during the final bounded recovery pass.
|
|
6190
|
+
const decodeCandidates = [
|
|
6191
|
+
...refinementCandidates.slice(0, decodeLimit),
|
|
6192
|
+
...triangleRefinementCandidates.slice(0, decodeLimit)
|
|
6193
|
+
];
|
|
6194
|
+
for (const candidate of decodeCandidates) {
|
|
5247
6195
|
try {
|
|
5248
6196
|
const decoded = decodeMatrix(candidate.classified.matrix, {
|
|
5249
6197
|
structureTolerance: options.structureTolerance ?? 0.18,
|
|
5250
6198
|
cellConfidence: candidate.classified.confidence,
|
|
6199
|
+
cellEncodingHint: candidate.cellEncoding,
|
|
5251
6200
|
maxErasureConfidence: options.maxErasureConfidence
|
|
5252
6201
|
});
|
|
5253
6202
|
if (decoded.version !== geometry.version) continue;
|
|
@@ -5258,7 +6207,7 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
5258
6207
|
samplingOffset: { x: candidate.offsetX, y: candidate.offsetY },
|
|
5259
6208
|
colorCalibrated: true,
|
|
5260
6209
|
colorNormalization: candidate.attempt.colorNormalization,
|
|
5261
|
-
samplingMode:
|
|
6210
|
+
samplingMode: candidate.samplingMode,
|
|
5262
6211
|
geometry,
|
|
5263
6212
|
observedPalette: candidate.observedPalette,
|
|
5264
6213
|
averageColorDistance: candidate.classified.averageColorDistance,
|
|
@@ -5308,6 +6257,15 @@ function tryAxisAlignedScan(imageData, options) {
|
|
|
5308
6257
|
for (let profileIndex = 0; profileIndex < radiusProfiles.length && !accepted; profileIndex++) {
|
|
5309
6258
|
try {
|
|
5310
6259
|
const sampled = sampleAxisAlignedGrid(imageData, bounds, size, radiusProfiles[profileIndex]);
|
|
6260
|
+
const triangleSampled = options.highDensitySampling === false || options.triangle16 === false
|
|
6261
|
+
? null
|
|
6262
|
+
: sampleAxisAlignedTriangleGrid(
|
|
6263
|
+
imageData,
|
|
6264
|
+
bounds,
|
|
6265
|
+
size,
|
|
6266
|
+
Math.min(0.10, Math.max(0.04, radiusProfiles[profileIndex] * 0.48)),
|
|
6267
|
+
options.highDensitySampleInset ?? options.triangleSampleInset ?? 0.28
|
|
6268
|
+
);
|
|
5311
6269
|
const layout = createLayout(version);
|
|
5312
6270
|
const classifierAttempts = [];
|
|
5313
6271
|
try {
|
|
@@ -5347,45 +6305,70 @@ function tryAxisAlignedScan(imageData, options) {
|
|
|
5347
6305
|
});
|
|
5348
6306
|
|
|
5349
6307
|
for (const attempt of classifierAttempts) {
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
6308
|
+
const centerGrid = attempt.rgbGrid ?? sampled.rgbGrid;
|
|
6309
|
+
const samplingMode = profileIndex === 0 ? "axis" : "axis-center";
|
|
6310
|
+
const classifiedAttempts = [{
|
|
6311
|
+
classified: classifySampledRgbGrid(centerGrid, attempt.classifier, layout),
|
|
6312
|
+
cellEncoding: CELL_ENCODINGS.RGBW,
|
|
6313
|
+
samplingMode
|
|
6314
|
+
}];
|
|
6315
|
+
if (triangleSampled?.triangleGrid && attempt.rgbGrid === sampled.rgbGrid) {
|
|
6316
|
+
classifiedAttempts.push({
|
|
6317
|
+
classified: classifyTriangleSampledRgbGrid(
|
|
6318
|
+
centerGrid,
|
|
6319
|
+
triangleSampled.triangleGrid,
|
|
6320
|
+
attempt.classifier,
|
|
6321
|
+
layout
|
|
6322
|
+
),
|
|
6323
|
+
cellEncoding: CELL_ENCODINGS.TRIANGLE16,
|
|
6324
|
+
samplingMode: `${samplingMode}-triangle16`
|
|
5363
6325
|
});
|
|
5364
|
-
const decoded = decodeMatrix(classified.matrix, {
|
|
5365
|
-
structureTolerance: options.structureTolerance ?? 0.12,
|
|
5366
|
-
cellConfidence: classified.confidence,
|
|
5367
|
-
maxErasureConfidence: options.maxErasureConfidence
|
|
5368
|
-
});
|
|
5369
|
-
candidates.push({
|
|
5370
|
-
...decoded,
|
|
5371
|
-
bounds,
|
|
5372
|
-
sampledVersion: version,
|
|
5373
|
-
moduleWidth: moduleW,
|
|
5374
|
-
moduleHeight: moduleH,
|
|
5375
|
-
perspectiveCorrected: false,
|
|
5376
|
-
colorCalibrated: attempt.calibrated,
|
|
5377
|
-
colorNormalization: attempt.colorNormalization,
|
|
5378
|
-
samplingMode: profileIndex === 0 ? "axis" : "axis-center",
|
|
5379
|
-
averageColorDistance: classified.averageColorDistance,
|
|
5380
|
-
averageCellConfidence: classified.averageCellConfidence,
|
|
5381
|
-
minimumCellConfidence: classified.minimumCellConfidence,
|
|
5382
|
-
lowConfidenceCells: classified.lowConfidenceCells
|
|
5383
|
-
});
|
|
5384
|
-
accepted = true;
|
|
5385
|
-
break;
|
|
5386
|
-
} catch {
|
|
5387
|
-
// Try next classifier/profile.
|
|
5388
6326
|
}
|
|
6327
|
+
|
|
6328
|
+
for (const classifiedAttempt of classifiedAttempts) {
|
|
6329
|
+
try {
|
|
6330
|
+
const classified = classifiedAttempt.classified;
|
|
6331
|
+
const structureScore = structuralAccuracy(classified.matrix, layout);
|
|
6332
|
+
pushObservation(options, {
|
|
6333
|
+
version,
|
|
6334
|
+
matrix: classified.matrix,
|
|
6335
|
+
confidence: classified.confidence,
|
|
6336
|
+
bounds,
|
|
6337
|
+
samplingMode: classifiedAttempt.samplingMode,
|
|
6338
|
+
colorNormalization: attempt.colorNormalization,
|
|
6339
|
+
cellEncoding: classifiedAttempt.cellEncoding,
|
|
6340
|
+
structureScore,
|
|
6341
|
+
averageCellConfidence: classified.averageCellConfidence,
|
|
6342
|
+
lowConfidenceCells: classified.lowConfidenceCells
|
|
6343
|
+
});
|
|
6344
|
+
const decoded = decodeMatrix(classified.matrix, {
|
|
6345
|
+
structureTolerance: options.structureTolerance ?? 0.12,
|
|
6346
|
+
cellConfidence: classified.confidence,
|
|
6347
|
+
cellEncodingHint: classifiedAttempt.cellEncoding,
|
|
6348
|
+
maxErasureConfidence: options.maxErasureConfidence
|
|
6349
|
+
});
|
|
6350
|
+
candidates.push({
|
|
6351
|
+
...decoded,
|
|
6352
|
+
bounds,
|
|
6353
|
+
sampledVersion: version,
|
|
6354
|
+
moduleWidth: moduleW,
|
|
6355
|
+
moduleHeight: moduleH,
|
|
6356
|
+
perspectiveCorrected: false,
|
|
6357
|
+
colorCalibrated: attempt.calibrated,
|
|
6358
|
+
colorNormalization: attempt.colorNormalization,
|
|
6359
|
+
samplingMode: classifiedAttempt.samplingMode,
|
|
6360
|
+
averageColorDistance: classified.averageColorDistance,
|
|
6361
|
+
averageCellConfidence: classified.averageCellConfidence,
|
|
6362
|
+
minimumCellConfidence: classified.minimumCellConfidence,
|
|
6363
|
+
lowConfidenceCells: classified.lowConfidenceCells
|
|
6364
|
+
});
|
|
6365
|
+
accepted = true;
|
|
6366
|
+
break;
|
|
6367
|
+
} catch {
|
|
6368
|
+
// Try the next cell encoding/classifier/profile.
|
|
6369
|
+
}
|
|
6370
|
+
}
|
|
6371
|
+
if (accepted) break;
|
|
5389
6372
|
}
|
|
5390
6373
|
} catch {
|
|
5391
6374
|
// Try a narrower centre sample, then next version.
|
|
@@ -5497,7 +6480,7 @@ function perspectiveStress(imageData, severity) {
|
|
|
5497
6480
|
{ x: width - 1 - inset * 1.0, y: height - 1 - inset * 0.35 },
|
|
5498
6481
|
{ x: inset * 0.20, y: height - 1 - inset * 0.95 }
|
|
5499
6482
|
];
|
|
5500
|
-
const inverse = computeHomography(
|
|
6483
|
+
const inverse = computeHomography(source, destination);
|
|
5501
6484
|
const out = new Uint8ClampedArray(width * height * 4);
|
|
5502
6485
|
out.fill(255);
|
|
5503
6486
|
for (let y = 0; y < height; y++) {
|
|
@@ -5782,6 +6765,36 @@ function scanImageData(imageData, options = {}) {
|
|
|
5782
6765
|
if (perspective) return decorateScanResult(perspective, debugContext, options);
|
|
5783
6766
|
}
|
|
5784
6767
|
|
|
6768
|
+
// Triangle16 has half-cell color regions, so geometry that is adequate for
|
|
6769
|
+
// solid RGBW cells can still be off by a few tenths of a module and mix the
|
|
6770
|
+
// two triangle colors. If normal geometry was found but no payload decoded,
|
|
6771
|
+
// retry geometry once with finer alignment localization. This stays off the
|
|
6772
|
+
// clean/normal camera fast path.
|
|
6773
|
+
if (
|
|
6774
|
+
options.perspective !== false &&
|
|
6775
|
+
options.preciseAlignmentRecovery !== false &&
|
|
6776
|
+
!options._preciseAlignmentRecovery &&
|
|
6777
|
+
geometryCollector.length > 0
|
|
6778
|
+
) {
|
|
6779
|
+
try {
|
|
6780
|
+
const precise = tryPerspectiveScan(imageData, {
|
|
6781
|
+
...scanOptions,
|
|
6782
|
+
preciseAlignment: true,
|
|
6783
|
+
_preciseAlignmentRecovery: true,
|
|
6784
|
+
_diagnosticLabel: "precise-alignment"
|
|
6785
|
+
});
|
|
6786
|
+
if (precise) {
|
|
6787
|
+
return decorateScanResult({
|
|
6788
|
+
...precise,
|
|
6789
|
+
geometryRefined: true,
|
|
6790
|
+
recoveryMode: precise.recoveryMode ?? "precise-alignment"
|
|
6791
|
+
}, debugContext, options);
|
|
6792
|
+
}
|
|
6793
|
+
} catch {
|
|
6794
|
+
// Continue to color recovery and axis-aligned fallback.
|
|
6795
|
+
}
|
|
6796
|
+
}
|
|
6797
|
+
|
|
5785
6798
|
// If the normal scanner could see QuadQR geometry but could not decode the
|
|
5786
6799
|
// colors, retry the same frame after a single global Auto Tone / Contrast /
|
|
5787
6800
|
// Color-style pass. This is deliberately after the normal path so clean
|
|
@@ -6185,6 +7198,7 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6185
7198
|
|
|
6186
7199
|
const scanInterval = Math.max(80, options.scanInterval ?? 180);
|
|
6187
7200
|
const scratchCanvas = document.createElement("canvas");
|
|
7201
|
+
const highResolutionCanvas = document.createElement("canvas");
|
|
6188
7202
|
const multiFrameEnabled = options.multiFrame !== false;
|
|
6189
7203
|
const multiFrameWindow = Math.max(2, Math.min(8, Math.round(options.multiFrameWindow ?? 4)));
|
|
6190
7204
|
const multiFrameMinFrames = Math.max(2, Math.min(multiFrameWindow, Math.round(options.multiFrameMinFrames ?? 2)));
|
|
@@ -6192,6 +7206,12 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6192
7206
|
const cameraAutoColorEvery = Math.max(1, Math.round(options.cameraAutoColorEvery ?? 1));
|
|
6193
7207
|
const cameraAutoEnhanceEvery = Math.max(1, Math.round(options.cameraAutoEnhanceEvery ?? 2));
|
|
6194
7208
|
const cameraFinderRecoveryEvery = Math.max(1, Math.round(options.cameraFinderRecoveryEvery ?? 2));
|
|
7209
|
+
const cameraHighResolutionEvery = Math.max(1, Math.round(options.cameraHighResolutionEvery ?? 2));
|
|
7210
|
+
const baseCameraMaxDimension = Math.max(480, Math.round(options.maxDimension ?? 1080));
|
|
7211
|
+
const cameraHighResolutionMaxDimension = Math.max(
|
|
7212
|
+
baseCameraMaxDimension,
|
|
7213
|
+
Math.round(options.cameraHighResolutionMaxDimension ?? 1600)
|
|
7214
|
+
);
|
|
6195
7215
|
let missStreak = 0;
|
|
6196
7216
|
let stopped = false;
|
|
6197
7217
|
let busy = false;
|
|
@@ -6239,7 +7259,7 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6239
7259
|
|
|
6240
7260
|
const scanNow = () => scanVideoFrame(video, {
|
|
6241
7261
|
...options,
|
|
6242
|
-
maxDimension:
|
|
7262
|
+
maxDimension: baseCameraMaxDimension,
|
|
6243
7263
|
canvas: scratchCanvas
|
|
6244
7264
|
});
|
|
6245
7265
|
|
|
@@ -6306,7 +7326,7 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6306
7326
|
busy = true;
|
|
6307
7327
|
frameNumber++;
|
|
6308
7328
|
const observations = [];
|
|
6309
|
-
const frameDiagnostics =
|
|
7329
|
+
const frameDiagnostics = {};
|
|
6310
7330
|
const capturedFrame = {};
|
|
6311
7331
|
const frameStarted = nowMs();
|
|
6312
7332
|
let allowAutoEnhance = false;
|
|
@@ -6334,7 +7354,7 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6334
7354
|
// If geometry exists, the rectified QR-only pixel enhancer is both
|
|
6335
7355
|
// stronger and much cheaper than reprocessing the whole live frame.
|
|
6336
7356
|
fullFrameAutoEnhanceRecovery: options.fullFrameAutoEnhanceRecovery ?? false,
|
|
6337
|
-
maxDimension:
|
|
7357
|
+
maxDimension: baseCameraMaxDimension,
|
|
6338
7358
|
canvas: scratchCanvas,
|
|
6339
7359
|
_capturedFrame: capturedFrame,
|
|
6340
7360
|
_observationCollector: observations,
|
|
@@ -6375,6 +7395,71 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6375
7395
|
...frameDiagnostics
|
|
6376
7396
|
});
|
|
6377
7397
|
|
|
7398
|
+
// Geometry-aware high-resolution retry. A dense QuadQR can look large
|
|
7399
|
+
// enough in the preview while each individual module has become too
|
|
7400
|
+
// small after the normal 1080 px scanner cap. If the fast pass already
|
|
7401
|
+
// sees at least two convincing finders, spend one bounded retry on a
|
|
7402
|
+
// higher-resolution copy of the visible camera ROI. Empty frames and
|
|
7403
|
+
// ordinary small codes never pay this cost.
|
|
7404
|
+
const shouldTryHighResolution = options.cameraHighResolutionRecovery !== false &&
|
|
7405
|
+
cameraHighResolutionMaxDimension > baseCameraMaxDimension &&
|
|
7406
|
+
(frameDiagnostics?.finderCount ?? 0) >= (options.cameraHighResolutionMinFinders ?? 2) &&
|
|
7407
|
+
((missStreak - 1) % cameraHighResolutionEvery === 0);
|
|
7408
|
+
if (shouldTryHighResolution) {
|
|
7409
|
+
const highResolutionObservations = [];
|
|
7410
|
+
const highResolutionDiagnostics = {};
|
|
7411
|
+
const highResolutionCapturedFrame = {};
|
|
7412
|
+
emitDiagnostic({
|
|
7413
|
+
type: "method",
|
|
7414
|
+
state: "trying",
|
|
7415
|
+
method: "high-resolution-geometry-recovery",
|
|
7416
|
+
message: `Dense geometry detected · retrying at up to ${cameraHighResolutionMaxDimension}px`,
|
|
7417
|
+
...frameDiagnostics
|
|
7418
|
+
});
|
|
7419
|
+
try {
|
|
7420
|
+
const recoveryStarted = nowMs();
|
|
7421
|
+
const recovered = scanVideoFrame(video, {
|
|
7422
|
+
...options,
|
|
7423
|
+
_diagnosticLabel: "high-resolution-geometry-recovery",
|
|
7424
|
+
finderRecovery: true,
|
|
7425
|
+
autoEnhanceRecovery: false,
|
|
7426
|
+
fullFrameAutoEnhanceRecovery: false,
|
|
7427
|
+
maxDimension: cameraHighResolutionMaxDimension,
|
|
7428
|
+
canvas: highResolutionCanvas,
|
|
7429
|
+
_capturedFrame: highResolutionCapturedFrame,
|
|
7430
|
+
_observationCollector: highResolutionObservations,
|
|
7431
|
+
_frameDiagnostics: highResolutionDiagnostics
|
|
7432
|
+
});
|
|
7433
|
+
const recoveryElapsedMs = nowMs() - recoveryStarted;
|
|
7434
|
+
emitDiagnostic({
|
|
7435
|
+
type: "success",
|
|
7436
|
+
state: "decoded",
|
|
7437
|
+
method: "high-resolution-geometry-recovery",
|
|
7438
|
+
elapsedMs: recoveryElapsedMs,
|
|
7439
|
+
message: `High-resolution retry decoded v${recovered.version} · ${Math.round(recoveryElapsedMs)} ms`,
|
|
7440
|
+
...(highResolutionDiagnostics ?? frameDiagnostics)
|
|
7441
|
+
});
|
|
7442
|
+
missStreak = 0;
|
|
7443
|
+
observationHistory.clear();
|
|
7444
|
+
if (emitResult({
|
|
7445
|
+
...recovered,
|
|
7446
|
+
cameraHighResolutionRecovery: true,
|
|
7447
|
+
cameraProgressiveRecovery: true,
|
|
7448
|
+
recoveryMode: recovered.recoveryMode ?? "high-resolution-geometry-recovery"
|
|
7449
|
+
}, highResolutionCapturedFrame, highResolutionDiagnostics ?? frameDiagnostics)) return;
|
|
7450
|
+
} catch (highResolutionError) {
|
|
7451
|
+
observations.push(...highResolutionObservations);
|
|
7452
|
+
emitDiagnostic({
|
|
7453
|
+
type: "method",
|
|
7454
|
+
state: "failed",
|
|
7455
|
+
method: "high-resolution-geometry-recovery",
|
|
7456
|
+
message: `High-resolution geometry retry did not decode${highResolutionDiagnostics?.finderCount != null ? ` · ${highResolutionDiagnostics.finderCount} finder(s)` : ""}`,
|
|
7457
|
+
error: highResolutionError?.message ?? String(highResolutionError),
|
|
7458
|
+
...(highResolutionDiagnostics ?? frameDiagnostics)
|
|
7459
|
+
});
|
|
7460
|
+
}
|
|
7461
|
+
}
|
|
7462
|
+
|
|
6378
7463
|
// The user's real phone-camera case is dominated by color cast before
|
|
6379
7464
|
// finder detection: Photoshop Auto Color alone makes the same live QR
|
|
6380
7465
|
// immediately detectable. Retry the exact captured frame with a cheap
|
|
@@ -6679,11 +7764,14 @@ const internals = Object.freeze({
|
|
|
6679
7764
|
HEADER_CODEWORD_CELLS,
|
|
6680
7765
|
COMPACT_HEADER_CODEWORD_CELLS,
|
|
6681
7766
|
CELLS_PER_BYTE,
|
|
7767
|
+
RGBW_CELLS_PER_BYTE,
|
|
7768
|
+
TRIANGLE16_CELLS_PER_BYTE,
|
|
7769
|
+
TRIANGLE16_FLAG,
|
|
6682
7770
|
TEXT_FLAG,
|
|
6683
7771
|
SECURE_FLAG
|
|
6684
7772
|
});
|
|
6685
7773
|
|
|
6686
|
-
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 };
|
|
7774
|
+
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 };
|
|
6687
7775
|
})();
|
|
6688
7776
|
const QuadQR = __qqr_quadqr_js;
|
|
6689
7777
|
|