This commit is contained in:
Samuele Locatelli
2023-12-15 10:57:32 +01:00
7 changed files with 833 additions and 1 deletions
+31 -1
View File
@@ -91,11 +91,41 @@
</div>
</div>
<BarcodeReader ScanResult="(e) => ScanDoneHandler(e)"
ScanBtnTitle="Scan"
ResetBtnTitle="Reset"
CloseBtnTitle="Close"
UseBuiltinDiv="false"
@ref="barcodeReaderCustom"
SelectDeviceBtnTitle="Select Device">
</BarcodeReader>
<div @ref="barcodeReaderCustom.Element" class="d-flex justify-content-center">
@* <div style="width: 480px; max-width: 100%"> *@
<div class="col-12 col-md-8 col-lg-6">
<button class="btn btn-outline-success p-2 m-1 w-25" data-action="startButton">Scan</button>
<button class="btn btn-outline-success p-2 m-1 w-25" data-action="resetButton">Reset</button>
<div data-action="sourceSelectPanel" style="display:none">
<label for="sourceSelect">Source:</label>
<select data-action="sourceSelect" style="max-width:100%" class="form-control">
</select>
</div>
<div>
<video id="video" playsinline="true" autoplay="true" class="w-100 h-100 border rounded shadow" muted="true"></video>
</div>
</div>
</div>
@code {
private int currentCount = 0;
protected BarcodeReader barcodeReaderCustom { get; set; } = null!;
protected Random rnd = new Random();
protected async Task ScanDoneHandler(string value)
{
await Task.Delay(1);
}
private void IncrementCount()
{
currentCount++;
File diff suppressed because one or more lines are too long
+34
View File
@@ -0,0 +1,34 @@
@if (UseBuiltinDiv)
{
<div class="modal alert-popup" tabindex="-1" style="display:block" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<!-- Edit form for the current item -->
<div class="modal-body" @ref="Element">
<button class="btn btn-primary p-2 m-1 w-25" data-action="startButton">@ScanBtnTitle</button>
<button class="btn btn-secondary p-2 m-1 w-25" data-action="resetButton">@ResetBtnTitle</button>
<button type="button" class="btn btn-info p-2 m-1 w-25" data-action="closeButton">@CloseBtnTitle</button>
<div data-action="sourceSelectPanel" style="display:none">
<label for="sourceSelect">@SelectDeviceBtnTitle:</label>
<select data-action="sourceSelect" style="max-width:100%" class="form-select form-control">
</select>
</div>
<div>
<video id="video"
muted
webkit-playsinline
playsinline
x-webkit-airplay="allow"
x5-video-player-type="h5"
x5-video-player-fullscreen="true"
x5-video-orientation="portraint"
style="min-height:150px;max-height:50%; max-width: 100%;border: 1px solid gray"></video>
</div>
</div>
</div>
</div>
</div>
}
+260
View File
@@ -0,0 +1,260 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using EgwCoreLib.Utils;
using ZXingBlazor.Components;
namespace EgwCoreLib.Razor
{
public partial class BarcodeReader : IAsyncDisposable
{
[Inject]
[NotNull]
private IJSRuntime? JS { get; set; }
private IJSObjectReference? module;
private DotNetObjectReference<BarcodeReader>? Instance { get; set; }
[NotNull]
private StorageService? Storage { get; set; }
/// <summary>
/// Scan button title
/// </summary>
[Parameter]
public string ScanBtnTitle { get; set; } = "扫码";
/// <summary>
/// Reset button title
/// </summary>
[Parameter]
public string ResetBtnTitle { get; set; } = "复位";
/// <summary>
/// Close button title
/// </summary>
[Parameter]
public string CloseBtnTitle { get; set; } = "关闭";
/// <summary>
/// Select device button title
/// </summary>
[Parameter]
public string SelectDeviceBtnTitle { get; set; } = "选择设备";
/// <summary>
/// Scan result callback method
/// </summary>
[Parameter]
public EventCallback<string> ScanResult { get; set; }
/// <summary>
/// Close scan code callback method
/// </summary>
[Parameter]
public EventCallback Close { get; set; }
/// <summary>
/// Error callback method
/// </summary>
[Parameter]
public Func<string, Task>? OnError { get; set; }
/// <summary>
/// Use builtin Div
/// </summary>
[Parameter] public bool UseBuiltinDiv { get; set; } = true;
/// <summary>
/// Decode only Pdf417 format
/// </summary>
[Parameter]
public bool Pdf417Only { get; set; }
/// <summary>
/// Decode Once or Decode Continuously, default is Once
/// </summary>
[Parameter]
public bool Decodeonce { get; set; } = true;
/// <summary>
/// Decode All Formats, performance is poor, you can set options.formats to customize specify the encoding formats. The default is false
/// </summary>
[Parameter]
public bool DecodeAllFormats { get; set; }
/// <summary>
/// ZXingOptions
/// </summary>
[Parameter]
public ZXingOptions? Options { get; set; }
/// <summary>
///
/// </summary>
public ElementReference Element { get; set; }
/// <summary>
/// Device ID
/// </summary>
[Parameter]
public string? DeviceID { get; set; }
/// <summary>
/// Save the last used device ID to be called automatically next time
/// </summary>
[Parameter]
public bool SaveDeviceID { get; set; } = true;
// To prevent making JavaScript interop calls during prerendering
protected override async Task OnAfterRenderAsync(bool firstRender)
{
try
{
if (!firstRender) return;
Storage = new StorageService(JS);
module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/EgwCoreLib.Razor/BarcodeReader.razor.js" + "?v=" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
Instance = DotNetObjectReference.Create(this);
if (Options == null)
{
Options = new ZXingOptions()
{
Pdf417 = Pdf417Only,
Decodeonce = Decodeonce,
DecodeAllFormats = DecodeAllFormats,
//TRY_HARDER = true
};
}
try
{
if (SaveDeviceID) DeviceID = await Storage.GetValue("CamsDeviceID", DeviceID);
}
catch (Exception)
{
}
await module.InvokeVoidAsync("init", Instance, Element, Element.Id, Options, DeviceID);
}
catch (Exception e)
{
if (OnError != null) await OnError.Invoke(e.Message);
}
}
public async Task Start()
{
await module!.InvokeVoidAsync("start", Element.Id);
}
public async Task Stop()
{
await module!.InvokeVoidAsync("stop", Element.Id);
}
public async Task Reload()
{
await module!.InvokeVoidAsync("reload", Element.Id);
}
[JSInvokable]
public async Task GetResult(string val) => await ScanResult.InvokeAsync(val);
[JSInvokable]
public async Task CloseScan() => await Close.InvokeAsync();
[JSInvokable]
public async Task GetError(string err)
{
if (OnError != null) await OnError.Invoke(err);
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
await module!.InvokeVoidAsync("destroy", Element.Id);
Instance?.Dispose();
if (module is not null)
{
await module.DisposeAsync();
}
}
/// <summary>
/// 选择摄像头回调方法
/// </summary>
/// <param name="base64encodedstring"></param>
/// <returns></returns>
[JSInvokable]
public async Task SelectDeviceID(string deviceID, string deviceName)
{
try
{
if (SaveDeviceID)
{
await Storage.SetValue("CamsDeviceID", deviceID);
await Storage.SetValue("CamsDeviceName", deviceName);
}
}
catch
{
}
}
#region StorageService
private class StorageService
{
private readonly IJSRuntime JSRuntime;
public StorageService(IJSRuntime jsRuntime)
{
JSRuntime = jsRuntime;
}
public async Task SetValue<TValue>(string key, TValue value)
{
await JSRuntime.InvokeVoidAsync("eval", $"localStorage.setItem('{key}', '{value}')");
}
public async Task<TValue?> GetValue<TValue>(string key, TValue? def)
{
try
{
var cValue = await JSRuntime.InvokeAsync<TValue>("eval", $"localStorage.getItem('{key}');");
return cValue ?? def;
}
catch
{
var cValue = await JSRuntime.InvokeAsync<string>("eval", $"localStorage.getItem('{key}');");
if (cValue == null)
return def;
var newValue = GetValueI<TValue>(cValue);
return newValue ?? def;
}
}
public static T? GetValueI<T>(string value)
{
TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
return (T?)converter.ConvertFrom(value);
}
return default;
//return (T)Convert.ChangeType(value, typeof(T));
}
public async Task RemoveValue(string key)
{
await JSRuntime.InvokeVoidAsync("eval", $"localStorage.removeItem('{key}')");
}
}
#endregion
}
}
+317
View File
@@ -0,0 +1,317 @@
import './lib/zxing/zxing.min.js';
let codeReader = null;
let id = null;
let supportsVibrate = false;
let opt = null;
let inst = null;
let selectedDeviceId = null;
let deviceID = null;
let element = null;
let debug = false;
export function vibrate() {
if (supportsVibrate) navigator.vibrate(1000);
}
export function init(instance, ele, elementid, options, deviceid) {
console.log('init' + elementid);
inst = instance;
opt = options;
id = elementid;
deviceID = deviceid;
element = ele;
debug = options.debug;
supportsVibrate = "vibrate" in navigator;
let startButton = element.querySelector("[data-action=startButton]");
let resetButton = element.querySelector("[data-action=resetButton]");
let closeButton = element.querySelector("[data-action=closeButton]");
if (startButton) startButton.addEventListener('click', () => {
start(elementid);
})
if (resetButton) resetButton.addEventListener('click', () => {
stop(elementid);
if (debug) console.log('Reset.')
})
if (closeButton) closeButton.addEventListener('click', () => {
stop(elementid);
if (debug) console.log('closeButton.')
instance.invokeMethodAsync("CloseScan");
})
load(elementid);
}
export function reload(elementid) {
load(elementid);
}
function genHints(opt) {
const hints = new Map();
if (opt.TRY_HARDER) {
hints.set(ZXing.DecodeHintType.TRY_HARDER, opt.TRY_HARDER);
}
if (opt.ASSUME_CODE_39_CHECK_DIGIT) {
hints.set(ZXing.DecodeHintType.ASSUME_CODE_39_CHECK_DIGIT, opt.ASSUME_CODE_39_CHECK_DIGIT);
}
if (opt.ASSUME_GS1) {
hints.set(ZXing.DecodeHintType.ASSUME_GS1, opt.ASSUME_GS1);
}
if (opt.CHARACTER_SET) {
hints.set(ZXing.DecodeHintType.CHARACTER_SET, opt.CHARACTER_SET);
}
if (opt.OTHER) {
hints.set(ZXing.DecodeHintType.OTHER, opt.OTHER);
}
if (opt.PURE_BARCODE) {
hints.set(ZXing.DecodeHintType.PURE_BARCODE, opt.PURE_BARCODE);
}
if (opt.RETURN_CODABAR_START_END) {
hints.set(ZXing.DecodeHintType.RETURN_CODABAR_START_END, opt.RETURN_CODABAR_START_END);
}
if (opt.TRY_HARDER) {
hints.set(ZXing.DecodeHintType.TRY_HARDER, opt.TRY_HARDER);
}
return hints;
}
export function load(elementid) {
if (id == elementid) {
const sourceSelect = element.querySelector("[data-action=sourceSelect]");
const sourceSelectPanel = element.querySelector("[data-action=sourceSelectPanel]");
const hints = genHints(opt);
if (opt.pdf417) {
codeReader = new ZXing.BrowserPDF417Reader(hints);
if (debug) console.log('ZXing code PDF417 reader initialized')
} else if (opt.decodeAllFormats) {
const formats = opt.formats;
hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, formats);
codeReader = new ZXing.BrowserMultiFormatReader(hints)
if (debug) console.log('ZXing code reader initialized with all formats')
} else {
codeReader = new ZXing.BrowserMultiFormatReader(hints)
if (debug) console.log('ZXing code reader initialized')
}
codeReader.timeBetweenDecodingAttempts = opt.timeBetweenDecodingAttempts;
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({ audio: false, video: true })
.then(() => {
codeReader.listVideoInputDevices()
.then((videoInputDevices) => {
if (deviceID != null) {
selectedDeviceId = deviceID
} else if (videoInputDevices.length > 1) {
selectedDeviceId = videoInputDevices[1].deviceId
} else {
selectedDeviceId = videoInputDevices[0].deviceId
}
if (debug) console.log('videoInputDevices:' + videoInputDevices.length);
if (videoInputDevices.length > 1) {
sourceSelect.innerHTML = '';
videoInputDevices.forEach((device) => {
const sourceOption = document.createElement('option');
if (device.label === '') {
sourceOption.text = 'Camera' + (sourceSelect.length + 1);
} else {
sourceOption.text = device.label
}
sourceOption.value = device.deviceId
if (selectedDeviceId != null && device.deviceId == selectedDeviceId) {
sourceOption.selected = true
}
sourceSelect.appendChild(sourceOption)
})
sourceSelect.onchange = () => {
selectedDeviceId = sourceSelect.value;
inst.invokeMethodAsync('SelectDeviceID', selectedDeviceId, sourceSelect.options[sourceSelect.selectedIndex].text);
codeReader.reset();
start(elementid);
}
sourceSelectPanel.style.display = 'block'
}
start(elementid);
})
.catch((err) => {
console.log(err)
inst.invokeMethodAsync("GetError", err + '');
})
})
.catch((err) => {
console.error(`An error occurred: ${err}`);
inst.invokeMethodAsync('GetError', `An error occurred: ${err}`);
});
}
}
}
export function start(elementid) {
if (undefined !== codeReader && null !== codeReader && id == elementid) {
if (opt.decodeonce) {
codeReader.decodeOnceFromVideoDevice(selectedDeviceId, 'video').then((result) => {
if (debug) console.log(result)
vibrate();
if (debug) console.log('autostop');
codeReader.reset();
return inst.invokeMethodAsync("GetResult", result.text);
}).catch((err) => {
if (err && !(err instanceof ZXing.NotFoundException)) {
console.log(err)
inst.invokeMethodAsync("GetError", err + '');
}
})
} else {
codeReader.decodeFromVideoDevice(selectedDeviceId, 'video', (result, err) => {
if (result) {
if (debug) console.log(result)
vibrate();
if (debug) console.log('None-stop');
inst.invokeMethodAsync("GetResult", result.text);
}
if (err && !(err instanceof ZXing.NotFoundException)) {
console.log(err)
inst.invokeMethodAsync("GetError", err + '');
}
})
}
var x = `decodeContinuously`;
if (opt.decodeonce) x = `decodeOnce`;
if (debug) console.log(`Started ` + x + ` decode from camera with id ${selectedDeviceId}`)
if (debug) console.log(id, 'start');
}
}
export function stop(elementid) {
if (undefined !== codeReader && null !== codeReader && id == elementid) {
codeReader.reset();
if (debug) console.log(id, 'stop');
}
}
export function QRCodeSvg(instance, input, element, tobase64, size = 300) {
const codeWriter = new ZXing.BrowserQRCodeSvgWriter()
if (debug) console.log('ZXing code writer initialized')
if (tobase64) {
const elementTemp = document.createElement('elementTemp');
codeWriter.writeToDom(elementTemp, input, size, size)
let svgElement = elementTemp.firstChild
const svgData = (new XMLSerializer()).serializeToString(svgElement)
//const blob = new Blob([svgData])
instance.invokeMethodAsync("GetQRCode", svgData);
} else {
codeWriter.writeToDom(element.querySelector("[data-action=result]"), input, size, size)
}
}
export function DecodeFormImage(instance, element, options, data) {
var codeReaderImage = null;
const hints = genHints(options);
if (options.pdf417) {
codeReaderImage = new ZXing.BrowserPDF417Reader(hints);
if (debug) console.log('ZXing code PDF417 reader initialized')
} else if (options.decodeAllFormats) {
const formats = options.formats;
hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, formats);
codeReaderImage = new ZXing.BrowserMultiFormatReader(hints)
if (debug) console.log('ZXing code reader initialized with all formats')
} else {
codeReaderImage = new ZXing.BrowserMultiFormatReader(hints)
if (debug) console.log('ZXing code reader initialized')
}
if (debug) console.log('ZXing code reader initialized')
if (data != null) {
codeReaderImage.decodeFromImageUrl(data).then(result => {
if (result) {
vibrate();
if (debug) console.log(result.text);
instance.invokeMethodAsync('GetResult', result.text)
}
}).catch((err) => {
if (err) {
console.log(err)
instance.invokeMethodAsync('GetError', err.message)
}
})
}
else {
const resetFile = () => {
let file = element.querySelector('[type="file"]')
if (file) {
file.removeEventListener('change', scanImageHandler)
file.remove()
}
file = document.createElement('input')
file.setAttribute('type', 'file')
file.setAttribute('hidden', 'true')
file.setAttribute('accept', 'image/*')
//file.setAttribute('capture', 'true')
element.append(file)
file.addEventListener('change', scanImageHandler)
codeReaderImage.file = file
return file
}
const scanImageHandler = () => {
const files = codeReaderImage.file.files
if (files.length === 0) {
return
}
const reader = new FileReader()
reader.onloadend = e => {
codeReaderImage.decodeFromImageUrl(e.target.result).then(result => {
if (result) {
vibrate();
if (debug) console.log(result.text);
instance.invokeMethodAsync('GetResult', result.text)
} else {
instance.invokeMethodAsync('GetError', "no valid barcode detected")
}
}).catch((err) => {
if (err) {
console.log(err)
instance.invokeMethodAsync('GetError', err.message)
}
})
}
reader.readAsDataURL(files[0])
}
let file = resetFile()
file.click()
}
}
export function destroy(elementid) {
if (undefined !== codeReader && null !== codeReader && id == elementid) {
codeReader.reset();
codeReader = null;
//id = null;
id = null;
opt = null;
inst = null;
selectedDeviceId = null;
deviceID = null;
element = null;
if (debug) console.log(id, 'destroy');
}
}
File diff suppressed because one or more lines are too long
+189
View File
@@ -0,0 +1,189 @@
// **********************************
// Densen Informatica 中讯科技
// 作者:Alex Chow
// e-mail:zhouchuanglin@gmail.com
// **********************************
using System.Text.Json.Serialization;
namespace ZXingBlazor.Components;
/// <summary>
/// ZXing 选项类
/// </summary>
/// <remarks>https://zxing.github.io/zxing/apidocs/com/google/zxing/DecodeHintType.htm</remarks>
public class ZXingOptions
{
/// <summary>
/// 只解码 Pdf417 格式 / decode only Pdf417 format
/// </summary>
public bool Pdf417 { get; set; }
/// <summary>
/// 单次|连续解码,默认单次 / Decode Once or Decode Continuously, default is Once
/// </summary>
public bool Decodeonce { get; set; } = true;
/// <summary>
/// Time Between Decoding Attempts
/// </summary>
public int TimeBetweenDecodingAttempts { get; set; } = 10;
/// <summary>
/// 解码所有编码形式,性能较差, 开启后可用 options.formats 指定编码形式.默认为 false | Decodde All Formats, performance is poor, you can set options.formats to customize specify the encoding formats. The default is false
/// </summary>
public bool DecodeAllFormats { get; set; }
/// <summary>
/// 已知图像是几种可能的格式之一。
/// </summary>
public List<BarcodeFormat> formats { get; set; } = new List<BarcodeFormat>() {
BarcodeFormat.AZTEC ,
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_93,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
BarcodeFormat.ITF,
BarcodeFormat.MAXICODE,
BarcodeFormat.PDF_417,
BarcodeFormat.QR_CODE,
BarcodeFormat.RSS_14,
BarcodeFormat.RSS_EXPANDED,
BarcodeFormat.UPC_A,
BarcodeFormat.UPC_E,
BarcodeFormat.UPC_EAN_EXTENSION,
};
public bool Debug { get; set; }
///// <summary>
///// 如果为 true,尝试解码为倒置图像。所有配置的解码器都被简单地用倒置图像第二次调用
///// </summary>
//[JsonPropertyName("ALSO_INVERTED")]
//public bool ALSO_INVERTED { get; set; }
/// <summary>
/// EAN 或 UPC 条形码允许的扩展长度, 默认为 2.
/// </summary>
[JsonPropertyName("ALLOWED_EAN_EXTENSIONS")]
public int[]? ALLOWED_EAN_EXTENSIONS { get; set; } //= new int[] { 2 };
/// <summary>
/// 允许的编码数据长度——拒绝任何其他长度
/// </summary>
[JsonPropertyName("ALLOWED_LENGTHS")]
public int[]? ALLOWED_LENGTHS { get; set; }
/// <summary>
/// 假设 Code 39 代码使用校验位。
/// </summary>
[JsonPropertyName("ASSUME_CODE_39_CHECK_DIGIT")]
public bool? ASSUME_CODE_39_CHECK_DIGIT { get; set; }
/// <summary>
/// 假设条形码正在作为 GS1 条形码进行处理,并根据需要修改行为
/// </summary>
[JsonPropertyName("ASSUME_GS1")]
public bool? ASSUME_GS1 { get; set; }
/// <summary>
/// 指定解码时使用的字符编码(如果适用)
/// </summary>
[JsonPropertyName("CHARACTER_SET")]
public string? CHARACTER_SET { get; set; }
///// <summary>
///// ResultPoint 当发现可能的情况时,需要通过回调通知调用者, 映射到一个ResultPointCallback
///// </summary>
//public object NEED_RESULT_POINT_CALLBACK { get; set; }
/// <summary>
/// 未指定的、特定于应用程序的提示。
/// </summary>
[JsonPropertyName("OTHER")]
public object? OTHER { get; set; }
/// <summary>
/// 图像是条形码的纯单色图像。
/// </summary>
[JsonPropertyName("PURE_BARCODE")]
public bool? PURE_BARCODE { get; set; }
/// <summary>
/// 如果为 true,则返回 Codabar 条形码中的开始和结束数字,而不是剥离它们
/// <remark>如果为 true,则返回 Codabar 条形码中的开始和结束数字,而不是剥离它们。它们是字母,而其余的是数字。默认情况下,它们会被剥离,但这会导致它们不会被剥离</remark>
/// </summary>
[JsonPropertyName("RETURN_CODABAR_START_END")]
public bool? RETURN_CODABAR_START_END { get; set; }
/// <summary>
/// 花更多的时间尝试寻找条形码;优化准确性,而不是速度
/// </summary>
[JsonPropertyName("TRY_HARDER")]
public bool? TRY_HARDER { get; set; }
}
/**
* Enumerates barcode formats known to this package. Please keep alphabetized.
*
* @author Sean Owen
*/
public enum BarcodeFormat
{
/** Aztec 2D barcode format. */
AZTEC,
/** CODABAR 1D format. */
CODABAR,
/** Code 39 1D format. */
CODE_39,
/** Code 93 1D format. */
CODE_93,
/** Code 128 1D format. */
CODE_128,
/** Data Matrix 2D barcode format. */
DATA_MATRIX,
/** EAN-8 1D format. */
EAN_8,
/** EAN-13 1D format. */
EAN_13,
/** ITF (Interleaved Two of Five) 1D format. */
ITF,
/** MaxiCode 2D barcode format. */
MAXICODE,
/** PDF417 format. */
PDF_417,
/** QR Code 2D barcode format. */
QR_CODE,
/** RSS 14 */
RSS_14,
/** RSS EXPANDED */
RSS_EXPANDED,
/** UPC-A 1D format. */
UPC_A,
/** UPC-E 1D format. */
UPC_E,
/** UPC/EAN extension format. Not a stand-alone format. */
UPC_EAN_EXTENSION
}