Upload an image to scan a grid of DataMatrix/QR barcodes. The scanner performs fast scan, layout analysis, and deep decode to read
every cell in the grid.
No file selected
0.0s
Fast Scan
Layout Analysis
Deep Decode
Final Result
Image preview will appear here after uploading
Results
Row
Col
Status
Text
// ─── SDK configuration ────────────────────────────────────────────────
let sdkReady = false;
const sdkReadyPromise = (async () => {
Dynamsoft.License.LicenseManager.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAwLWRicl9qc19zYW1wbGVzIiwib3JnYW5pemF0aW9uSUQiOiIyMDAwMDAifQ==");
try {
await Dynamsoft.Core.CoreModule.loadWasm();
sdkReady = true;
log("[Init] WASM preloaded.");
} catch (e) {
console.error("[Init] Failed to preload WASM:", e);
throw e;
}
})();
// ─── Constants ────────────────────────────────────────────────────────
const FAST_TEMPLATE = "GridFastScan";
const DEEP_TEMPLATE = "GridDeepDecode";
const SCALE_FACTOR = 2.0;
// ─── Geometry helpers ─────────────────────────────────────────────────
function computeCenter(quad) {
const cx = quad.points.reduce((s, p) => s + p.x, 0) / 4;
const cy = quad.points.reduce((s, p) => s + p.y, 0) / 4;
return { cx, cy };
}
function expandQuad(quad, scale) {
const { cx, cy } = computeCenter(quad);
return {
points: quad.points.map((p) => ({
x: Math.round(cx + (p.x - cx) * scale),
y: Math.round(cy + (p.y - cy) * scale),
})),
};
}
/**
* Produces a string key from a quad's corner coordinates so we can
* match LayoutAnalyzer LES_INPUT elements back to the decoded texts.
* Uses rounded integers to tolerate sub-pixel differences.
*/
function quadKey(quad) {
return quad.points.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join("|");
}
// ─── Canvas drawing helpers ───────────────────────────────────────────
function drawImage(ctx, bitmap) {
ctx.drawImage(bitmap, 0, 0);
}
function drawSolidQuads(ctx, quads, color = "#00cc44", lineWidth = 2) {
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.setLineDash([]);
for (const quad of quads) {
const pts = quad.points;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < 4; i++) ctx.lineTo(pts[i].x, pts[i].y);
ctx.closePath();
ctx.stroke();
}
ctx.restore();
}
function drawDashedQuads(ctx, quads, color = "#ff3333", lineWidth = 3, dashLen = 10) {
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.setLineDash([dashLen, dashLen]);
for (const quad of quads) {
const pts = quad.points;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < 4; i++) ctx.lineTo(pts[i].x, pts[i].y);
ctx.closePath();
ctx.stroke();
}
ctx.restore();
}
function getTopLeft(quad) {
return {
x: Math.min(...quad.points.map((p) => p.x)),
y: Math.min(...quad.points.map((p) => p.y)),
};
}
function drawTexts(ctx, items, color = "#00ff55", fontSize = 11) {
ctx.save();
ctx.fillStyle = color;
ctx.font = `bold ${fontSize}px monospace`;
for (const item of items) {
if (!item.text) continue;
const { x, y } = getTopLeft(item.location);
ctx.fillText(item.text, x + 1, Math.max(y - 2, fontSize));
}
ctx.restore();
}
// ─── UI helpers ───────────────────────────────────────────────────────
function setPhaseStatus(num, status, subText = "") {
const el = document.getElementById(`phase${num}`);
if (!el) return;
el.className = `phase phase-${status}`;
const sub = document.getElementById(`phase${num}-sub`);
if (sub) sub.textContent = subText;
}
function log(msg, type) {
const logEl = document.getElementById("logSection");
logEl.style.display = "block";
const div = document.createElement("div");
div.className = "log-entry" + (type ? ` log-${type}` : "");
div.textContent = msg;
logEl.appendChild(div);
logEl.scrollTop = logEl.scrollHeight;
}
function escapeHtml(str) {
return String(str).replace(/&/g, "&").replace(//g, ">");
}
function withTimeout(promise, timeoutMs, timeoutMessage) {
let timer = null;
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
});
return Promise.race([promise, timeoutPromise]).finally(() => {
if (timer) clearTimeout(timer);
});
}
const AUTO_AXIS = { elementCount: -1, isStaggered: false, angle: -1, isEqualSpacing: false, spacing: -1, spacingUnit: 0 };
async function analyzeLayoutWithFallback(locations, width, height) {
return await withTimeout(
Dynamsoft.Utility.LayoutAnalyzer.analyze(locations, {
pattern: Dynamsoft.Utility.EnumLayoutPattern.LP_MATRIX,
axes: [AUTO_AXIS, AUTO_AXIS],
inputImageWidth: width,
inputImageHeight: height,
}),
60000,
"Phase 2 timeout on LP_MATRIX (60s)",
);
}
// ─── Progress bar helpers ─────────────────────────────────────────
let _progressTimer = null;
let _progressStart = 0;
function startProgress() {
const strip = document.getElementById("progressStrip");
const fill = document.getElementById("progressFill");
const elapsed = document.getElementById("progressElapsed");
strip.className = "progress-strip active";
fill.className = "progress-fill animating";
fill.style.width = "0%";
elapsed.textContent = "0.0s";
_progressStart = Date.now();
_progressTimer = setInterval(() => {
const s = ((Date.now() - _progressStart) / 1000).toFixed(1);
elapsed.textContent = `${s}s`;
}, 100);
}
function updateProgress(pct) {
document.getElementById("progressFill").style.width = `${Math.min(pct, 100)}%`;
}
function stopProgress(status = "done") {
if (_progressTimer) {
clearInterval(_progressTimer);
_progressTimer = null;
}
const strip = document.getElementById("progressStrip");
const fill = document.getElementById("progressFill");
const elapsed = document.getElementById("progressElapsed");
fill.classList.remove("animating");
fill.style.width = "100%";
strip.className = `progress-strip active ${status}`;
const s = ((Date.now() - _progressStart) / 1000).toFixed(1);
elapsed.textContent = `${s}s`;
}
// ─── Results rendering ────────────────────────────────────────────────
function renderResults(gridItems) {
let totalDecoded = 0,
totalFailed = 0,
totalCells = 0;
Object.values(gridItems).forEach((cols) =>
Object.values(cols).forEach((item) => {
totalCells++;
if (item.type === "decoded" || item.type === "deep-decoded") totalDecoded++;
else if (item.type === "failed") totalFailed++;
}),
);
document.getElementById("resultsSummary").innerHTML = `
${totalDecoded}Decoded
${totalFailed}Failed
${totalCells}Total Cells
`;
const tbody = document.getElementById("resultsBody");
tbody.innerHTML = "";
Object.keys(gridItems)
.map(Number)
.sort((a, b) => a - b)
.forEach((ri) => {
Object.keys(gridItems[ri])
.map(Number)
.sort((a, b) => a - b)
.forEach((ci) => {
const item = gridItems[ri][ci];
const statusLabel = item.type === "decoded" ? "Decoded" : item.type === "deep-decoded" ? "Deep Decoded" : "Failed";
const tr = document.createElement("tr");
tr.innerHTML = `