Replace CDN React/ReactDOM/Babel with local libs; remove Babel and inline scripts Build Tailwind locally, add safelist; switch to assets/tailwind.css Self-host Font Awesome and Inter (CSS + woff2); remove external font CDNs Implement strict CSP (no unsafe-inline/eval; scripts/styles/fonts from self) Extract inline handlers; move PWA scripts to external files Add local QR code generation (qrcode lib) and remove api.qrserver.com Improve SessionTypeSelector visual selection (highlighted background and ring) Keep PWA working with service worker and offline assets Refs: CSP hardening, offline-first, no external dependencies
235 lines
6.0 KiB
JavaScript
235 lines
6.0 KiB
JavaScript
/**
|
|
* Data mask pattern reference
|
|
* @type {Object}
|
|
*/
|
|
exports.Patterns = {
|
|
PATTERN000: 0,
|
|
PATTERN001: 1,
|
|
PATTERN010: 2,
|
|
PATTERN011: 3,
|
|
PATTERN100: 4,
|
|
PATTERN101: 5,
|
|
PATTERN110: 6,
|
|
PATTERN111: 7
|
|
}
|
|
|
|
/**
|
|
* Weighted penalty scores for the undesirable features
|
|
* @type {Object}
|
|
*/
|
|
const PenaltyScores = {
|
|
N1: 3,
|
|
N2: 3,
|
|
N3: 40,
|
|
N4: 10
|
|
}
|
|
|
|
/**
|
|
* Check if mask pattern value is valid
|
|
*
|
|
* @param {Number} mask Mask pattern
|
|
* @return {Boolean} true if valid, false otherwise
|
|
*/
|
|
exports.isValid = function isValid (mask) {
|
|
return mask != null && mask !== '' && !isNaN(mask) && mask >= 0 && mask <= 7
|
|
}
|
|
|
|
/**
|
|
* Returns mask pattern from a value.
|
|
* If value is not valid, returns undefined
|
|
*
|
|
* @param {Number|String} value Mask pattern value
|
|
* @return {Number} Valid mask pattern or undefined
|
|
*/
|
|
exports.from = function from (value) {
|
|
return exports.isValid(value) ? parseInt(value, 10) : undefined
|
|
}
|
|
|
|
/**
|
|
* Find adjacent modules in row/column with the same color
|
|
* and assign a penalty value.
|
|
*
|
|
* Points: N1 + i
|
|
* i is the amount by which the number of adjacent modules of the same color exceeds 5
|
|
*/
|
|
exports.getPenaltyN1 = function getPenaltyN1 (data) {
|
|
const size = data.size
|
|
let points = 0
|
|
let sameCountCol = 0
|
|
let sameCountRow = 0
|
|
let lastCol = null
|
|
let lastRow = null
|
|
|
|
for (let row = 0; row < size; row++) {
|
|
sameCountCol = sameCountRow = 0
|
|
lastCol = lastRow = null
|
|
|
|
for (let col = 0; col < size; col++) {
|
|
let module = data.get(row, col)
|
|
if (module === lastCol) {
|
|
sameCountCol++
|
|
} else {
|
|
if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5)
|
|
lastCol = module
|
|
sameCountCol = 1
|
|
}
|
|
|
|
module = data.get(col, row)
|
|
if (module === lastRow) {
|
|
sameCountRow++
|
|
} else {
|
|
if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5)
|
|
lastRow = module
|
|
sameCountRow = 1
|
|
}
|
|
}
|
|
|
|
if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5)
|
|
if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5)
|
|
}
|
|
|
|
return points
|
|
}
|
|
|
|
/**
|
|
* Find 2x2 blocks with the same color and assign a penalty value
|
|
*
|
|
* Points: N2 * (m - 1) * (n - 1)
|
|
*/
|
|
exports.getPenaltyN2 = function getPenaltyN2 (data) {
|
|
const size = data.size
|
|
let points = 0
|
|
|
|
for (let row = 0; row < size - 1; row++) {
|
|
for (let col = 0; col < size - 1; col++) {
|
|
const last = data.get(row, col) +
|
|
data.get(row, col + 1) +
|
|
data.get(row + 1, col) +
|
|
data.get(row + 1, col + 1)
|
|
|
|
if (last === 4 || last === 0) points++
|
|
}
|
|
}
|
|
|
|
return points * PenaltyScores.N2
|
|
}
|
|
|
|
/**
|
|
* Find 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column,
|
|
* preceded or followed by light area 4 modules wide
|
|
*
|
|
* Points: N3 * number of pattern found
|
|
*/
|
|
exports.getPenaltyN3 = function getPenaltyN3 (data) {
|
|
const size = data.size
|
|
let points = 0
|
|
let bitsCol = 0
|
|
let bitsRow = 0
|
|
|
|
for (let row = 0; row < size; row++) {
|
|
bitsCol = bitsRow = 0
|
|
for (let col = 0; col < size; col++) {
|
|
bitsCol = ((bitsCol << 1) & 0x7FF) | data.get(row, col)
|
|
if (col >= 10 && (bitsCol === 0x5D0 || bitsCol === 0x05D)) points++
|
|
|
|
bitsRow = ((bitsRow << 1) & 0x7FF) | data.get(col, row)
|
|
if (col >= 10 && (bitsRow === 0x5D0 || bitsRow === 0x05D)) points++
|
|
}
|
|
}
|
|
|
|
return points * PenaltyScores.N3
|
|
}
|
|
|
|
/**
|
|
* Calculate proportion of dark modules in entire symbol
|
|
*
|
|
* Points: N4 * k
|
|
*
|
|
* k is the rating of the deviation of the proportion of dark modules
|
|
* in the symbol from 50% in steps of 5%
|
|
*/
|
|
exports.getPenaltyN4 = function getPenaltyN4 (data) {
|
|
let darkCount = 0
|
|
const modulesCount = data.data.length
|
|
|
|
for (let i = 0; i < modulesCount; i++) darkCount += data.data[i]
|
|
|
|
const k = Math.abs(Math.ceil((darkCount * 100 / modulesCount) / 5) - 10)
|
|
|
|
return k * PenaltyScores.N4
|
|
}
|
|
|
|
/**
|
|
* Return mask value at given position
|
|
*
|
|
* @param {Number} maskPattern Pattern reference value
|
|
* @param {Number} i Row
|
|
* @param {Number} j Column
|
|
* @return {Boolean} Mask value
|
|
*/
|
|
function getMaskAt (maskPattern, i, j) {
|
|
switch (maskPattern) {
|
|
case exports.Patterns.PATTERN000: return (i + j) % 2 === 0
|
|
case exports.Patterns.PATTERN001: return i % 2 === 0
|
|
case exports.Patterns.PATTERN010: return j % 3 === 0
|
|
case exports.Patterns.PATTERN011: return (i + j) % 3 === 0
|
|
case exports.Patterns.PATTERN100: return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0
|
|
case exports.Patterns.PATTERN101: return (i * j) % 2 + (i * j) % 3 === 0
|
|
case exports.Patterns.PATTERN110: return ((i * j) % 2 + (i * j) % 3) % 2 === 0
|
|
case exports.Patterns.PATTERN111: return ((i * j) % 3 + (i + j) % 2) % 2 === 0
|
|
|
|
default: throw new Error('bad maskPattern:' + maskPattern)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply a mask pattern to a BitMatrix
|
|
*
|
|
* @param {Number} pattern Pattern reference number
|
|
* @param {BitMatrix} data BitMatrix data
|
|
*/
|
|
exports.applyMask = function applyMask (pattern, data) {
|
|
const size = data.size
|
|
|
|
for (let col = 0; col < size; col++) {
|
|
for (let row = 0; row < size; row++) {
|
|
if (data.isReserved(row, col)) continue
|
|
data.xor(row, col, getMaskAt(pattern, row, col))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the best mask pattern for data
|
|
*
|
|
* @param {BitMatrix} data
|
|
* @return {Number} Mask pattern reference number
|
|
*/
|
|
exports.getBestMask = function getBestMask (data, setupFormatFunc) {
|
|
const numPatterns = Object.keys(exports.Patterns).length
|
|
let bestPattern = 0
|
|
let lowerPenalty = Infinity
|
|
|
|
for (let p = 0; p < numPatterns; p++) {
|
|
setupFormatFunc(p)
|
|
exports.applyMask(p, data)
|
|
|
|
// Calculate penalty
|
|
const penalty =
|
|
exports.getPenaltyN1(data) +
|
|
exports.getPenaltyN2(data) +
|
|
exports.getPenaltyN3(data) +
|
|
exports.getPenaltyN4(data)
|
|
|
|
// Undo previously applied mask
|
|
exports.applyMask(p, data)
|
|
|
|
if (penalty < lowerPenalty) {
|
|
lowerPenalty = penalty
|
|
bestPattern = p
|
|
}
|
|
}
|
|
|
|
return bestPattern
|
|
}
|