Skip to content

Commit 4dc393d

Browse files
committed
Support scanning of 1D/2D codes other than QR codes
1 parent 6a82fb9 commit 4dc393d

5 files changed

Lines changed: 99 additions & 59 deletions

File tree

src/main/resources/messages_en.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ label.load=Load
779779
label.load.buttonLabel=Load
780780
label.load.file=Load text file
781781
label.load.image=Load from image (OCR)
782-
label.load.qrcode=Scan QR code
782+
label.load.code=Scan code
783783
label.load.message=Loaded
784784
label.load.errorMessage=Failed to load. Please try again.
785785
label.copyToClipboard=Copy to Clipboard

src/main/resources/messages_ja.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ label.load=読込
779779
label.load.buttonLabel=読込
780780
label.load.file=テキストファイルから読込
781781
label.load.image=画像から読込 (OCR)
782-
label.load.qrcode=QRコードから読込
782+
label.load.code=コードから読込
783783
label.load.message=読み込みました
784784
label.load.errorMessage=読込に失敗しました。もう一度お試しください。
785785
label.copyToClipboard=クリップボードにコピー

src/main/resources/messages_ru.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ label.load=Загрузить
779779
label.load.buttonLabel=Загрузить
780780
label.load.file=Загрузка из текстовый файла
781781
label.load.image=Загрузка из изображения (OCR)
782-
label.load.qrcode=Отсканировать QR-код
782+
label.load.code=Отсканировать код
783783
label.load.message=Читать
784784
label.load.errorMessage=Ошибка загрузки. Пожалуйста, попробуйте еще раз.
785785
label.copyToClipboard=Скопировать в буфер обмена

src/main/webapp/WEB-INF/pages/index.jsp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/main.css?v=${dc:fileLastModified(pageContext, '/static/css/main.css')}" />
3333
<script defer src="${pageContext.request.contextPath}/static/js/all.min.js?v=${dc:fileLastModified(pageContext, '/static/js/all.min.js')}"></script>
3434
<script id="scriptTesseract" data-src="https://cdn.jsdelivr.net/npm/tesseract.js@6.0.1/dist/tesseract.min.js" integrity="sha256-EP/3hIQGd1nEMCigKnLXbQuQ6xcwK7I7WKnsVBC8kos=" crossorigin="anonymous"></script>
35-
<script id="scriptJsqr" data-src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js" integrity="sha256-vEDIoVGWI2sjFNsIVvcsoLSZgM1UE7jIUqc0n1/uCFk=" crossorigin="anonymous"></script>
35+
<script id="scriptZXing" data-src="https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/umd/index.min.js" integrity="sha256-18yPad1wvc86wAya5XK/KsufQTK6N5xy34QuTbkYZS0=" crossorigin="anonymous"></script>
3636
<style><%-- Bootstrap Icons --%>
3737
.bi-globe2 { --bi-icon: url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1/icons/globe2.svg'); }
3838
.bi-pin-angle-fill { --bi-icon: url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1/icons/pin-angle-fill.svg'); }
@@ -233,7 +233,7 @@
233233
<ul class="dropdown-menu dropdown-menu-end" role="menu">
234234
<li id="loadFile" data-load-message="${dc:h(msg['label.load.message'])}" data-load-error-message="${dc:h(msg['label.load.errorMessage'])}" tabindex="0"><i class="bi bi-file-text"></i> ${dc:h(msg['label.load.file'])}</li>
235235
<li id="loadImage" data-load-message="${dc:h(msg['label.load.message'])}" data-load-error-message="${dc:h(msg['label.load.errorMessage'])}" tabindex="0"><i class="bi bi-camera"></i> ${dc:h(msg['label.load.image'])}</li>
236-
<li id="loadQrcode" data-load-message="${dc:h(msg['label.load.message'])}" data-load-error-message="${dc:h(msg['label.load.errorMessage'])}" tabindex="0"><i class="bi bi-qr-code-scan"></i> ${dc:h(msg['label.load.qrcode'])}</li>
236+
<li id="loadCode" data-load-message="${dc:h(msg['label.load.message'])}" data-load-error-message="${dc:h(msg['label.load.errorMessage'])}" tabindex="0"><i class="bi bi-qr-code-scan"></i> ${dc:h(msg['label.load.code'])}</li>
237237
</ul>
238238
</button>
239239
<button type="button" class="btn btn-v-icon-label permanent-link popover-toggle" title="${dc:h(msg['label.permanentLink'])}">
@@ -2377,8 +2377,8 @@
23772377
<div style="display:none" aria-hidden="true">
23782378
<div>
23792379
<input type="file" id="loadFileInput" accept="text/*" />
2380-
<input type="file" id="loadImageInput" accept="image/*" />
2381-
<input type="file" id="loadQrcodeInput" accept="image/*" />
2380+
<input type="file" id="loadImageInput" accept="image/*;capture=camera" />
2381+
<input type="file" id="loadCodeInput" accept="image/*;capture=camera" />
23822382
</div>
23832383
23842384
<svg>

src/main/webapp/static/js/main.js

Lines changed: 92 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ $.onReady(function () {
3030
const elLoadFileInput = $.id("loadFileInput");
3131
const elLoadImage = $.id("loadImage");
3232
const elLoadImageInput = $.id("loadImageInput");
33-
const elLoadQrcode = $.id("loadQrcode");
34-
const elLoadQrcodeInput = $.id("loadQrcodeInput");
33+
const elLoadCode = $.id("loadCode");
34+
const elLoadCodeInput = $.id("loadCodeInput");
3535
const elOeGroup = $.id("oeGroup");
3636
const elOeGroupBtns = $.all("#oeGroup .btn:not(.dropdown-toggle)");
3737
const elOexBtn = $.id("oex");
@@ -512,11 +512,11 @@ $.onReady(function () {
512512
}
513513
});
514514

515-
$.on(elLoadQrcode, "click", function () {
516-
elLoadQrcodeInput.click();
515+
$.on(elLoadCode, "click", function () {
516+
elLoadCodeInput.click();
517517
});
518518

519-
$.on(elLoadQrcodeInput, "change", async function () {
519+
$.on(elLoadCodeInput, "change", async function () {
520520
if (this.files.length === 0) {
521521
return;
522522
}
@@ -525,10 +525,10 @@ $.onReady(function () {
525525
this.value = "";
526526

527527
try {
528-
updateValue(await readQrcodeAsync(file));
529-
showTooltip(elLoadBtn, elLoadQrcode.getAttribute("data-load-message"), 2000);
528+
updateValue(await readCodeAsync(file));
529+
showTooltip(elLoadBtn, elLoadCode.getAttribute("data-load-message"), 2000);
530530
} catch (ex) {
531-
showMessageDialog(elLoadQrcode.getAttribute("data-load-error-message"));
531+
showMessageDialog(elLoadCode.getAttribute("data-load-error-message"));
532532
}
533533
});
534534

@@ -1019,32 +1019,83 @@ $.onReady(function () {
10191019
return ret.data.text;
10201020
}
10211021

1022-
async function readQrcodeAsync(file) {
1023-
await loadScriptAsync("#scriptJsqr");
1024-
1022+
async function readCodeAsync(file) {
10251023
const img = await readFileAsImageAsync(file);
10261024

1027-
const code = readQrcodeFromImage(img, [600, 200, 1000, 400, 800, 1200, 1400, 1600]);
1025+
let code = await detectCodeAsync(img);
10281026
if (code === null) {
1029-
reject(new Error("Could not parse."));
1030-
return;
1027+
code = await detectCodeByZXingAsync(img);
1028+
}
1029+
if (code === null) {
1030+
throw new Error("Could not parse.");
1031+
}
1032+
1033+
return code;
1034+
}
1035+
1036+
async function detectCodeAsync(img) {
1037+
if (!("BarcodeDetector" in window)) {
1038+
return null;
1039+
}
1040+
1041+
const canvas = toCanvas(img, 1.0, "white");
1042+
1043+
const detector = new BarcodeDetector();
1044+
const barcodes = await detector.detect(canvas);
1045+
1046+
if (barcodes.length === 0) {
1047+
return null;
10311048
}
10321049

1033-
let data;
1034-
if (code.data.length === 0 && 0 < code.binaryData.length) {
1035-
// If the QR code cannot be parsed as UTF-8 text
1050+
const code = Array.from(barcodes, (barcode) => barcode.rawValue).join("\n");
1051+
1052+
if (code.length === 0) {
1053+
return null;
1054+
}
1055+
1056+
return code;
1057+
}
1058+
1059+
async function detectCodeByZXingAsync(img) {
1060+
await loadScriptAsync("#scriptZXing");
1061+
1062+
const codeReader = new ZXing.BrowserMultiFormatReader();
1063+
const hints = new Map([
1064+
[ZXing.DecodeHintType.TRY_HARDER, true]
1065+
]);
1066+
1067+
let parsedOrgSize = false;
1068+
let result = null;
1069+
for (let maxSize of [480, 640, 800, 1024, 1280, 1600, 1920, 2160]) {
1070+
const scale = calcImageScale(img, maxSize);
1071+
if (scale === 1.0) {
1072+
if (parsedOrgSize) {
1073+
continue;
1074+
}
1075+
parsedOrgSize = true;
1076+
}
1077+
1078+
const canvas = toCanvas(img, scale, "white");
1079+
10361080
try {
1037-
// Parse the binary data as JIS X 0208 (Shift_JIS) text
1038-
data = new TextDecoder("shift-jis", {fatal: true}).decode(Uint8Array.from(code.binaryData));
1081+
result = await codeReader.decodeFromImageUrl(canvas.toDataURL("image/png"), hints);
1082+
break;
10391083
} catch (ex) {
1040-
// Convert the binary data to hex string
1041-
data = code.binaryData.map((b) => ("0" + (b & 0xFF).toString(16)).slice(-2)).join("");
1084+
continue;
10421085
}
1043-
} else {
1044-
data = code.data;
10451086
}
10461087

1047-
return data;
1088+
if (result === null) {
1089+
return null;
1090+
}
1091+
1092+
let code = result.getText();
1093+
if (code.includes("\uFFFD")) {
1094+
// Binary to hex string
1095+
code = Array.from(result.getRawBytes(), (b) => b.toString(16).padStart(2, "0")).join("");
1096+
}
1097+
1098+
return code;
10481099
}
10491100

10501101
function updateValue(val) {
@@ -1250,38 +1301,27 @@ function readFileAsImageAsync(file) {
12501301
});
12511302
}
12521303

1253-
function readQrcodeFromImage(imgElm, maxSizes) {
1254-
const minImgSize = Math.min(imgElm.width, imgElm.height);
1255-
1304+
function calcImageScale(img, maxSize) {
1305+
const maxImgSize = Math.max(img.naturalWidth, img.naturalHeight);
1306+
const scale = Math.min(1.0, 1.0 * maxSize / maxImgSize);
1307+
return scale;
1308+
}
1309+
1310+
function toCanvas(img, scale, bgColor) {
12561311
const canvas = document.createElement("canvas");
1312+
canvas.width = img.naturalWidth * scale;
1313+
canvas.height = img.naturalHeight * scale;
12571314

1258-
let code = null;
1259-
let parsedOrgSize = false;
1260-
for (const maxSize of maxSizes) {
1261-
const r = Math.min(1.0, 1.0 * maxSize / minImgSize);
1262-
1263-
if (1.0 <= r) {
1264-
if (parsedOrgSize) {
1265-
break;
1266-
}
1267-
parsedOrgSize = true;
1268-
}
1269-
1270-
canvas.width = imgElm.width * r;
1271-
canvas.height = imgElm.height * r;
1272-
1273-
const ctx = canvas.getContext("2d");
1274-
ctx.scale(r, r);
1275-
ctx.drawImage(imgElm, 0, 0);
1276-
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
1277-
1278-
code = jsQR(imageData.data, imageData.width, imageData.height);
1279-
if (code !== null) {
1280-
break;
1281-
}
1315+
const ctx = canvas.getContext("2d");
1316+
if (bgColor) {
1317+
ctx.fillStyle = bgColor;
1318+
ctx.fillRect(0, 0, canvas.width, canvas.height);
12821319
}
1320+
ctx.imageSmoothingEnabled = true;
1321+
ctx.imageSmoothingQuality = "high";
1322+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
12831323

1284-
return code;
1324+
return canvas;
12851325
}
12861326

12871327
function separateThousand(num) {

0 commit comments

Comments
 (0)