id-scanner-lib 1.3.3 → 1.5.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.
Files changed (101) hide show
  1. package/README.md +55 -460
  2. package/dist/id-scanner-lib.esm.js +4641 -0
  3. package/dist/id-scanner-lib.esm.js.map +1 -0
  4. package/dist/id-scanner-lib.js +14755 -0
  5. package/dist/id-scanner-lib.js.map +1 -0
  6. package/dist/types/core/base-module.d.ts +44 -0
  7. package/dist/types/core/camera-manager.d.ts +258 -0
  8. package/dist/types/core/config.d.ts +88 -0
  9. package/dist/types/core/errors.d.ts +111 -0
  10. package/dist/types/core/event-emitter.d.ts +55 -0
  11. package/dist/types/core/logger.d.ts +277 -0
  12. package/dist/types/core/module-manager.d.ts +78 -0
  13. package/dist/types/core/plugin-manager.d.ts +158 -0
  14. package/dist/types/core/resource-manager.d.ts +246 -0
  15. package/dist/types/core/result.d.ts +83 -0
  16. package/dist/types/core/scanner-factory.d.ts +93 -0
  17. package/dist/types/index.bundle.d.ts +1303 -0
  18. package/dist/types/index.d.ts +86 -0
  19. package/dist/types/interfaces/external-types.d.ts +174 -0
  20. package/dist/types/interfaces/face-detection.d.ts +293 -0
  21. package/dist/types/interfaces/scanner-module.d.ts +280 -0
  22. package/dist/types/modules/face/face-detector.d.ts +170 -0
  23. package/dist/types/modules/face/index.d.ts +56 -0
  24. package/dist/types/modules/face/liveness-detector.d.ts +177 -0
  25. package/dist/types/modules/face/types.d.ts +136 -0
  26. package/dist/types/modules/id-card/anti-fake-detector.d.ts +170 -0
  27. package/dist/types/modules/id-card/id-card-detector.d.ts +131 -0
  28. package/dist/types/modules/id-card/index.d.ts +89 -0
  29. package/dist/types/modules/id-card/ocr-processor.d.ts +110 -0
  30. package/dist/types/modules/id-card/ocr-worker.d.ts +31 -0
  31. package/dist/types/modules/id-card/types.d.ts +181 -0
  32. package/dist/types/modules/qrcode/index.d.ts +51 -0
  33. package/dist/types/modules/qrcode/qr-code-scanner.d.ts +64 -0
  34. package/dist/types/modules/qrcode/types.d.ts +67 -0
  35. package/dist/types/utils/camera.d.ts +81 -0
  36. package/dist/types/utils/image-processing.d.ts +176 -0
  37. package/dist/types/utils/index.d.ts +175 -0
  38. package/dist/types/utils/performance.d.ts +81 -0
  39. package/dist/types/utils/resource-manager.d.ts +53 -0
  40. package/dist/types/utils/types.d.ts +166 -0
  41. package/dist/types/utils/worker.d.ts +52 -0
  42. package/dist/types/version.d.ts +7 -0
  43. package/package.json +76 -75
  44. package/src/core/base-module.ts +78 -0
  45. package/src/core/camera-manager.ts +798 -0
  46. package/src/core/config.ts +268 -0
  47. package/src/core/errors.ts +174 -0
  48. package/src/core/event-emitter.ts +110 -0
  49. package/src/core/logger.ts +549 -0
  50. package/src/core/module-manager.ts +165 -0
  51. package/src/core/plugin-manager.ts +429 -0
  52. package/src/core/resource-manager.ts +762 -0
  53. package/src/core/result.ts +163 -0
  54. package/src/core/scanner-factory.ts +237 -0
  55. package/src/index.ts +113 -936
  56. package/src/interfaces/external-types.ts +200 -0
  57. package/src/interfaces/face-detection.ts +309 -0
  58. package/src/interfaces/scanner-module.ts +384 -0
  59. package/src/modules/face/face-detector.ts +931 -0
  60. package/src/modules/face/index.ts +208 -0
  61. package/src/modules/face/liveness-detector.ts +908 -0
  62. package/src/modules/face/types.ts +133 -0
  63. package/src/{id-recognition → modules/id-card}/anti-fake-detector.ts +273 -239
  64. package/src/modules/id-card/id-card-detector.ts +474 -0
  65. package/src/modules/id-card/index.ts +425 -0
  66. package/src/{id-recognition → modules/id-card}/ocr-processor.ts +149 -92
  67. package/src/modules/id-card/ocr-worker.ts +259 -0
  68. package/src/modules/id-card/types.ts +178 -0
  69. package/src/modules/qrcode/index.ts +175 -0
  70. package/src/modules/qrcode/qr-code-scanner.ts +230 -0
  71. package/src/modules/qrcode/types.ts +65 -0
  72. package/src/types/tesseract.d.ts +265 -22
  73. package/src/utils/image-processing.ts +68 -49
  74. package/src/utils/index.ts +426 -0
  75. package/src/utils/performance.ts +168 -131
  76. package/src/utils/resource-manager.ts +65 -146
  77. package/src/utils/types.ts +90 -2
  78. package/src/utils/worker.ts +123 -84
  79. package/src/version.ts +11 -0
  80. package/tools/scaffold.js +543 -0
  81. package/dist/id-scanner-core.esm.js +0 -11349
  82. package/dist/id-scanner-core.js +0 -11361
  83. package/dist/id-scanner-core.min.js +0 -1
  84. package/dist/id-scanner-ocr.esm.js +0 -2319
  85. package/dist/id-scanner-ocr.js +0 -2328
  86. package/dist/id-scanner-ocr.min.js +0 -1
  87. package/dist/id-scanner-qr.esm.js +0 -1296
  88. package/dist/id-scanner-qr.js +0 -1305
  89. package/dist/id-scanner-qr.min.js +0 -1
  90. package/dist/id-scanner.js +0 -4561
  91. package/dist/id-scanner.min.js +0 -1
  92. package/src/core.ts +0 -138
  93. package/src/demo/demo.ts +0 -204
  94. package/src/id-recognition/data-extractor.ts +0 -262
  95. package/src/id-recognition/id-detector.ts +0 -510
  96. package/src/id-recognition/ocr-worker.ts +0 -156
  97. package/src/index-umd.ts +0 -477
  98. package/src/ocr-module.ts +0 -187
  99. package/src/qr-module.ts +0 -179
  100. package/src/scanner/barcode-scanner.ts +0 -251
  101. package/src/scanner/qr-scanner.ts +0 -167
@@ -1,4561 +0,0 @@
1
- (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('tesseract.js'), require('jsqr')) :
3
- typeof define === 'function' && define.amd ? define(['exports', 'tesseract.js', 'jsqr'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.IDScanner = {}, global.Tesseract, global.jsQR));
5
- })(this, (function (exports, tesseract_js, jsQR) { 'use strict';
6
-
7
- /**
8
- * @file 相机工具类
9
- * @description 提供访问和控制设备摄像头的功能
10
- * @module Camera
11
- */
12
- /**
13
- * 相机工具类
14
- *
15
- * 提供访问设备摄像头、获取视频流以及捕获图像帧的功能
16
- *
17
- * @example
18
- * ```typescript
19
- * // 创建相机实例
20
- * const camera = new Camera({
21
- * width: 1280,
22
- * height: 720,
23
- * facingMode: 'environment' // 使用后置摄像头
24
- * });
25
- *
26
- * // 初始化相机
27
- * const videoElement = document.getElementById('video') as HTMLVideoElement;
28
- * await camera.start(videoElement);
29
- *
30
- * // 捕获当前视频帧
31
- * const imageData = camera.captureFrame();
32
- *
33
- * // 使用结束后释放资源
34
- * camera.stop();
35
- * ```
36
- */
37
- class Camera {
38
- /**
39
- * 创建相机实例
40
- * @param {CameraOptions} [options] - 相机配置选项
41
- */
42
- constructor(options = {}) {
43
- this.options = options;
44
- this.stream = null;
45
- this.videoElement = null;
46
- this.options = {
47
- width: 640,
48
- height: 480,
49
- facingMode: 'environment',
50
- ...options
51
- };
52
- }
53
- /**
54
- * 启动摄像头并将视频流绑定到视频元素
55
- * @param videoElement HTML视频元素
56
- * @returns Promise<void>
57
- */
58
- async start(videoElement) {
59
- return this.initialize(videoElement);
60
- }
61
- /**
62
- * 停止摄像头并释放资源
63
- */
64
- stop() {
65
- this.release();
66
- }
67
- /**
68
- * 初始化相机,获取视频流并绑定到视频元素
69
- *
70
- * @param {HTMLVideoElement} videoElement - 用于显示视频流的视频元素
71
- * @returns {Promise<void>} 初始化完成的Promise
72
- * @throws 如果无法访问摄像头,将抛出错误
73
- */
74
- async initialize(videoElement) {
75
- this.videoElement = videoElement;
76
- try {
77
- // 构建媒体约束
78
- const constraints = {
79
- video: {
80
- width: { ideal: this.options.width },
81
- height: { ideal: this.options.height },
82
- facingMode: this.options.facingMode
83
- }
84
- };
85
- // 获取视频流
86
- this.stream = await navigator.mediaDevices.getUserMedia(constraints);
87
- // 绑定到视频元素
88
- if (this.videoElement) {
89
- this.videoElement.srcObject = this.stream;
90
- await new Promise((resolve) => {
91
- if (this.videoElement) {
92
- this.videoElement.onloadedmetadata = () => {
93
- if (this.videoElement) {
94
- this.videoElement.play().then(() => resolve());
95
- }
96
- };
97
- }
98
- });
99
- }
100
- }
101
- catch (error) {
102
- console.error('无法访问摄像头:', error);
103
- throw new Error('无法访问摄像头。请确保已授予摄像头访问权限,并且摄像头未被其他应用程序占用。');
104
- }
105
- }
106
- /**
107
- * 捕获当前视频帧
108
- *
109
- * @returns {ImageData|null} 视频帧的ImageData对象,如果未初始化则返回null
110
- */
111
- captureFrame() {
112
- if (!this.videoElement) {
113
- return null;
114
- }
115
- // 创建Canvas元素用于捕获视频帧
116
- const canvas = document.createElement('canvas');
117
- canvas.width = this.videoElement.videoWidth;
118
- canvas.height = this.videoElement.videoHeight;
119
- const context = canvas.getContext('2d');
120
- if (!context) {
121
- return null;
122
- }
123
- // 将视频内容绘制到Canvas中
124
- context.drawImage(this.videoElement, 0, 0, canvas.width, canvas.height);
125
- // 获取ImageData对象
126
- return context.getImageData(0, 0, canvas.width, canvas.height);
127
- }
128
- /**
129
- * 释放摄像头资源
130
- */
131
- release() {
132
- // 停止视频流的所有轨道
133
- if (this.stream) {
134
- this.stream.getTracks().forEach(track => track.stop());
135
- this.stream = null;
136
- }
137
- // 清除视频元素绑定
138
- if (this.videoElement) {
139
- this.videoElement.srcObject = null;
140
- this.videoElement = null;
141
- }
142
- }
143
- }
144
-
145
- /**
146
- * @file 二维码扫描模块
147
- * @description 提供实时二维码扫描和识别功能
148
- * @module QRScanner
149
- */
150
- /**
151
- * 二维码扫描器类
152
- *
153
- * 提供实时扫描和识别摄像头中的二维码的功能
154
- *
155
- * @example
156
- * ```typescript
157
- * // 创建二维码扫描器
158
- * const qrScanner = new QRScanner({
159
- * scanInterval: 100, // 每100ms扫描一次
160
- * onScan: (result) => {
161
- * console.log('扫描到二维码:', result);
162
- * },
163
- * onError: (error) => {
164
- * console.error('扫描错误:', error);
165
- * }
166
- * });
167
- *
168
- * // 启动扫描
169
- * const videoElement = document.getElementById('video') as HTMLVideoElement;
170
- * await qrScanner.start(videoElement);
171
- *
172
- * // 停止扫描
173
- * qrScanner.stop();
174
- * ```
175
- */
176
- class QRScanner {
177
- /**
178
- * 创建二维码扫描器实例
179
- *
180
- * @param {QRScannerOptions} [options] - 扫描器配置选项
181
- */
182
- constructor(options = {}) {
183
- this.options = options;
184
- this.scanning = false;
185
- this.scanTimer = null;
186
- this.options = {
187
- scanInterval: 200,
188
- ...options,
189
- };
190
- this.camera = new Camera();
191
- }
192
- /**
193
- * 启动二维码扫描
194
- *
195
- * 初始化相机并开始连续扫描视频帧中的二维码
196
- *
197
- * @param {HTMLVideoElement} videoElement - 用于显示相机画面的video元素
198
- * @returns {Promise<void>} 启动完成的Promise
199
- * @throws 如果无法访问相机,将通过onError回调报告错误
200
- */
201
- async start(videoElement) {
202
- try {
203
- await this.camera.initialize(videoElement);
204
- this.scanning = true;
205
- this.scan();
206
- }
207
- catch (error) {
208
- if (this.options.onError) {
209
- this.options.onError(error instanceof Error ? error : new Error(String(error)));
210
- }
211
- }
212
- }
213
- /**
214
- * 执行一次二维码扫描
215
- *
216
- * 内部方法,捕获当前视频帧并尝试识别其中的二维码
217
- *
218
- * @private
219
- */
220
- scan() {
221
- if (!this.scanning)
222
- return;
223
- const imageData = this.camera.captureFrame();
224
- if (imageData) {
225
- const code = jsQR(imageData.data, imageData.width, imageData.height);
226
- if (code && this.options.onScan) {
227
- this.options.onScan(code.data);
228
- }
229
- }
230
- this.scanTimer = window.setTimeout(() => this.scan(), this.options.scanInterval);
231
- }
232
- /**
233
- * 停止二维码扫描
234
- *
235
- * 停止扫描循环并释放相机资源
236
- */
237
- stop() {
238
- this.scanning = false;
239
- if (this.scanTimer) {
240
- clearTimeout(this.scanTimer);
241
- this.scanTimer = null;
242
- }
243
- this.camera.release();
244
- }
245
- /**
246
- * 处理图像数据中的二维码
247
- *
248
- * @param {ImageData} imageData - 要处理的图像数据
249
- * @returns {string | null} 识别到的二维码内容,如未识别到则返回null
250
- */
251
- processImageData(imageData) {
252
- try {
253
- if (!imageData ||
254
- !imageData.data ||
255
- imageData.width <= 0 ||
256
- imageData.height <= 0) {
257
- throw new Error("无效的图像数据");
258
- }
259
- const code = jsQR(imageData.data, imageData.width, imageData.height);
260
- if (code && code.data) {
261
- return code.data;
262
- }
263
- return null;
264
- }
265
- catch (error) {
266
- if (this.options.onError) {
267
- this.options.onError(error instanceof Error ? error : new Error(String(error)));
268
- }
269
- return null;
270
- }
271
- }
272
- }
273
-
274
- /**
275
- * Browser Image Compression
276
- * v2.0.2
277
- * by Donald <donaldcwl@gmail.com>
278
- * https://github.com/Donaldcwl/browser-image-compression
279
- */
280
-
281
- function _mergeNamespaces$1(e,t){return t.forEach((function(t){t&&"string"!=typeof t&&!Array.isArray(t)&&Object.keys(t).forEach((function(r){if("default"!==r&&!(r in e)){var i=Object.getOwnPropertyDescriptor(t,r);Object.defineProperty(e,r,i.get?i:{enumerable:true,get:function(){return t[r]}});}}));})),Object.freeze(e)}function copyExifWithoutOrientation(e,t){return new Promise((function(r,i){let o;return getApp1Segment(e).then((function(e){try{return o=e,r(new Blob([t.slice(0,2),o,t.slice(2)],{type:"image/jpeg"}))}catch(e){return i(e)}}),i)}))}const getApp1Segment=e=>new Promise(((t,r)=>{const i=new FileReader;i.addEventListener("load",(({target:{result:e}})=>{const i=new DataView(e);let o=0;if(65496!==i.getUint16(o))return r("not a valid JPEG");for(o+=2;;){const a=i.getUint16(o);if(65498===a)break;const s=i.getUint16(o+2);if(65505===a&&1165519206===i.getUint32(o+4)){const a=o+10;let f;switch(i.getUint16(a)){case 18761:f=true;break;case 19789:f=false;break;default:return r("TIFF header contains invalid endian")}if(42!==i.getUint16(a+2,f))return r("TIFF header contains invalid version");const l=i.getUint32(a+4,f),c=a+l+2+12*i.getUint16(a+l,f);for(let e=a+l+2;e<c;e+=12){if(274==i.getUint16(e,f)){if(3!==i.getUint16(e+2,f))return r("Orientation data type is invalid");if(1!==i.getUint32(e+4,f))return r("Orientation data count is invalid");i.setUint16(e+8,1,f);break}}return t(e.slice(o,o+2+s))}o+=2+s;}return t(new Blob)})),i.readAsArrayBuffer(e);}));var e={},t={get exports(){return e},set exports(t){e=t;}};!function(e){var r,i,UZIP={};t.exports=UZIP,UZIP.parse=function(e,t){for(var r=UZIP.bin.readUshort,i=UZIP.bin.readUint,o=0,a={},s=new Uint8Array(e),f=s.length-4;101010256!=i(s,f);)f--;o=f;o+=4;var l=r(s,o+=4);r(s,o+=2);var c=i(s,o+=2),u=i(s,o+=4);o+=4,o=u;for(var h=0;h<l;h++){i(s,o),o+=4,o+=4,o+=4,i(s,o+=4);c=i(s,o+=4);var d=i(s,o+=4),A=r(s,o+=4),g=r(s,o+2),p=r(s,o+4);o+=6;var m=i(s,o+=8);o+=4,o+=A+g+p,UZIP._readLocal(s,m,a,c,d,t);}return a},UZIP._readLocal=function(e,t,r,i,o,a){var s=UZIP.bin.readUshort,f=UZIP.bin.readUint;f(e,t),s(e,t+=4),s(e,t+=2);var l=s(e,t+=2);f(e,t+=2),f(e,t+=4),t+=4;var c=s(e,t+=8),u=s(e,t+=2);t+=2;var h=UZIP.bin.readUTF8(e,t,c);if(t+=c,t+=u,a)r[h]={size:o,csize:i};else {var d=new Uint8Array(e.buffer,t);if(0==l)r[h]=new Uint8Array(d.buffer.slice(t,t+i));else {if(8!=l)throw "unknown compression method: "+l;var A=new Uint8Array(o);UZIP.inflateRaw(d,A),r[h]=A;}}},UZIP.inflateRaw=function(e,t){return UZIP.F.inflate(e,t)},UZIP.inflate=function(e,t){return UZIP.inflateRaw(new Uint8Array(e.buffer,e.byteOffset+2,e.length-6),t)},UZIP.deflate=function(e,t){null==t&&(t={level:6});var r=0,i=new Uint8Array(50+Math.floor(1.1*e.length));i[r]=120,i[r+1]=156,r+=2,r=UZIP.F.deflateRaw(e,i,r,t.level);var o=UZIP.adler(e,0,e.length);return i[r+0]=o>>>24&255,i[r+1]=o>>>16&255,i[r+2]=o>>>8&255,i[r+3]=o>>>0&255,new Uint8Array(i.buffer,0,r+4)},UZIP.deflateRaw=function(e,t){null==t&&(t={level:6});var r=new Uint8Array(50+Math.floor(1.1*e.length)),i=UZIP.F.deflateRaw(e,r,i,t.level);return new Uint8Array(r.buffer,0,i)},UZIP.encode=function(e,t){null==t&&(t=false);var r=0,i=UZIP.bin.writeUint,o=UZIP.bin.writeUshort,a={};for(var s in e){var f=!UZIP._noNeed(s)&&!t,l=e[s],c=UZIP.crc.crc(l,0,l.length);a[s]={cpr:f,usize:l.length,crc:c,file:f?UZIP.deflateRaw(l):l};}for(var s in a)r+=a[s].file.length+30+46+2*UZIP.bin.sizeUTF8(s);r+=22;var u=new Uint8Array(r),h=0,d=[];for(var s in a){var A=a[s];d.push(h),h=UZIP._writeHeader(u,h,s,A,0);}var g=0,p=h;for(var s in a){A=a[s];d.push(h),h=UZIP._writeHeader(u,h,s,A,1,d[g++]);}var m=h-p;return i(u,h,101010256),h+=4,o(u,h+=4,g),o(u,h+=2,g),i(u,h+=2,m),i(u,h+=4,p),h+=4,h+=2,u.buffer},UZIP._noNeed=function(e){var t=e.split(".").pop().toLowerCase();return -1!="png,jpg,jpeg,zip".indexOf(t)},UZIP._writeHeader=function(e,t,r,i,o,a){var s=UZIP.bin.writeUint,f=UZIP.bin.writeUshort,l=i.file;return s(e,t,0==o?67324752:33639248),t+=4,1==o&&(t+=2),f(e,t,20),f(e,t+=2,0),f(e,t+=2,i.cpr?8:0),s(e,t+=2,0),s(e,t+=4,i.crc),s(e,t+=4,l.length),s(e,t+=4,i.usize),f(e,t+=4,UZIP.bin.sizeUTF8(r)),f(e,t+=2,0),t+=2,1==o&&(t+=2,t+=2,s(e,t+=6,a),t+=4),t+=UZIP.bin.writeUTF8(e,t,r),0==o&&(e.set(l,t),t+=l.length),t},UZIP.crc={table:function(){for(var e=new Uint32Array(256),t=0;t<256;t++){for(var r=t,i=0;i<8;i++)1&r?r=3988292384^r>>>1:r>>>=1;e[t]=r;}return e}(),update:function(e,t,r,i){for(var o=0;o<i;o++)e=UZIP.crc.table[255&(e^t[r+o])]^e>>>8;return e},crc:function(e,t,r){return 4294967295^UZIP.crc.update(4294967295,e,t,r)}},UZIP.adler=function(e,t,r){for(var i=1,o=0,a=t,s=t+r;a<s;){for(var f=Math.min(a+5552,s);a<f;)o+=i+=e[a++];i%=65521,o%=65521;}return o<<16|i},UZIP.bin={readUshort:function(e,t){return e[t]|e[t+1]<<8},writeUshort:function(e,t,r){e[t]=255&r,e[t+1]=r>>8&255;},readUint:function(e,t){return 16777216*e[t+3]+(e[t+2]<<16|e[t+1]<<8|e[t])},writeUint:function(e,t,r){e[t]=255&r,e[t+1]=r>>8&255,e[t+2]=r>>16&255,e[t+3]=r>>24&255;},readASCII:function(e,t,r){for(var i="",o=0;o<r;o++)i+=String.fromCharCode(e[t+o]);return i},writeASCII:function(e,t,r){for(var i=0;i<r.length;i++)e[t+i]=r.charCodeAt(i);},pad:function(e){return e.length<2?"0"+e:e},readUTF8:function(e,t,r){for(var i,o="",a=0;a<r;a++)o+="%"+UZIP.bin.pad(e[t+a].toString(16));try{i=decodeURIComponent(o);}catch(i){return UZIP.bin.readASCII(e,t,r)}return i},writeUTF8:function(e,t,r){for(var i=r.length,o=0,a=0;a<i;a++){var s=r.charCodeAt(a);if(0==(4294967168&s))e[t+o]=s,o++;else if(0==(4294965248&s))e[t+o]=192|s>>6,e[t+o+1]=128|s>>0&63,o+=2;else if(0==(4294901760&s))e[t+o]=224|s>>12,e[t+o+1]=128|s>>6&63,e[t+o+2]=128|s>>0&63,o+=3;else {if(0!=(4292870144&s))throw "e";e[t+o]=240|s>>18,e[t+o+1]=128|s>>12&63,e[t+o+2]=128|s>>6&63,e[t+o+3]=128|s>>0&63,o+=4;}}return o},sizeUTF8:function(e){for(var t=e.length,r=0,i=0;i<t;i++){var o=e.charCodeAt(i);if(0==(4294967168&o))r++;else if(0==(4294965248&o))r+=2;else if(0==(4294901760&o))r+=3;else {if(0!=(4292870144&o))throw "e";r+=4;}}return r}},UZIP.F={},UZIP.F.deflateRaw=function(e,t,r,i){var o=[[0,0,0,0,0],[4,4,8,4,0],[4,5,16,8,0],[4,6,16,16,0],[4,10,16,32,0],[8,16,32,32,0],[8,16,128,128,0],[8,32,128,256,0],[32,128,258,1024,1],[32,258,258,4096,1]][i],a=UZIP.F.U,s=UZIP.F._goodIndex;var f=UZIP.F._putsE,l=0,c=r<<3,u=0,h=e.length;if(0==i){for(;l<h;){f(t,c,l+(_=Math.min(65535,h-l))==h?1:0),c=UZIP.F._copyExact(e,l,_,t,c+8),l+=_;}return c>>>3}var d=a.lits,A=a.strt,g=a.prev,p=0,m=0,w=0,v=0,b=0,y=0;for(h>2&&(A[y=UZIP.F._hash(e,0)]=0),l=0;l<h;l++){if(b=y,l+1<h-2){y=UZIP.F._hash(e,l+1);var E=l+1&32767;g[E]=A[y],A[y]=E;}if(u<=l){(p>14e3||m>26697)&&h-l>100&&(u<l&&(d[p]=l-u,p+=2,u=l),c=UZIP.F._writeBlock(l==h-1||u==h?1:0,d,p,v,e,w,l-w,t,c),p=m=v=0,w=l);var F=0;l<h-2&&(F=UZIP.F._bestMatch(e,l,g,b,Math.min(o[2],h-l),o[3]));var _=F>>>16,B=65535&F;if(0!=F){B=65535&F;var U=s(_=F>>>16,a.of0);a.lhst[257+U]++;var C=s(B,a.df0);a.dhst[C]++,v+=a.exb[U]+a.dxb[C],d[p]=_<<23|l-u,d[p+1]=B<<16|U<<8|C,p+=2,u=l+_;}else a.lhst[e[l]]++;m++;}}for(w==l&&0!=e.length||(u<l&&(d[p]=l-u,p+=2,u=l),c=UZIP.F._writeBlock(1,d,p,v,e,w,l-w,t,c),p=0,m=0,p=m=v=0,w=l);0!=(7&c);)c++;return c>>>3},UZIP.F._bestMatch=function(e,t,r,i,o,a){var s=32767&t,f=r[s],l=s-f+32768&32767;if(f==s||i!=UZIP.F._hash(e,t-l))return 0;for(var c=0,u=0,h=Math.min(32767,t);l<=h&&0!=--a&&f!=s;){if(0==c||e[t+c]==e[t+c-l]){var d=UZIP.F._howLong(e,t,l);if(d>c){if(u=l,(c=d)>=o)break;l+2<d&&(d=l+2);for(var A=0,g=0;g<d-2;g++){var p=t-l+g+32768&32767,m=p-r[p]+32768&32767;m>A&&(A=m,f=p);}}}l+=(s=f)-(f=r[s])+32768&32767;}return c<<16|u},UZIP.F._howLong=function(e,t,r){if(e[t]!=e[t-r]||e[t+1]!=e[t+1-r]||e[t+2]!=e[t+2-r])return 0;var i=t,o=Math.min(e.length,t+258);for(t+=3;t<o&&e[t]==e[t-r];)t++;return t-i},UZIP.F._hash=function(e,t){return (e[t]<<8|e[t+1])+(e[t+2]<<4)&65535},UZIP.saved=0,UZIP.F._writeBlock=function(e,t,r,i,o,a,s,f,l){var c,u,h,d,A,g,p,m,w,v=UZIP.F.U,b=UZIP.F._putsF,y=UZIP.F._putsE;v.lhst[256]++,u=(c=UZIP.F.getTrees())[0],h=c[1],d=c[2],A=c[3],g=c[4],p=c[5],m=c[6],w=c[7];var E=32+(0==(l+3&7)?0:8-(l+3&7))+(s<<3),F=i+UZIP.F.contSize(v.fltree,v.lhst)+UZIP.F.contSize(v.fdtree,v.dhst),_=i+UZIP.F.contSize(v.ltree,v.lhst)+UZIP.F.contSize(v.dtree,v.dhst);_+=14+3*p+UZIP.F.contSize(v.itree,v.ihst)+(2*v.ihst[16]+3*v.ihst[17]+7*v.ihst[18]);for(var B=0;B<286;B++)v.lhst[B]=0;for(B=0;B<30;B++)v.dhst[B]=0;for(B=0;B<19;B++)v.ihst[B]=0;var U=E<F&&E<_?0:F<_?1:2;if(b(f,l,e),b(f,l+1,U),l+=3,0==U){for(;0!=(7&l);)l++;l=UZIP.F._copyExact(o,a,s,f,l);}else {var C,I;if(1==U&&(C=v.fltree,I=v.fdtree),2==U){UZIP.F.makeCodes(v.ltree,u),UZIP.F.revCodes(v.ltree,u),UZIP.F.makeCodes(v.dtree,h),UZIP.F.revCodes(v.dtree,h),UZIP.F.makeCodes(v.itree,d),UZIP.F.revCodes(v.itree,d),C=v.ltree,I=v.dtree,y(f,l,A-257),y(f,l+=5,g-1),y(f,l+=5,p-4),l+=4;for(var Q=0;Q<p;Q++)y(f,l+3*Q,v.itree[1+(v.ordr[Q]<<1)]);l+=3*p,l=UZIP.F._codeTiny(m,v.itree,f,l),l=UZIP.F._codeTiny(w,v.itree,f,l);}for(var M=a,x=0;x<r;x+=2){for(var S=t[x],R=S>>>23,T=M+(8388607&S);M<T;)l=UZIP.F._writeLit(o[M++],C,f,l);if(0!=R){var O=t[x+1],P=O>>16,H=O>>8&255,L=255&O;y(f,l=UZIP.F._writeLit(257+H,C,f,l),R-v.of0[H]),l+=v.exb[H],b(f,l=UZIP.F._writeLit(L,I,f,l),P-v.df0[L]),l+=v.dxb[L],M+=R;}}l=UZIP.F._writeLit(256,C,f,l);}return l},UZIP.F._copyExact=function(e,t,r,i,o){var a=o>>>3;return i[a]=r,i[a+1]=r>>>8,i[a+2]=255-i[a],i[a+3]=255-i[a+1],a+=4,i.set(new Uint8Array(e.buffer,t,r),a),o+(r+4<<3)},UZIP.F.getTrees=function(){for(var e=UZIP.F.U,t=UZIP.F._hufTree(e.lhst,e.ltree,15),r=UZIP.F._hufTree(e.dhst,e.dtree,15),i=[],o=UZIP.F._lenCodes(e.ltree,i),a=[],s=UZIP.F._lenCodes(e.dtree,a),f=0;f<i.length;f+=2)e.ihst[i[f]]++;for(f=0;f<a.length;f+=2)e.ihst[a[f]]++;for(var l=UZIP.F._hufTree(e.ihst,e.itree,7),c=19;c>4&&0==e.itree[1+(e.ordr[c-1]<<1)];)c--;return [t,r,l,o,s,c,i,a]},UZIP.F.getSecond=function(e){for(var t=[],r=0;r<e.length;r+=2)t.push(e[r+1]);return t},UZIP.F.nonZero=function(e){for(var t="",r=0;r<e.length;r+=2)0!=e[r+1]&&(t+=(r>>1)+",");return t},UZIP.F.contSize=function(e,t){for(var r=0,i=0;i<t.length;i++)r+=t[i]*e[1+(i<<1)];return r},UZIP.F._codeTiny=function(e,t,r,i){for(var o=0;o<e.length;o+=2){var a=e[o],s=e[o+1];i=UZIP.F._writeLit(a,t,r,i);var f=16==a?2:17==a?3:7;a>15&&(UZIP.F._putsE(r,i,s,f),i+=f);}return i},UZIP.F._lenCodes=function(e,t){for(var r=e.length;2!=r&&0==e[r-1];)r-=2;for(var i=0;i<r;i+=2){var o=e[i+1],a=i+3<r?e[i+3]:-1,s=i+5<r?e[i+5]:-1,f=0==i?-1:e[i-1];if(0==o&&a==o&&s==o){for(var l=i+5;l+2<r&&e[l+2]==o;)l+=2;(c=Math.min(l+1-i>>>1,138))<11?t.push(17,c-3):t.push(18,c-11),i+=2*c-2;}else if(o==f&&a==o&&s==o){for(l=i+5;l+2<r&&e[l+2]==o;)l+=2;var c=Math.min(l+1-i>>>1,6);t.push(16,c-3),i+=2*c-2;}else t.push(o,0);}return r>>>1},UZIP.F._hufTree=function(e,t,r){var i=[],o=e.length,a=t.length,s=0;for(s=0;s<a;s+=2)t[s]=0,t[s+1]=0;for(s=0;s<o;s++)0!=e[s]&&i.push({lit:s,f:e[s]});var f=i.length,l=i.slice(0);if(0==f)return 0;if(1==f){var c=i[0].lit;l=0==c?1:0;return t[1+(c<<1)]=1,t[1+(l<<1)]=1,1}i.sort((function(e,t){return e.f-t.f}));var u=i[0],h=i[1],d=0,A=1,g=2;for(i[0]={lit:-1,f:u.f+h.f,l:u,r:h,d:0};A!=f-1;)u=d!=A&&(g==f||i[d].f<i[g].f)?i[d++]:i[g++],h=d!=A&&(g==f||i[d].f<i[g].f)?i[d++]:i[g++],i[A++]={lit:-1,f:u.f+h.f,l:u,r:h};var p=UZIP.F.setDepth(i[A-1],0);for(p>r&&(UZIP.F.restrictDepth(l,r,p),p=r),s=0;s<f;s++)t[1+(l[s].lit<<1)]=l[s].d;return p},UZIP.F.setDepth=function(e,t){return -1!=e.lit?(e.d=t,t):Math.max(UZIP.F.setDepth(e.l,t+1),UZIP.F.setDepth(e.r,t+1))},UZIP.F.restrictDepth=function(e,t,r){var i=0,o=1<<r-t,a=0;for(e.sort((function(e,t){return t.d==e.d?e.f-t.f:t.d-e.d})),i=0;i<e.length&&e[i].d>t;i++){var s=e[i].d;e[i].d=t,a+=o-(1<<r-s);}for(a>>>=r-t;a>0;){(s=e[i].d)<t?(e[i].d++,a-=1<<t-s-1):i++;}for(;i>=0;i--)e[i].d==t&&a<0&&(e[i].d--,a++);0!=a&&console.log("debt left");},UZIP.F._goodIndex=function(e,t){var r=0;return t[16|r]<=e&&(r|=16),t[8|r]<=e&&(r|=8),t[4|r]<=e&&(r|=4),t[2|r]<=e&&(r|=2),t[1|r]<=e&&(r|=1),r},UZIP.F._writeLit=function(e,t,r,i){return UZIP.F._putsF(r,i,t[e<<1]),i+t[1+(e<<1)]},UZIP.F.inflate=function(e,t){var r=Uint8Array;if(3==e[0]&&0==e[1])return t||new r(0);var i=UZIP.F,o=i._bitsF,a=i._bitsE,s=i._decodeTiny,f=i.makeCodes,l=i.codes2map,c=i._get17,u=i.U,h=null==t;h&&(t=new r(e.length>>>2<<3));for(var d,A,g=0,p=0,m=0,w=0,v=0,b=0,y=0,E=0,F=0;0==g;)if(g=o(e,F,1),p=o(e,F+1,2),F+=3,0!=p){if(h&&(t=UZIP.F._check(t,E+(1<<17))),1==p&&(d=u.flmap,A=u.fdmap,b=511,y=31),2==p){m=a(e,F,5)+257,w=a(e,F+5,5)+1,v=a(e,F+10,4)+4,F+=14;for(var _=0;_<38;_+=2)u.itree[_]=0,u.itree[_+1]=0;var B=1;for(_=0;_<v;_++){var U=a(e,F+3*_,3);u.itree[1+(u.ordr[_]<<1)]=U,U>B&&(B=U);}F+=3*v,f(u.itree,B),l(u.itree,B,u.imap),d=u.lmap,A=u.dmap,F=s(u.imap,(1<<B)-1,m+w,e,F,u.ttree);var C=i._copyOut(u.ttree,0,m,u.ltree);b=(1<<C)-1;var I=i._copyOut(u.ttree,m,w,u.dtree);y=(1<<I)-1,f(u.ltree,C),l(u.ltree,C,d),f(u.dtree,I),l(u.dtree,I,A);}for(;;){var Q=d[c(e,F)&b];F+=15&Q;var M=Q>>>4;if(M>>>8==0)t[E++]=M;else {if(256==M)break;var x=E+M-254;if(M>264){var S=u.ldef[M-257];x=E+(S>>>3)+a(e,F,7&S),F+=7&S;}var R=A[c(e,F)&y];F+=15&R;var T=R>>>4,O=u.ddef[T],P=(O>>>4)+o(e,F,15&O);for(F+=15&O,h&&(t=UZIP.F._check(t,E+(1<<17)));E<x;)t[E]=t[E++-P],t[E]=t[E++-P],t[E]=t[E++-P],t[E]=t[E++-P];E=x;}}}else {0!=(7&F)&&(F+=8-(7&F));var H=4+(F>>>3),L=e[H-4]|e[H-3]<<8;h&&(t=UZIP.F._check(t,E+L)),t.set(new r(e.buffer,e.byteOffset+H,L),E),F=H+L<<3,E+=L;}return t.length==E?t:t.slice(0,E)},UZIP.F._check=function(e,t){var r=e.length;if(t<=r)return e;var i=new Uint8Array(Math.max(r<<1,t));return i.set(e,0),i},UZIP.F._decodeTiny=function(e,t,r,i,o,a){for(var s=UZIP.F._bitsE,f=UZIP.F._get17,l=0;l<r;){var c=e[f(i,o)&t];o+=15&c;var u=c>>>4;if(u<=15)a[l]=u,l++;else {var h=0,d=0;16==u?(d=3+s(i,o,2),o+=2,h=a[l-1]):17==u?(d=3+s(i,o,3),o+=3):18==u&&(d=11+s(i,o,7),o+=7);for(var A=l+d;l<A;)a[l]=h,l++;}}return o},UZIP.F._copyOut=function(e,t,r,i){for(var o=0,a=0,s=i.length>>>1;a<r;){var f=e[a+t];i[a<<1]=0,i[1+(a<<1)]=f,f>o&&(o=f),a++;}for(;a<s;)i[a<<1]=0,i[1+(a<<1)]=0,a++;return o},UZIP.F.makeCodes=function(e,t){for(var r,i,o,a,s=UZIP.F.U,f=e.length,l=s.bl_count,c=0;c<=t;c++)l[c]=0;for(c=1;c<f;c+=2)l[e[c]]++;var u=s.next_code;for(r=0,l[0]=0,i=1;i<=t;i++)r=r+l[i-1]<<1,u[i]=r;for(o=0;o<f;o+=2)0!=(a=e[o+1])&&(e[o]=u[a],u[a]++);},UZIP.F.codes2map=function(e,t,r){for(var i=e.length,o=UZIP.F.U.rev15,a=0;a<i;a+=2)if(0!=e[a+1])for(var s=a>>1,f=e[a+1],l=s<<4|f,c=t-f,u=e[a]<<c,h=u+(1<<c);u!=h;){r[o[u]>>>15-t]=l,u++;}},UZIP.F.revCodes=function(e,t){for(var r=UZIP.F.U.rev15,i=15-t,o=0;o<e.length;o+=2){var a=e[o]<<t-e[o+1];e[o]=r[a]>>>i;}},UZIP.F._putsE=function(e,t,r){r<<=7&t;var i=t>>>3;e[i]|=r,e[i+1]|=r>>>8;},UZIP.F._putsF=function(e,t,r){r<<=7&t;var i=t>>>3;e[i]|=r,e[i+1]|=r>>>8,e[i+2]|=r>>>16;},UZIP.F._bitsE=function(e,t,r){return (e[t>>>3]|e[1+(t>>>3)]<<8)>>>(7&t)&(1<<r)-1},UZIP.F._bitsF=function(e,t,r){return (e[t>>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16)>>>(7&t)&(1<<r)-1},UZIP.F._get17=function(e,t){return (e[t>>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16)>>>(7&t)},UZIP.F._get25=function(e,t){return (e[t>>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16|e[3+(t>>>3)]<<24)>>>(7&t)},UZIP.F.U=(r=Uint16Array,i=Uint32Array,{next_code:new r(16),bl_count:new r(16),ordr:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],of0:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],exb:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],ldef:new r(32),df0:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],dxb:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],ddef:new i(32),flmap:new r(512),fltree:[],fdmap:new r(32),fdtree:[],lmap:new r(32768),ltree:[],ttree:[],dmap:new r(32768),dtree:[],imap:new r(512),itree:[],rev15:new r(32768),lhst:new i(286),dhst:new i(30),ihst:new i(19),lits:new i(15e3),strt:new r(65536),prev:new r(32768)}),function(){for(var e=UZIP.F.U,t=0;t<32768;t++){var r=t;r=(4278255360&(r=(4042322160&(r=(3435973836&(r=(2863311530&r)>>>1|(1431655765&r)<<1))>>>2|(858993459&r)<<2))>>>4|(252645135&r)<<4))>>>8|(16711935&r)<<8,e.rev15[t]=(r>>>16|r<<16)>>>17;}function pushV(e,t,r){for(;0!=t--;)e.push(0,r);}for(t=0;t<32;t++)e.ldef[t]=e.of0[t]<<3|e.exb[t],e.ddef[t]=e.df0[t]<<4|e.dxb[t];pushV(e.fltree,144,8),pushV(e.fltree,112,9),pushV(e.fltree,24,7),pushV(e.fltree,8,8),UZIP.F.makeCodes(e.fltree,9),UZIP.F.codes2map(e.fltree,9,e.flmap),UZIP.F.revCodes(e.fltree,9),pushV(e.fdtree,32,5),UZIP.F.makeCodes(e.fdtree,5),UZIP.F.codes2map(e.fdtree,5,e.fdmap),UZIP.F.revCodes(e.fdtree,5),pushV(e.itree,19,0),pushV(e.ltree,286,0),pushV(e.dtree,30,0),pushV(e.ttree,320,0);}();}();var UZIP=_mergeNamespaces$1({__proto__:null,default:e},[e]);const UPNG=function(){var e={nextZero(e,t){for(;0!=e[t];)t++;return t},readUshort:(e,t)=>e[t]<<8|e[t+1],writeUshort(e,t,r){e[t]=r>>8&255,e[t+1]=255&r;},readUint:(e,t)=>16777216*e[t]+(e[t+1]<<16|e[t+2]<<8|e[t+3]),writeUint(e,t,r){e[t]=r>>24&255,e[t+1]=r>>16&255,e[t+2]=r>>8&255,e[t+3]=255&r;},readASCII(e,t,r){let i="";for(let o=0;o<r;o++)i+=String.fromCharCode(e[t+o]);return i},writeASCII(e,t,r){for(let i=0;i<r.length;i++)e[t+i]=r.charCodeAt(i);},readBytes(e,t,r){const i=[];for(let o=0;o<r;o++)i.push(e[t+o]);return i},pad:e=>e.length<2?`0${e}`:e,readUTF8(t,r,i){let o,a="";for(let o=0;o<i;o++)a+=`%${e.pad(t[r+o].toString(16))}`;try{o=decodeURIComponent(a);}catch(o){return e.readASCII(t,r,i)}return o}};function decodeImage(t,r,i,o){const a=r*i,s=_getBPP(o),f=Math.ceil(r*s/8),l=new Uint8Array(4*a),c=new Uint32Array(l.buffer),{ctype:u}=o,{depth:h}=o,d=e.readUshort;if(6==u){const e=a<<2;if(8==h)for(var A=0;A<e;A+=4)l[A]=t[A],l[A+1]=t[A+1],l[A+2]=t[A+2],l[A+3]=t[A+3];if(16==h)for(A=0;A<e;A++)l[A]=t[A<<1];}else if(2==u){const e=o.tabs.tRNS;if(null==e){if(8==h)for(A=0;A<a;A++){var g=3*A;c[A]=255<<24|t[g+2]<<16|t[g+1]<<8|t[g];}if(16==h)for(A=0;A<a;A++){g=6*A;c[A]=255<<24|t[g+4]<<16|t[g+2]<<8|t[g];}}else {var p=e[0];const r=e[1],i=e[2];if(8==h)for(A=0;A<a;A++){var m=A<<2;g=3*A;c[A]=255<<24|t[g+2]<<16|t[g+1]<<8|t[g],t[g]==p&&t[g+1]==r&&t[g+2]==i&&(l[m+3]=0);}if(16==h)for(A=0;A<a;A++){m=A<<2,g=6*A;c[A]=255<<24|t[g+4]<<16|t[g+2]<<8|t[g],d(t,g)==p&&d(t,g+2)==r&&d(t,g+4)==i&&(l[m+3]=0);}}}else if(3==u){const e=o.tabs.PLTE,s=o.tabs.tRNS,c=s?s.length:0;if(1==h)for(var w=0;w<i;w++){var v=w*f,b=w*r;for(A=0;A<r;A++){m=b+A<<2;var y=3*(E=t[v+(A>>3)]>>7-((7&A)<<0)&1);l[m]=e[y],l[m+1]=e[y+1],l[m+2]=e[y+2],l[m+3]=E<c?s[E]:255;}}if(2==h)for(w=0;w<i;w++)for(v=w*f,b=w*r,A=0;A<r;A++){m=b+A<<2,y=3*(E=t[v+(A>>2)]>>6-((3&A)<<1)&3);l[m]=e[y],l[m+1]=e[y+1],l[m+2]=e[y+2],l[m+3]=E<c?s[E]:255;}if(4==h)for(w=0;w<i;w++)for(v=w*f,b=w*r,A=0;A<r;A++){m=b+A<<2,y=3*(E=t[v+(A>>1)]>>4-((1&A)<<2)&15);l[m]=e[y],l[m+1]=e[y+1],l[m+2]=e[y+2],l[m+3]=E<c?s[E]:255;}if(8==h)for(A=0;A<a;A++){var E;m=A<<2,y=3*(E=t[A]);l[m]=e[y],l[m+1]=e[y+1],l[m+2]=e[y+2],l[m+3]=E<c?s[E]:255;}}else if(4==u){if(8==h)for(A=0;A<a;A++){m=A<<2;var F=t[_=A<<1];l[m]=F,l[m+1]=F,l[m+2]=F,l[m+3]=t[_+1];}if(16==h)for(A=0;A<a;A++){var _;m=A<<2,F=t[_=A<<2];l[m]=F,l[m+1]=F,l[m+2]=F,l[m+3]=t[_+2];}}else if(0==u)for(p=o.tabs.tRNS?o.tabs.tRNS:-1,w=0;w<i;w++){const e=w*f,i=w*r;if(1==h)for(var B=0;B<r;B++){var U=(F=255*(t[e+(B>>>3)]>>>7-(7&B)&1))==255*p?0:255;c[i+B]=U<<24|F<<16|F<<8|F;}else if(2==h)for(B=0;B<r;B++){U=(F=85*(t[e+(B>>>2)]>>>6-((3&B)<<1)&3))==85*p?0:255;c[i+B]=U<<24|F<<16|F<<8|F;}else if(4==h)for(B=0;B<r;B++){U=(F=17*(t[e+(B>>>1)]>>>4-((1&B)<<2)&15))==17*p?0:255;c[i+B]=U<<24|F<<16|F<<8|F;}else if(8==h)for(B=0;B<r;B++){U=(F=t[e+B])==p?0:255;c[i+B]=U<<24|F<<16|F<<8|F;}else if(16==h)for(B=0;B<r;B++){F=t[e+(B<<1)],U=d(t,e+(B<<1))==p?0:255;c[i+B]=U<<24|F<<16|F<<8|F;}}return l}function _decompress(e,r,i,o){const a=_getBPP(e),s=Math.ceil(i*a/8),f=new Uint8Array((s+1+e.interlace)*o);return r=e.tabs.CgBI?t(r,f):_inflate(r,f),0==e.interlace?r=_filterZero(r,e,0,i,o):1==e.interlace&&(r=function _readInterlace(e,t){const r=t.width,i=t.height,o=_getBPP(t),a=o>>3,s=Math.ceil(r*o/8),f=new Uint8Array(i*s);let l=0;const c=[0,0,4,0,2,0,1],u=[0,4,0,2,0,1,0],h=[8,8,8,4,4,2,2],d=[8,8,4,4,2,2,1];let A=0;for(;A<7;){const p=h[A],m=d[A];let w=0,v=0,b=c[A];for(;b<i;)b+=p,v++;let y=u[A];for(;y<r;)y+=m,w++;const E=Math.ceil(w*o/8);_filterZero(e,t,l,w,v);let F=0,_=c[A];for(;_<i;){let t=u[A],i=l+F*E<<3;for(;t<r;){var g;if(1==o)g=(g=e[i>>3])>>7-(7&i)&1,f[_*s+(t>>3)]|=g<<7-((7&t)<<0);if(2==o)g=(g=e[i>>3])>>6-(7&i)&3,f[_*s+(t>>2)]|=g<<6-((3&t)<<1);if(4==o)g=(g=e[i>>3])>>4-(7&i)&15,f[_*s+(t>>1)]|=g<<4-((1&t)<<2);if(o>=8){const r=_*s+t*a;for(let t=0;t<a;t++)f[r+t]=e[(i>>3)+t];}i+=o,t+=m;}F++,_+=p;}w*v!=0&&(l+=v*(1+E)),A+=1;}return f}(r,e)),r}function _inflate(e,r){return t(new Uint8Array(e.buffer,2,e.length-6),r)}var t=function(){const e={H:{}};return e.H.N=function(t,r){const i=Uint8Array;let o,a,s=0,f=0,l=0,c=0,u=0,h=0,d=0,A=0,g=0;if(3==t[0]&&0==t[1])return r||new i(0);const p=e.H,m=p.b,w=p.e,v=p.R,b=p.n,y=p.A,E=p.Z,F=p.m,_=null==r;for(_&&(r=new i(t.length>>>2<<5));0==s;)if(s=m(t,g,1),f=m(t,g+1,2),g+=3,0!=f){if(_&&(r=e.H.W(r,A+(1<<17))),1==f&&(o=F.J,a=F.h,h=511,d=31),2==f){l=w(t,g,5)+257,c=w(t,g+5,5)+1,u=w(t,g+10,4)+4,g+=14;let e=1;for(var B=0;B<38;B+=2)F.Q[B]=0,F.Q[B+1]=0;for(B=0;B<u;B++){const r=w(t,g+3*B,3);F.Q[1+(F.X[B]<<1)]=r,r>e&&(e=r);}g+=3*u,b(F.Q,e),y(F.Q,e,F.u),o=F.w,a=F.d,g=v(F.u,(1<<e)-1,l+c,t,g,F.v);const r=p.V(F.v,0,l,F.C);h=(1<<r)-1;const i=p.V(F.v,l,c,F.D);d=(1<<i)-1,b(F.C,r),y(F.C,r,o),b(F.D,i),y(F.D,i,a);}for(;;){const e=o[E(t,g)&h];g+=15&e;const i=e>>>4;if(i>>>8==0)r[A++]=i;else {if(256==i)break;{let e=A+i-254;if(i>264){const r=F.q[i-257];e=A+(r>>>3)+w(t,g,7&r),g+=7&r;}const o=a[E(t,g)&d];g+=15&o;const s=o>>>4,f=F.c[s],l=(f>>>4)+m(t,g,15&f);for(g+=15&f;A<e;)r[A]=r[A++-l],r[A]=r[A++-l],r[A]=r[A++-l],r[A]=r[A++-l];A=e;}}}}else {0!=(7&g)&&(g+=8-(7&g));const o=4+(g>>>3),a=t[o-4]|t[o-3]<<8;_&&(r=e.H.W(r,A+a)),r.set(new i(t.buffer,t.byteOffset+o,a),A),g=o+a<<3,A+=a;}return r.length==A?r:r.slice(0,A)},e.H.W=function(e,t){const r=e.length;if(t<=r)return e;const i=new Uint8Array(r<<1);return i.set(e,0),i},e.H.R=function(t,r,i,o,a,s){const f=e.H.e,l=e.H.Z;let c=0;for(;c<i;){const e=t[l(o,a)&r];a+=15&e;const i=e>>>4;if(i<=15)s[c]=i,c++;else {let e=0,t=0;16==i?(t=3+f(o,a,2),a+=2,e=s[c-1]):17==i?(t=3+f(o,a,3),a+=3):18==i&&(t=11+f(o,a,7),a+=7);const r=c+t;for(;c<r;)s[c]=e,c++;}}return a},e.H.V=function(e,t,r,i){let o=0,a=0;const s=i.length>>>1;for(;a<r;){const r=e[a+t];i[a<<1]=0,i[1+(a<<1)]=r,r>o&&(o=r),a++;}for(;a<s;)i[a<<1]=0,i[1+(a<<1)]=0,a++;return o},e.H.n=function(t,r){const i=e.H.m,o=t.length;let a,s,f;let l;const c=i.j;for(var u=0;u<=r;u++)c[u]=0;for(u=1;u<o;u+=2)c[t[u]]++;const h=i.K;for(a=0,c[0]=0,s=1;s<=r;s++)a=a+c[s-1]<<1,h[s]=a;for(f=0;f<o;f+=2)l=t[f+1],0!=l&&(t[f]=h[l],h[l]++);},e.H.A=function(t,r,i){const o=t.length,a=e.H.m.r;for(let e=0;e<o;e+=2)if(0!=t[e+1]){const o=e>>1,s=t[e+1],f=o<<4|s,l=r-s;let c=t[e]<<l;const u=c+(1<<l);for(;c!=u;){i[a[c]>>>15-r]=f,c++;}}},e.H.l=function(t,r){const i=e.H.m.r,o=15-r;for(let e=0;e<t.length;e+=2){const a=t[e]<<r-t[e+1];t[e]=i[a]>>>o;}},e.H.M=function(e,t,r){r<<=7&t;const i=t>>>3;e[i]|=r,e[i+1]|=r>>>8;},e.H.I=function(e,t,r){r<<=7&t;const i=t>>>3;e[i]|=r,e[i+1]|=r>>>8,e[i+2]|=r>>>16;},e.H.e=function(e,t,r){return (e[t>>>3]|e[1+(t>>>3)]<<8)>>>(7&t)&(1<<r)-1},e.H.b=function(e,t,r){return (e[t>>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16)>>>(7&t)&(1<<r)-1},e.H.Z=function(e,t){return (e[t>>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16)>>>(7&t)},e.H.i=function(e,t){return (e[t>>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16|e[3+(t>>>3)]<<24)>>>(7&t)},e.H.m=function(){const e=Uint16Array,t=Uint32Array;return {K:new e(16),j:new e(16),X:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],S:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],T:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],q:new e(32),p:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],z:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],c:new t(32),J:new e(512),_:[],h:new e(32),$:[],w:new e(32768),C:[],v:[],d:new e(32768),D:[],u:new e(512),Q:[],r:new e(32768),s:new t(286),Y:new t(30),a:new t(19),t:new t(15e3),k:new e(65536),g:new e(32768)}}(),function(){const t=e.H.m;for(var r=0;r<32768;r++){let e=r;e=(2863311530&e)>>>1|(1431655765&e)<<1,e=(3435973836&e)>>>2|(858993459&e)<<2,e=(4042322160&e)>>>4|(252645135&e)<<4,e=(4278255360&e)>>>8|(16711935&e)<<8,t.r[r]=(e>>>16|e<<16)>>>17;}function n(e,t,r){for(;0!=t--;)e.push(0,r);}for(r=0;r<32;r++)t.q[r]=t.S[r]<<3|t.T[r],t.c[r]=t.p[r]<<4|t.z[r];n(t._,144,8),n(t._,112,9),n(t._,24,7),n(t._,8,8),e.H.n(t._,9),e.H.A(t._,9,t.J),e.H.l(t._,9),n(t.$,32,5),e.H.n(t.$,5),e.H.A(t.$,5,t.h),e.H.l(t.$,5),n(t.Q,19,0),n(t.C,286,0),n(t.D,30,0),n(t.v,320,0);}(),e.H.N}();function _getBPP(e){return [1,null,3,1,2,null,4][e.ctype]*e.depth}function _filterZero(e,t,r,i,o){let a=_getBPP(t);const s=Math.ceil(i*a/8);let f,l;a=Math.ceil(a/8);let c=e[r],u=0;if(c>1&&(e[r]=[0,0,1][c-2]),3==c)for(u=a;u<s;u++)e[u+1]=e[u+1]+(e[u+1-a]>>>1)&255;for(let t=0;t<o;t++)if(f=r+t*s,l=f+t+1,c=e[l-1],u=0,0==c)for(;u<s;u++)e[f+u]=e[l+u];else if(1==c){for(;u<a;u++)e[f+u]=e[l+u];for(;u<s;u++)e[f+u]=e[l+u]+e[f+u-a];}else if(2==c)for(;u<s;u++)e[f+u]=e[l+u]+e[f+u-s];else if(3==c){for(;u<a;u++)e[f+u]=e[l+u]+(e[f+u-s]>>>1);for(;u<s;u++)e[f+u]=e[l+u]+(e[f+u-s]+e[f+u-a]>>>1);}else {for(;u<a;u++)e[f+u]=e[l+u]+_paeth(0,e[f+u-s],0);for(;u<s;u++)e[f+u]=e[l+u]+_paeth(e[f+u-a],e[f+u-s],e[f+u-a-s]);}return e}function _paeth(e,t,r){const i=e+t-r,o=i-e,a=i-t,s=i-r;return o*o<=a*a&&o*o<=s*s?e:a*a<=s*s?t:r}function _IHDR(t,r,i){i.width=e.readUint(t,r),r+=4,i.height=e.readUint(t,r),r+=4,i.depth=t[r],r++,i.ctype=t[r],r++,i.compress=t[r],r++,i.filter=t[r],r++,i.interlace=t[r],r++;}function _copyTile(e,t,r,i,o,a,s,f,l){const c=Math.min(t,o),u=Math.min(r,a);let h=0,d=0;for(let r=0;r<u;r++)for(let a=0;a<c;a++)if(s>=0&&f>=0?(h=r*t+a<<2,d=(f+r)*o+s+a<<2):(h=(-f+r)*t-s+a<<2,d=r*o+a<<2),0==l)i[d]=e[h],i[d+1]=e[h+1],i[d+2]=e[h+2],i[d+3]=e[h+3];else if(1==l){var A=e[h+3]*(1/255),g=e[h]*A,p=e[h+1]*A,m=e[h+2]*A,w=i[d+3]*(1/255),v=i[d]*w,b=i[d+1]*w,y=i[d+2]*w;const t=1-A,r=A+w*t,o=0==r?0:1/r;i[d+3]=255*r,i[d+0]=(g+v*t)*o,i[d+1]=(p+b*t)*o,i[d+2]=(m+y*t)*o;}else if(2==l){A=e[h+3],g=e[h],p=e[h+1],m=e[h+2],w=i[d+3],v=i[d],b=i[d+1],y=i[d+2];A==w&&g==v&&p==b&&m==y?(i[d]=0,i[d+1]=0,i[d+2]=0,i[d+3]=0):(i[d]=g,i[d+1]=p,i[d+2]=m,i[d+3]=A);}else if(3==l){A=e[h+3],g=e[h],p=e[h+1],m=e[h+2],w=i[d+3],v=i[d],b=i[d+1],y=i[d+2];if(A==w&&g==v&&p==b&&m==y)continue;if(A<220&&w>20)return false}return true}return {decode:function decode(r){const i=new Uint8Array(r);let o=8;const a=e,s=a.readUshort,f=a.readUint,l={tabs:{},frames:[]},c=new Uint8Array(i.length);let u,h=0,d=0;const A=[137,80,78,71,13,10,26,10];for(var g=0;g<8;g++)if(i[g]!=A[g])throw "The input is not a PNG file!";for(;o<i.length;){const e=a.readUint(i,o);o+=4;const r=a.readASCII(i,o,4);if(o+=4,"IHDR"==r)_IHDR(i,o,l);else if("iCCP"==r){for(var p=o;0!=i[p];)p++;a.readASCII(i,o,p-o);const s=i.slice(p+2,o+e);let f=null;try{f=_inflate(s);}catch(e){f=t(s);}l.tabs[r]=f;}else if("CgBI"==r)l.tabs[r]=i.slice(o,o+4);else if("IDAT"==r){for(g=0;g<e;g++)c[h+g]=i[o+g];h+=e;}else if("acTL"==r)l.tabs[r]={num_frames:f(i,o),num_plays:f(i,o+4)},u=new Uint8Array(i.length);else if("fcTL"==r){if(0!=d)(E=l.frames[l.frames.length-1]).data=_decompress(l,u.slice(0,d),E.rect.width,E.rect.height),d=0;const e={x:f(i,o+12),y:f(i,o+16),width:f(i,o+4),height:f(i,o+8)};let t=s(i,o+22);t=s(i,o+20)/(0==t?100:t);const r={rect:e,delay:Math.round(1e3*t),dispose:i[o+24],blend:i[o+25]};l.frames.push(r);}else if("fdAT"==r){for(g=0;g<e-4;g++)u[d+g]=i[o+g+4];d+=e-4;}else if("pHYs"==r)l.tabs[r]=[a.readUint(i,o),a.readUint(i,o+4),i[o+8]];else if("cHRM"==r){l.tabs[r]=[];for(g=0;g<8;g++)l.tabs[r].push(a.readUint(i,o+4*g));}else if("tEXt"==r||"zTXt"==r){null==l.tabs[r]&&(l.tabs[r]={});var m=a.nextZero(i,o),w=a.readASCII(i,o,m-o),v=o+e-m-1;if("tEXt"==r)y=a.readASCII(i,m+1,v);else {var b=_inflate(i.slice(m+2,m+2+v));y=a.readUTF8(b,0,b.length);}l.tabs[r][w]=y;}else if("iTXt"==r){null==l.tabs[r]&&(l.tabs[r]={});m=0,p=o;m=a.nextZero(i,p);w=a.readASCII(i,p,m-p);const t=i[p=m+1];var y;p+=2,m=a.nextZero(i,p),a.readASCII(i,p,m-p),p=m+1,m=a.nextZero(i,p),a.readUTF8(i,p,m-p);v=e-((p=m+1)-o);if(0==t)y=a.readUTF8(i,p,v);else {b=_inflate(i.slice(p,p+v));y=a.readUTF8(b,0,b.length);}l.tabs[r][w]=y;}else if("PLTE"==r)l.tabs[r]=a.readBytes(i,o,e);else if("hIST"==r){const e=l.tabs.PLTE.length/3;l.tabs[r]=[];for(g=0;g<e;g++)l.tabs[r].push(s(i,o+2*g));}else if("tRNS"==r)3==l.ctype?l.tabs[r]=a.readBytes(i,o,e):0==l.ctype?l.tabs[r]=s(i,o):2==l.ctype&&(l.tabs[r]=[s(i,o),s(i,o+2),s(i,o+4)]);else if("gAMA"==r)l.tabs[r]=a.readUint(i,o)/1e5;else if("sRGB"==r)l.tabs[r]=i[o];else if("bKGD"==r)0==l.ctype||4==l.ctype?l.tabs[r]=[s(i,o)]:2==l.ctype||6==l.ctype?l.tabs[r]=[s(i,o),s(i,o+2),s(i,o+4)]:3==l.ctype&&(l.tabs[r]=i[o]);else if("IEND"==r)break;o+=e,a.readUint(i,o),o+=4;}var E;return 0!=d&&((E=l.frames[l.frames.length-1]).data=_decompress(l,u.slice(0,d),E.rect.width,E.rect.height)),l.data=_decompress(l,c,l.width,l.height),delete l.compress,delete l.interlace,delete l.filter,l},toRGBA8:function toRGBA8(e){const t=e.width,r=e.height;if(null==e.tabs.acTL)return [decodeImage(e.data,t,r,e).buffer];const i=[];null==e.frames[0].data&&(e.frames[0].data=e.data);const o=t*r*4,a=new Uint8Array(o),s=new Uint8Array(o),f=new Uint8Array(o);for(let c=0;c<e.frames.length;c++){const u=e.frames[c],h=u.rect.x,d=u.rect.y,A=u.rect.width,g=u.rect.height,p=decodeImage(u.data,A,g,e);if(0!=c)for(var l=0;l<o;l++)f[l]=a[l];if(0==u.blend?_copyTile(p,A,g,a,t,r,h,d,0):1==u.blend&&_copyTile(p,A,g,a,t,r,h,d,1),i.push(a.buffer.slice(0)),0==u.dispose);else if(1==u.dispose)_copyTile(s,A,g,a,t,r,h,d,0);else if(2==u.dispose)for(l=0;l<o;l++)a[l]=f[l];}return i},_paeth:_paeth,_copyTile:_copyTile,_bin:e}}();!function(){const{_copyTile:e}=UPNG,{_bin:t}=UPNG,r=UPNG._paeth;var i={table:function(){const e=new Uint32Array(256);for(let t=0;t<256;t++){let r=t;for(let e=0;e<8;e++)1&r?r=3988292384^r>>>1:r>>>=1;e[t]=r;}return e}(),update(e,t,r,o){for(let a=0;a<o;a++)e=i.table[255&(e^t[r+a])]^e>>>8;return e},crc:(e,t,r)=>4294967295^i.update(4294967295,e,t,r)};function addErr(e,t,r,i){t[r]+=e[0]*i>>4,t[r+1]+=e[1]*i>>4,t[r+2]+=e[2]*i>>4,t[r+3]+=e[3]*i>>4;}function N(e){return Math.max(0,Math.min(255,e))}function D(e,t){const r=e[0]-t[0],i=e[1]-t[1],o=e[2]-t[2],a=e[3]-t[3];return r*r+i*i+o*o+a*a}function dither(e,t,r,i,o,a,s){null==s&&(s=1);const f=i.length,l=[];for(var c=0;c<f;c++){const e=i[c];l.push([e>>>0&255,e>>>8&255,e>>>16&255,e>>>24&255]);}for(c=0;c<f;c++){let e=4294967295;for(var u=0,h=0;h<f;h++){var d=D(l[c],l[h]);h!=c&&d<e&&(e=d,u=h);}}const A=new Uint32Array(o.buffer),g=new Int16Array(t*r*4),p=[0,8,2,10,12,4,14,6,3,11,1,9,15,7,13,5];for(c=0;c<p.length;c++)p[c]=255*((p[c]+.5)/16-.5);for(let o=0;o<r;o++)for(let w=0;w<t;w++){var m;c=4*(o*t+w);if(2!=s)m=[N(e[c]+g[c]),N(e[c+1]+g[c+1]),N(e[c+2]+g[c+2]),N(e[c+3]+g[c+3])];else {d=p[4*(3&o)+(3&w)];m=[N(e[c]+d),N(e[c+1]+d),N(e[c+2]+d),N(e[c+3]+d)];}u=0;let v=16777215;for(h=0;h<f;h++){const e=D(m,l[h]);e<v&&(v=e,u=h);}const b=l[u],y=[m[0]-b[0],m[1]-b[1],m[2]-b[2],m[3]-b[3]];1==s&&(w!=t-1&&addErr(y,g,c+4,7),o!=r-1&&(0!=w&&addErr(y,g,c+4*t-4,3),addErr(y,g,c+4*t,5),w!=t-1&&addErr(y,g,c+4*t+4,1))),a[c>>2]=u,A[c>>2]=i[u];}}function _main(e,r,o,a,s){null==s&&(s={});const{crc:f}=i,l=t.writeUint,c=t.writeUshort,u=t.writeASCII;let h=8;const d=e.frames.length>1;let A,g=false,p=33+(d?20:0);if(null!=s.sRGB&&(p+=13),null!=s.pHYs&&(p+=21),null!=s.iCCP&&(A=pako.deflate(s.iCCP),p+=21+A.length+4),3==e.ctype){for(var m=e.plte.length,w=0;w<m;w++)e.plte[w]>>>24!=255&&(g=true);p+=8+3*m+4+(g?8+1*m+4:0);}for(var v=0;v<e.frames.length;v++){d&&(p+=38),p+=(F=e.frames[v]).cimg.length+12,0!=v&&(p+=4);}p+=12;const b=new Uint8Array(p),y=[137,80,78,71,13,10,26,10];for(w=0;w<8;w++)b[w]=y[w];if(l(b,h,13),h+=4,u(b,h,"IHDR"),h+=4,l(b,h,r),h+=4,l(b,h,o),h+=4,b[h]=e.depth,h++,b[h]=e.ctype,h++,b[h]=0,h++,b[h]=0,h++,b[h]=0,h++,l(b,h,f(b,h-17,17)),h+=4,null!=s.sRGB&&(l(b,h,1),h+=4,u(b,h,"sRGB"),h+=4,b[h]=s.sRGB,h++,l(b,h,f(b,h-5,5)),h+=4),null!=s.iCCP){const e=13+A.length;l(b,h,e),h+=4,u(b,h,"iCCP"),h+=4,u(b,h,"ICC profile"),h+=11,h+=2,b.set(A,h),h+=A.length,l(b,h,f(b,h-(e+4),e+4)),h+=4;}if(null!=s.pHYs&&(l(b,h,9),h+=4,u(b,h,"pHYs"),h+=4,l(b,h,s.pHYs[0]),h+=4,l(b,h,s.pHYs[1]),h+=4,b[h]=s.pHYs[2],h++,l(b,h,f(b,h-13,13)),h+=4),d&&(l(b,h,8),h+=4,u(b,h,"acTL"),h+=4,l(b,h,e.frames.length),h+=4,l(b,h,null!=s.loop?s.loop:0),h+=4,l(b,h,f(b,h-12,12)),h+=4),3==e.ctype){l(b,h,3*(m=e.plte.length)),h+=4,u(b,h,"PLTE"),h+=4;for(w=0;w<m;w++){const t=3*w,r=e.plte[w],i=255&r,o=r>>>8&255,a=r>>>16&255;b[h+t+0]=i,b[h+t+1]=o,b[h+t+2]=a;}if(h+=3*m,l(b,h,f(b,h-3*m-4,3*m+4)),h+=4,g){l(b,h,m),h+=4,u(b,h,"tRNS"),h+=4;for(w=0;w<m;w++)b[h+w]=e.plte[w]>>>24&255;h+=m,l(b,h,f(b,h-m-4,m+4)),h+=4;}}let E=0;for(v=0;v<e.frames.length;v++){var F=e.frames[v];d&&(l(b,h,26),h+=4,u(b,h,"fcTL"),h+=4,l(b,h,E++),h+=4,l(b,h,F.rect.width),h+=4,l(b,h,F.rect.height),h+=4,l(b,h,F.rect.x),h+=4,l(b,h,F.rect.y),h+=4,c(b,h,a[v]),h+=2,c(b,h,1e3),h+=2,b[h]=F.dispose,h++,b[h]=F.blend,h++,l(b,h,f(b,h-30,30)),h+=4);const t=F.cimg;l(b,h,(m=t.length)+(0==v?0:4)),h+=4;const r=h;u(b,h,0==v?"IDAT":"fdAT"),h+=4,0!=v&&(l(b,h,E++),h+=4),b.set(t,h),h+=m,l(b,h,f(b,r,h-r)),h+=4;}return l(b,h,0),h+=4,u(b,h,"IEND"),h+=4,l(b,h,f(b,h-4,4)),h+=4,b.buffer}function compressPNG(e,t,r){for(let i=0;i<e.frames.length;i++){const o=e.frames[i];const a=o.rect.height,s=new Uint8Array(a*o.bpl+a);o.cimg=_filterZero(o.img,a,o.bpp,o.bpl,s,t,r);}}function compress(t,r,i,o,a){const s=a[0],f=a[1],l=a[2],c=a[3],u=a[4],h=a[5];let d=6,A=8,g=255;for(var p=0;p<t.length;p++){const e=new Uint8Array(t[p]);for(var m=e.length,w=0;w<m;w+=4)g&=e[w+3];}const v=255!=g,b=function framize(t,r,i,o,a,s){const f=[];for(var l=0;l<t.length;l++){const h=new Uint8Array(t[l]),A=new Uint32Array(h.buffer);var c;let g=0,p=0,m=r,w=i,v=o?1:0;if(0!=l){const b=s||o||1==l||0!=f[l-2].dispose?1:2;let y=0,E=1e9;for(let e=0;e<b;e++){var u=new Uint8Array(t[l-1-e]);const o=new Uint32Array(t[l-1-e]);let s=r,f=i,c=-1,h=-1;for(let e=0;e<i;e++)for(let t=0;t<r;t++){A[d=e*r+t]!=o[d]&&(t<s&&(s=t),t>c&&(c=t),e<f&&(f=e),e>h&&(h=e));} -1==c&&(s=f=c=h=0),a&&(1==(1&s)&&s--,1==(1&f)&&f--);const v=(c-s+1)*(h-f+1);v<E&&(E=v,y=e,g=s,p=f,m=c-s+1,w=h-f+1);}u=new Uint8Array(t[l-1-y]);1==y&&(f[l-1].dispose=2),c=new Uint8Array(m*w*4),e(u,r,i,c,m,w,-g,-p,0),v=e(h,r,i,c,m,w,-g,-p,3)?1:0,1==v?_prepareDiff(h,r,i,c,{x:g,y:p,width:m,height:w}):e(h,r,i,c,m,w,-g,-p,0);}else c=h.slice(0);f.push({rect:{x:g,y:p,width:m,height:w},img:c,blend:v,dispose:0});}if(o)for(l=0;l<f.length;l++){if(1==(A=f[l]).blend)continue;const e=A.rect,o=f[l-1].rect,s=Math.min(e.x,o.x),c=Math.min(e.y,o.y),u={x:s,y:c,width:Math.max(e.x+e.width,o.x+o.width)-s,height:Math.max(e.y+e.height,o.y+o.height)-c};f[l-1].dispose=1,l-1!=0&&_updateFrame(t,r,i,f,l-1,u,a),_updateFrame(t,r,i,f,l,u,a);}let h=0;if(1!=t.length)for(var d=0;d<f.length;d++){var A;h+=(A=f[d]).rect.width*A.rect.height;}return f}(t,r,i,s,f,l),y={},E=[],F=[];if(0!=o){const e=[];for(w=0;w<b.length;w++)e.push(b[w].img.buffer);const t=function concatRGBA(e){let t=0;for(var r=0;r<e.length;r++)t+=e[r].byteLength;const i=new Uint8Array(t);let o=0;for(r=0;r<e.length;r++){const t=new Uint8Array(e[r]),a=t.length;for(let e=0;e<a;e+=4){let r=t[e],a=t[e+1],s=t[e+2];const f=t[e+3];0==f&&(r=a=s=0),i[o+e]=r,i[o+e+1]=a,i[o+e+2]=s,i[o+e+3]=f;}o+=a;}return i.buffer}(e),r=quantize(t,o);for(w=0;w<r.plte.length;w++)E.push(r.plte[w].est.rgba);let i=0;for(w=0;w<b.length;w++){const e=(B=b[w]).img.length;var _=new Uint8Array(r.inds.buffer,i>>2,e>>2);F.push(_);const t=new Uint8Array(r.abuf,i,e);h&&dither(B.img,B.rect.width,B.rect.height,E,t,_),B.img.set(t),i+=e;}}else for(p=0;p<b.length;p++){var B=b[p];const e=new Uint32Array(B.img.buffer);var U=B.rect.width;m=e.length,_=new Uint8Array(m);F.push(_);for(w=0;w<m;w++){const t=e[w];if(0!=w&&t==e[w-1])_[w]=_[w-1];else if(w>U&&t==e[w-U])_[w]=_[w-U];else {let e=y[t];if(null==e&&(y[t]=e=E.length,E.push(t),E.length>=300))break;_[w]=e;}}}const C=E.length;C<=256&&0==u&&(A=C<=2?1:C<=4?2:C<=16?4:8,A=Math.max(A,c));for(p=0;p<b.length;p++){(B=b[p]).rect.x;U=B.rect.width;const e=B.rect.height;let t=B.img;let r=4*U,i=4;if(C<=256&&0==u){r=Math.ceil(A*U/8);var I=new Uint8Array(r*e);const o=F[p];for(let t=0;t<e;t++){w=t*r;const e=t*U;if(8==A)for(var Q=0;Q<U;Q++)I[w+Q]=o[e+Q];else if(4==A)for(Q=0;Q<U;Q++)I[w+(Q>>1)]|=o[e+Q]<<4-4*(1&Q);else if(2==A)for(Q=0;Q<U;Q++)I[w+(Q>>2)]|=o[e+Q]<<6-2*(3&Q);else if(1==A)for(Q=0;Q<U;Q++)I[w+(Q>>3)]|=o[e+Q]<<7-1*(7&Q);}t=I,d=3,i=1;}else if(0==v&&1==b.length){I=new Uint8Array(U*e*3);const o=U*e;for(w=0;w<o;w++){const e=3*w,r=4*w;I[e]=t[r],I[e+1]=t[r+1],I[e+2]=t[r+2];}t=I,d=2,i=3,r=3*U;}B.img=t,B.bpl=r,B.bpp=i;}return {ctype:d,depth:A,plte:E,frames:b}}function _updateFrame(t,r,i,o,a,s,f){const l=Uint8Array,c=Uint32Array,u=new l(t[a-1]),h=new c(t[a-1]),d=a+1<t.length?new l(t[a+1]):null,A=new l(t[a]),g=new c(A.buffer);let p=r,m=i,w=-1,v=-1;for(let e=0;e<s.height;e++)for(let t=0;t<s.width;t++){const i=s.x+t,f=s.y+e,l=f*r+i,c=g[l];0==c||0==o[a-1].dispose&&h[l]==c&&(null==d||0!=d[4*l+3])||(i<p&&(p=i),i>w&&(w=i),f<m&&(m=f),f>v&&(v=f));} -1==w&&(p=m=w=v=0),f&&(1==(1&p)&&p--,1==(1&m)&&m--),s={x:p,y:m,width:w-p+1,height:v-m+1};const b=o[a];b.rect=s,b.blend=1,b.img=new Uint8Array(s.width*s.height*4),0==o[a-1].dispose?(e(u,r,i,b.img,s.width,s.height,-s.x,-s.y,0),_prepareDiff(A,r,i,b.img,s)):e(A,r,i,b.img,s.width,s.height,-s.x,-s.y,0);}function _prepareDiff(t,r,i,o,a){e(t,r,i,o,a.width,a.height,-a.x,-a.y,2);}function _filterZero(e,t,r,i,o,a,s){const f=[];let l,c=[0,1,2,3,4];-1!=a?c=[a]:(t*i>5e5||1==r)&&(c=[0]),s&&(l={level:0});const u=UZIP;for(var h=0;h<c.length;h++){for(let a=0;a<t;a++)_filterLine(o,e,a,i,r,c[h]);f.push(u.deflate(o,l));}let d,A=1e9;for(h=0;h<f.length;h++)f[h].length<A&&(d=h,A=f[h].length);return f[d]}function _filterLine(e,t,i,o,a,s){const f=i*o;let l=f+i;if(e[l]=s,l++,0==s)if(o<500)for(var c=0;c<o;c++)e[l+c]=t[f+c];else e.set(new Uint8Array(t.buffer,f,o),l);else if(1==s){for(c=0;c<a;c++)e[l+c]=t[f+c];for(c=a;c<o;c++)e[l+c]=t[f+c]-t[f+c-a]+256&255;}else if(0==i){for(c=0;c<a;c++)e[l+c]=t[f+c];if(2==s)for(c=a;c<o;c++)e[l+c]=t[f+c];if(3==s)for(c=a;c<o;c++)e[l+c]=t[f+c]-(t[f+c-a]>>1)+256&255;if(4==s)for(c=a;c<o;c++)e[l+c]=t[f+c]-r(t[f+c-a],0,0)+256&255;}else {if(2==s)for(c=0;c<o;c++)e[l+c]=t[f+c]+256-t[f+c-o]&255;if(3==s){for(c=0;c<a;c++)e[l+c]=t[f+c]+256-(t[f+c-o]>>1)&255;for(c=a;c<o;c++)e[l+c]=t[f+c]+256-(t[f+c-o]+t[f+c-a]>>1)&255;}if(4==s){for(c=0;c<a;c++)e[l+c]=t[f+c]+256-r(0,t[f+c-o],0)&255;for(c=a;c<o;c++)e[l+c]=t[f+c]+256-r(t[f+c-a],t[f+c-o],t[f+c-a-o])&255;}}}function quantize(e,t){const r=new Uint8Array(e),i=r.slice(0),o=new Uint32Array(i.buffer),a=getKDtree(i,t),s=a[0],f=a[1],l=r.length,c=new Uint8Array(l>>2);let u;if(r.length<2e7)for(var h=0;h<l;h+=4){u=getNearest(s,d=r[h]*(1/255),A=r[h+1]*(1/255),g=r[h+2]*(1/255),p=r[h+3]*(1/255)),c[h>>2]=u.ind,o[h>>2]=u.est.rgba;}else for(h=0;h<l;h+=4){var d=r[h]*(1/255),A=r[h+1]*(1/255),g=r[h+2]*(1/255),p=r[h+3]*(1/255);for(u=s;u.left;)u=planeDst(u.est,d,A,g,p)<=0?u.left:u.right;c[h>>2]=u.ind,o[h>>2]=u.est.rgba;}return {abuf:i.buffer,inds:c,plte:f}}function getKDtree(e,t,r){null==r&&(r=1e-4);const i=new Uint32Array(e.buffer),o={i0:0,i1:e.length,bst:null,est:null,tdst:0,left:null,right:null};o.bst=stats(e,o.i0,o.i1),o.est=estats(o.bst);const a=[o];for(;a.length<t;){let t=0,o=0;for(var s=0;s<a.length;s++)a[s].est.L>t&&(t=a[s].est.L,o=s);if(t<r)break;const f=a[o],l=splitPixels(e,i,f.i0,f.i1,f.est.e,f.est.eMq255);if(f.i0>=l||f.i1<=l){f.est.L=0;continue}const c={i0:f.i0,i1:l,bst:null,est:null,tdst:0,left:null,right:null};c.bst=stats(e,c.i0,c.i1),c.est=estats(c.bst);const u={i0:l,i1:f.i1,bst:null,est:null,tdst:0,left:null,right:null};u.bst={R:[],m:[],N:f.bst.N-c.bst.N};for(s=0;s<16;s++)u.bst.R[s]=f.bst.R[s]-c.bst.R[s];for(s=0;s<4;s++)u.bst.m[s]=f.bst.m[s]-c.bst.m[s];u.est=estats(u.bst),f.left=c,f.right=u,a[o]=c,a.push(u);}a.sort(((e,t)=>t.bst.N-e.bst.N));for(s=0;s<a.length;s++)a[s].ind=s;return [o,a]}function getNearest(e,t,r,i,o){if(null==e.left)return e.tdst=function dist(e,t,r,i,o){const a=t-e[0],s=r-e[1],f=i-e[2],l=o-e[3];return a*a+s*s+f*f+l*l}(e.est.q,t,r,i,o),e;const a=planeDst(e.est,t,r,i,o);let s=e.left,f=e.right;a>0&&(s=e.right,f=e.left);const l=getNearest(s,t,r,i,o);if(l.tdst<=a*a)return l;const c=getNearest(f,t,r,i,o);return c.tdst<l.tdst?c:l}function planeDst(e,t,r,i,o){const{e:a}=e;return a[0]*t+a[1]*r+a[2]*i+a[3]*o-e.eMq}function splitPixels(e,t,r,i,o,a){for(i-=4;r<i;){for(;vecDot(e,r,o)<=a;)r+=4;for(;vecDot(e,i,o)>a;)i-=4;if(r>=i)break;const s=t[r>>2];t[r>>2]=t[i>>2],t[i>>2]=s,r+=4,i-=4;}for(;vecDot(e,r,o)>a;)r-=4;return r+4}function vecDot(e,t,r){return e[t]*r[0]+e[t+1]*r[1]+e[t+2]*r[2]+e[t+3]*r[3]}function stats(e,t,r){const i=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],o=[0,0,0,0],a=r-t>>2;for(let a=t;a<r;a+=4){const t=e[a]*(1/255),r=e[a+1]*(1/255),s=e[a+2]*(1/255),f=e[a+3]*(1/255);o[0]+=t,o[1]+=r,o[2]+=s,o[3]+=f,i[0]+=t*t,i[1]+=t*r,i[2]+=t*s,i[3]+=t*f,i[5]+=r*r,i[6]+=r*s,i[7]+=r*f,i[10]+=s*s,i[11]+=s*f,i[15]+=f*f;}return i[4]=i[1],i[8]=i[2],i[9]=i[6],i[12]=i[3],i[13]=i[7],i[14]=i[11],{R:i,m:o,N:a}}function estats(e){const{R:t}=e,{m:r}=e,{N:i}=e,a=r[0],s=r[1],f=r[2],l=r[3],c=0==i?0:1/i,u=[t[0]-a*a*c,t[1]-a*s*c,t[2]-a*f*c,t[3]-a*l*c,t[4]-s*a*c,t[5]-s*s*c,t[6]-s*f*c,t[7]-s*l*c,t[8]-f*a*c,t[9]-f*s*c,t[10]-f*f*c,t[11]-f*l*c,t[12]-l*a*c,t[13]-l*s*c,t[14]-l*f*c,t[15]-l*l*c],h=u,d=o;let A=[Math.random(),Math.random(),Math.random(),Math.random()],g=0,p=0;if(0!=i)for(let e=0;e<16&&(A=d.multVec(h,A),p=Math.sqrt(d.dot(A,A)),A=d.sml(1/p,A),!(0!=e&&Math.abs(p-g)<1e-9));e++)g=p;const m=[a*c,s*c,f*c,l*c];return {Cov:u,q:m,e:A,L:g,eMq255:d.dot(d.sml(255,m),A),eMq:d.dot(A,m),rgba:(Math.round(255*m[3])<<24|Math.round(255*m[2])<<16|Math.round(255*m[1])<<8|Math.round(255*m[0])<<0)>>>0}}var o={multVec:(e,t)=>[e[0]*t[0]+e[1]*t[1]+e[2]*t[2]+e[3]*t[3],e[4]*t[0]+e[5]*t[1]+e[6]*t[2]+e[7]*t[3],e[8]*t[0]+e[9]*t[1]+e[10]*t[2]+e[11]*t[3],e[12]*t[0]+e[13]*t[1]+e[14]*t[2]+e[15]*t[3]],dot:(e,t)=>e[0]*t[0]+e[1]*t[1]+e[2]*t[2]+e[3]*t[3],sml:(e,t)=>[e*t[0],e*t[1],e*t[2],e*t[3]]};UPNG.encode=function encode(e,t,r,i,o,a,s){null==i&&(i=0),null==s&&(s=false);const f=compress(e,t,r,i,[false,false,false,0,s,false]);return compressPNG(f,-1),_main(f,t,r,o,a)},UPNG.encodeLL=function encodeLL(e,t,r,i,o,a,s,f){const l={ctype:0+(1==i?0:2)+(0==o?0:4),depth:a,frames:[]},c=(i+o)*a,u=c*t;for(let i=0;i<e.length;i++)l.frames.push({rect:{x:0,y:0,width:t,height:r},img:new Uint8Array(e[i]),blend:0,dispose:1,bpp:Math.ceil(c/8),bpl:Math.ceil(u/8)});return compressPNG(l,0,true),_main(l,t,r,s,f)},UPNG.encode.compress=compress,UPNG.encode.dither=dither,UPNG.quantize=quantize,UPNG.quantize.getKDtree=getKDtree,UPNG.quantize.getNearest=getNearest;}();const r={toArrayBuffer(e,t){const i=e.width,o=e.height,a=i<<2,s=e.getContext("2d").getImageData(0,0,i,o),f=new Uint32Array(s.data.buffer),l=(32*i+31)/32<<2,c=l*o,u=122+c,h=new ArrayBuffer(u),d=new DataView(h),A=1<<20;let g,p,m,w,v=A,b=0,y=0,E=0;function set16(e){d.setUint16(y,e,true),y+=2;}function set32(e){d.setUint32(y,e,true),y+=4;}function seek(e){y+=e;}set16(19778),set32(u),seek(4),set32(122),set32(108),set32(i),set32(-o>>>0),set16(1),set16(32),set32(3),set32(c),set32(2835),set32(2835),seek(8),set32(16711680),set32(65280),set32(255),set32(4278190080),set32(1466527264),function convert(){for(;b<o&&v>0;){for(w=122+b*l,g=0;g<a;)v--,p=f[E++],m=p>>>24,d.setUint32(w+g,p<<8|m),g+=4;b++;}E<f.length?(v=A,setTimeout(convert,r._dly)):t(h);}();},toBlob(e,t){this.toArrayBuffer(e,(e=>{t(new Blob([e],{type:"image/bmp"}));}));},_dly:9};var i={CHROME:"CHROME",FIREFOX:"FIREFOX",DESKTOP_SAFARI:"DESKTOP_SAFARI",IE:"IE",IOS:"IOS",ETC:"ETC"},o={[i.CHROME]:16384,[i.FIREFOX]:11180,[i.DESKTOP_SAFARI]:16384,[i.IE]:8192,[i.IOS]:4096,[i.ETC]:8192};const a="undefined"!=typeof window,s="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope,f=a&&window.cordova&&window.cordova.require&&window.cordova.require("cordova/modulemapper"),CustomFile=(a||s)&&(f&&f.getOriginalSymbol(window,"File")||"undefined"!=typeof File&&File),CustomFileReader=(a||s)&&(f&&f.getOriginalSymbol(window,"FileReader")||"undefined"!=typeof FileReader&&FileReader);function getFilefromDataUrl(e,t,r=Date.now()){return new Promise((i=>{const o=e.split(","),a=o[0].match(/:(.*?);/)[1],s=globalThis.atob(o[1]);let f=s.length;const l=new Uint8Array(f);for(;f--;)l[f]=s.charCodeAt(f);const c=new Blob([l],{type:a});c.name=t,c.lastModified=r,i(c);}))}function getDataUrlFromFile(e){return new Promise(((t,r)=>{const i=new CustomFileReader;i.onload=()=>t(i.result),i.onerror=e=>r(e),i.readAsDataURL(e);}))}function loadImage(e){return new Promise(((t,r)=>{const i=new Image;i.onload=()=>t(i),i.onerror=e=>r(e),i.src=e;}))}function getBrowserName(){if(void 0!==getBrowserName.cachedResult)return getBrowserName.cachedResult;let e=i.ETC;const{userAgent:t}=navigator;return /Chrom(e|ium)/i.test(t)?e=i.CHROME:/iP(ad|od|hone)/i.test(t)&&/WebKit/i.test(t)?e=i.IOS:/Safari/i.test(t)?e=i.DESKTOP_SAFARI:/Firefox/i.test(t)?e=i.FIREFOX:(/MSIE/i.test(t)||true==!!document.documentMode)&&(e=i.IE),getBrowserName.cachedResult=e,getBrowserName.cachedResult}function approximateBelowMaximumCanvasSizeOfBrowser(e,t){const r=getBrowserName(),i=o[r];let a=e,s=t,f=a*s;const l=a>s?s/a:a/s;for(;f>i*i;){const e=(i+a)/2,t=(i+s)/2;e<t?(s=t,a=t*l):(s=e*l,a=e),f=a*s;}return {width:a,height:s}}function getNewCanvasAndCtx(e,t){let r,i;try{if(r=new OffscreenCanvas(e,t),i=r.getContext("2d"),null===i)throw new Error("getContext of OffscreenCanvas returns null")}catch(e){r=document.createElement("canvas"),i=r.getContext("2d");}return r.width=e,r.height=t,[r,i]}function drawImageInCanvas(e,t){const{width:r,height:i}=approximateBelowMaximumCanvasSizeOfBrowser(e.width,e.height),[o,a]=getNewCanvasAndCtx(r,i);return t&&/jpe?g/.test(t)&&(a.fillStyle="white",a.fillRect(0,0,o.width,o.height)),a.drawImage(e,0,0,o.width,o.height),o}function isIOS(){return void 0!==isIOS.cachedResult||(isIOS.cachedResult=["iPad Simulator","iPhone Simulator","iPod Simulator","iPad","iPhone","iPod"].includes(navigator.platform)||navigator.userAgent.includes("Mac")&&"undefined"!=typeof document&&"ontouchend"in document),isIOS.cachedResult}function drawFileInCanvas(e,t={}){return new Promise((function(r,o){let a,s;var $Try_2_Post=function(){try{return s=drawImageInCanvas(a,t.fileType||e.type),r([a,s])}catch(e){return o(e)}},$Try_2_Catch=function(t){try{var $Try_3_Catch=function(e){try{throw e}catch(e){return o(e)}};try{let t;return getDataUrlFromFile(e).then((function(e){try{return t=e,loadImage(t).then((function(e){try{return a=e,function(){try{return $Try_2_Post()}catch(e){return o(e)}}()}catch(e){return $Try_3_Catch(e)}}),$Try_3_Catch)}catch(e){return $Try_3_Catch(e)}}),$Try_3_Catch)}catch(e){$Try_3_Catch(e);}}catch(e){return o(e)}};try{if(isIOS()||[i.DESKTOP_SAFARI,i.MOBILE_SAFARI].includes(getBrowserName()))throw new Error("Skip createImageBitmap on IOS and Safari");return createImageBitmap(e).then((function(e){try{return a=e,$Try_2_Post()}catch(e){return $Try_2_Catch()}}),$Try_2_Catch)}catch(e){$Try_2_Catch();}}))}function canvasToFile(e,t,i,o,a=1){return new Promise((function(s,f){let l;if("image/png"===t){let c,u,h;return c=e.getContext("2d"),({data:u}=c.getImageData(0,0,e.width,e.height)),h=UPNG.encode([u.buffer],e.width,e.height,4096*a),l=new Blob([h],{type:t}),l.name=i,l.lastModified=o,$If_4.call(this)}{if("image/bmp"===t)return new Promise((t=>r.toBlob(e,t))).then(function(e){try{return l=e,l.name=i,l.lastModified=o,$If_5.call(this)}catch(e){return f(e)}}.bind(this),f);{if("function"==typeof OffscreenCanvas&&e instanceof OffscreenCanvas)return e.convertToBlob({type:t,quality:a}).then(function(e){try{return l=e,l.name=i,l.lastModified=o,$If_6.call(this)}catch(e){return f(e)}}.bind(this),f);{let d;return d=e.toDataURL(t,a),getFilefromDataUrl(d,i,o).then(function(e){try{return l=e,$If_6.call(this)}catch(e){return f(e)}}.bind(this),f)}function $If_6(){return $If_5.call(this)}}function $If_5(){return $If_4.call(this)}}function $If_4(){return s(l)}}))}function cleanupCanvasMemory(e){e.width=0,e.height=0;}function isAutoOrientationInBrowser(){return new Promise((function(e,t){let i,o,a,s;return void 0!==isAutoOrientationInBrowser.cachedResult?e(isAutoOrientationInBrowser.cachedResult):(getFilefromDataUrl("data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAAAAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/xABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q==","test.jpg",Date.now()).then((function(r){try{return i=r,drawFileInCanvas(i).then((function(r){try{return o=r[1],canvasToFile(o,i.type,i.name,i.lastModified).then((function(r){try{return a=r,cleanupCanvasMemory(o),drawFileInCanvas(a).then((function(r){try{return s=r[0],isAutoOrientationInBrowser.cachedResult=1===s.width&&2===s.height,e(isAutoOrientationInBrowser.cachedResult)}catch(e){return t(e)}}),t)}catch(e){return t(e)}}),t)}catch(e){return t(e)}}),t)}catch(e){return t(e)}}),t))}))}function getExifOrientation(e){return new Promise(((t,r)=>{const i=new CustomFileReader;i.onload=e=>{const r=new DataView(e.target.result);if(65496!=r.getUint16(0,false))return t(-2);const i=r.byteLength;let o=2;for(;o<i;){if(r.getUint16(o+2,false)<=8)return t(-1);const e=r.getUint16(o,false);if(o+=2,65505==e){if(1165519206!=r.getUint32(o+=2,false))return t(-1);const e=18761==r.getUint16(o+=6,false);o+=r.getUint32(o+4,e);const i=r.getUint16(o,e);o+=2;for(let a=0;a<i;a++)if(274==r.getUint16(o+12*a,e))return t(r.getUint16(o+12*a+8,e))}else {if(65280!=(65280&e))break;o+=r.getUint16(o,false);}}return t(-1)},i.onerror=e=>r(e),i.readAsArrayBuffer(e);}))}function handleMaxWidthOrHeight(e,t){const{width:r}=e,{height:i}=e,{maxWidthOrHeight:o}=t;let a,s=e;return isFinite(o)&&(r>o||i>o)&&([s,a]=getNewCanvasAndCtx(r,i),r>i?(s.width=o,s.height=i/r*o):(s.width=r/i*o,s.height=o),a.drawImage(e,0,0,s.width,s.height),cleanupCanvasMemory(e)),s}function followExifOrientation(e,t){const{width:r}=e,{height:i}=e,[o,a]=getNewCanvasAndCtx(r,i);switch(t>4&&t<9?(o.width=i,o.height=r):(o.width=r,o.height=i),t){case 2:a.transform(-1,0,0,1,r,0);break;case 3:a.transform(-1,0,0,-1,r,i);break;case 4:a.transform(1,0,0,-1,0,i);break;case 5:a.transform(0,1,1,0,0,0);break;case 6:a.transform(0,1,-1,0,i,0);break;case 7:a.transform(0,-1,-1,0,i,r);break;case 8:a.transform(0,-1,1,0,0,r);}return a.drawImage(e,0,0,r,i),cleanupCanvasMemory(e),o}function compress(e,t,r=0){return new Promise((function(i,o){let a,s,f,l,c,u,h,d,A,g,p,m,w,v,b,y,E,F,_,B;function incProgress(e=5){if(t.signal&&t.signal.aborted)throw t.signal.reason;a+=e,t.onProgress(Math.min(a,100));}function setProgress(e){if(t.signal&&t.signal.aborted)throw t.signal.reason;a=Math.min(Math.max(e,a),100),t.onProgress(a);}return a=r,s=t.maxIteration||10,f=1024*t.maxSizeMB*1024,incProgress(),drawFileInCanvas(e,t).then(function(r){try{return [,l]=r,incProgress(),c=handleMaxWidthOrHeight(l,t),incProgress(),new Promise((function(r,i){var o;if(!(o=t.exifOrientation))return getExifOrientation(e).then(function(e){try{return o=e,$If_2.call(this)}catch(e){return i(e)}}.bind(this),i);function $If_2(){return r(o)}return $If_2.call(this)})).then(function(r){try{return u=r,incProgress(),isAutoOrientationInBrowser().then(function(r){try{return h=r?c:followExifOrientation(c,u),incProgress(),d=t.initialQuality||1,A=t.fileType||e.type,canvasToFile(h,A,e.name,e.lastModified,d).then(function(r){try{{if(g=r,incProgress(),p=g.size>f,m=g.size>e.size,!p&&!m)return setProgress(100),i(g);var a;function $Loop_3(){if(s--&&(b>f||b>w)){let t,r;return t=B?.95*_.width:_.width,r=B?.95*_.height:_.height,[E,F]=getNewCanvasAndCtx(t,r),F.drawImage(_,0,0,t,r),d*="image/png"===A?.85:.95,canvasToFile(E,A,e.name,e.lastModified,d).then((function(e){try{return y=e,cleanupCanvasMemory(_),_=E,b=y.size,setProgress(Math.min(99,Math.floor((v-b)/(v-f)*100))),$Loop_3}catch(e){return o(e)}}),o)}return [1]}return w=e.size,v=g.size,b=v,_=h,B=!t.alwaysKeepResolution&&p,(a=function(e){for(;e;){if(e.then)return void e.then(a,o);try{if(e.pop){if(e.length)return e.pop()?$Loop_3_exit.call(this):e;e=$Loop_3;}else e=e.call(this);}catch(e){return o(e)}}}.bind(this))($Loop_3);function $Loop_3_exit(){return cleanupCanvasMemory(_),cleanupCanvasMemory(E),cleanupCanvasMemory(c),cleanupCanvasMemory(h),cleanupCanvasMemory(l),setProgress(100),i(y)}}}catch(u){return o(u)}}.bind(this),o)}catch(e){return o(e)}}.bind(this),o)}catch(e){return o(e)}}.bind(this),o)}catch(e){return o(e)}}.bind(this),o)}))}const l="\nlet scriptImported = false\nself.addEventListener('message', async (e) => {\n const { file, id, imageCompressionLibUrl, options } = e.data\n options.onProgress = (progress) => self.postMessage({ progress, id })\n try {\n if (!scriptImported) {\n // console.log('[worker] importScripts', imageCompressionLibUrl)\n self.importScripts(imageCompressionLibUrl)\n scriptImported = true\n }\n // console.log('[worker] self', self)\n const compressedFile = await imageCompression(file, options)\n self.postMessage({ file: compressedFile, id })\n } catch (e) {\n // console.error('[worker] error', e)\n self.postMessage({ error: e.message + '\\n' + e.stack, id })\n }\n})\n";let c;function compressOnWebWorker(e,t){return new Promise(((r,i)=>{c||(c=function createWorkerScriptURL(e){const t=[];return t.push(e),URL.createObjectURL(new Blob(t))}(l));const o=new Worker(c);o.addEventListener("message",(function handler(e){if(t.signal&&t.signal.aborted)o.terminate();else if(void 0===e.data.progress){if(e.data.error)return i(new Error(e.data.error)),void o.terminate();r(e.data.file),o.terminate();}else t.onProgress(e.data.progress);})),o.addEventListener("error",i),t.signal&&t.signal.addEventListener("abort",(()=>{i(t.signal.reason),o.terminate();})),o.postMessage({file:e,imageCompressionLibUrl:t.libURL,options:{...t,onProgress:void 0,signal:void 0}});}))}function imageCompression(e,t){return new Promise((function(r,i){let o,a,s,f,l,c;if(o={...t},s=0,({onProgress:f}=o),o.maxSizeMB=o.maxSizeMB||Number.POSITIVE_INFINITY,l="boolean"!=typeof o.useWebWorker||o.useWebWorker,delete o.useWebWorker,o.onProgress=e=>{s=e,"function"==typeof f&&f(s);},!(e instanceof Blob||e instanceof CustomFile))return i(new Error("The file given is not an instance of Blob or File"));if(!/^image/.test(e.type))return i(new Error("The file given is not an image"));if(c="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope,!l||"function"!=typeof Worker||c)return compress(e,o).then(function(e){try{return a=e,$If_4.call(this)}catch(e){return i(e)}}.bind(this),i);var u=function(){try{return $If_4.call(this)}catch(e){return i(e)}}.bind(this),$Try_1_Catch=function(t){try{return compress(e,o).then((function(e){try{return a=e,u()}catch(e){return i(e)}}),i)}catch(e){return i(e)}};try{return o.libURL=o.libURL||"https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.2/dist/browser-image-compression.js",compressOnWebWorker(e,o).then((function(e){try{return a=e,u()}catch(e){return $Try_1_Catch()}}),$Try_1_Catch)}catch(e){$Try_1_Catch();}function $If_4(){try{a.name=e.name,a.lastModified=e.lastModified;}catch(e){}try{o.preserveExif&&"image/jpeg"===e.type&&(!o.fileType||o.fileType&&o.fileType===e.type)&&(a=copyExifWithoutOrientation(e,a));}catch(e){}return r(a)}}))}imageCompression.getDataUrlFromFile=getDataUrlFromFile,imageCompression.getFilefromDataUrl=getFilefromDataUrl,imageCompression.loadImage=loadImage,imageCompression.drawImageInCanvas=drawImageInCanvas,imageCompression.drawFileInCanvas=drawFileInCanvas,imageCompression.canvasToFile=canvasToFile,imageCompression.getExifOrientation=getExifOrientation,imageCompression.handleMaxWidthOrHeight=handleMaxWidthOrHeight,imageCompression.followExifOrientation=followExifOrientation,imageCompression.cleanupCanvasMemory=cleanupCanvasMemory,imageCompression.isAutoOrientationInBrowser=isAutoOrientationInBrowser,imageCompression.approximateBelowMaximumCanvasSizeOfBrowser=approximateBelowMaximumCanvasSizeOfBrowser,imageCompression.copyExifWithoutOrientation=copyExifWithoutOrientation,imageCompression.getBrowserName=getBrowserName,imageCompression.version="2.0.2";
282
-
283
- /**
284
- * @file 图像处理工具类
285
- * @description 提供图像预处理功能,用于提高OCR识别率
286
- * @module ImageProcessor
287
- * @version 1.3.2
288
- */
289
- /**
290
- * 图像处理工具类
291
- *
292
- * 提供各种图像处理功能,用于优化识别效果
293
- */
294
- class ImageProcessor {
295
- /**
296
- * 将ImageData转换为Canvas元素
297
- *
298
- * @param {ImageData} imageData - 要转换的图像数据
299
- * @returns {HTMLCanvasElement} 包含图像的Canvas元素
300
- */
301
- static imageDataToCanvas(imageData) {
302
- const canvas = document.createElement("canvas");
303
- canvas.width = imageData.width;
304
- canvas.height = imageData.height;
305
- const ctx = canvas.getContext("2d");
306
- if (ctx) {
307
- ctx.putImageData(imageData, 0, 0);
308
- }
309
- return canvas;
310
- }
311
- /**
312
- * 将Canvas转换为ImageData
313
- *
314
- * @param {HTMLCanvasElement} canvas - 要转换的Canvas元素
315
- * @returns {ImageData|null} Canvas的图像数据,如果获取失败则返回null
316
- */
317
- static canvasToImageData(canvas) {
318
- const ctx = canvas.getContext("2d");
319
- return ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
320
- }
321
- /**
322
- * 调整图像亮度和对比度
323
- *
324
- * @param imageData 原始图像数据
325
- * @param brightness 亮度调整值 (-100到100)
326
- * @param contrast 对比度调整值 (-100到100)
327
- * @returns 处理后的图像数据
328
- */
329
- static adjustBrightnessContrast(imageData, brightness = 0, contrast = 0) {
330
- // 将亮度和对比度范围限制在 -100 到 100 之间
331
- brightness = Math.max(-100, Math.min(100, brightness));
332
- contrast = Math.max(-100, Math.min(100, contrast));
333
- // 将范围转换为适合计算的值
334
- const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
335
- const briAdjust = (brightness / 100) * 255;
336
- const data = imageData.data;
337
- const length = data.length;
338
- for (let i = 0; i < length; i += 4) {
339
- // 分别处理 RGB 三个通道
340
- for (let j = 0; j < 3; j++) {
341
- // 应用亮度和对比度调整公式
342
- const newValue = factor * (data[i + j] + briAdjust - 128) + 128;
343
- data[i + j] = Math.max(0, Math.min(255, newValue));
344
- }
345
- // Alpha 通道保持不变
346
- }
347
- return imageData;
348
- }
349
- /**
350
- * 将图像转换为灰度图
351
- *
352
- * @param imageData 原始图像数据
353
- * @returns 灰度图像数据
354
- */
355
- static toGrayscale(imageData) {
356
- const data = imageData.data;
357
- const length = data.length;
358
- for (let i = 0; i < length; i += 4) {
359
- // 使用加权平均法将 RGB 转换为灰度值
360
- const gray = data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11;
361
- data[i] = data[i + 1] = data[i + 2] = gray;
362
- }
363
- return imageData;
364
- }
365
- /**
366
- * 锐化图像
367
- *
368
- * @param imageData 原始图像数据
369
- * @param amount 锐化程度,默认为2
370
- * @returns 锐化后的图像数据
371
- */
372
- static sharpen(imageData, amount = 2) {
373
- if (!imageData || !imageData.data)
374
- return imageData;
375
- const width = imageData.width;
376
- const height = imageData.height;
377
- const data = imageData.data;
378
- const outputData = new Uint8ClampedArray(data.length);
379
- // 锐化卷积核
380
- const kernel = [
381
- 0,
382
- -amount,
383
- 0,
384
- -amount,
385
- 1 + 4 * amount,
386
- -amount,
387
- 0,
388
- -amount,
389
- 0,
390
- ];
391
- // 应用卷积
392
- for (let y = 1; y < height - 1; y++) {
393
- for (let x = 1; x < width - 1; x++) {
394
- const pos = (y * width + x) * 4;
395
- // 对每个通道应用卷积
396
- for (let c = 0; c < 3; c++) {
397
- let val = 0;
398
- for (let ky = -1; ky <= 1; ky++) {
399
- for (let kx = -1; kx <= 1; kx++) {
400
- const kernelPos = (ky + 1) * 3 + (kx + 1);
401
- const dataPos = ((y + ky) * width + (x + kx)) * 4 + c;
402
- val += data[dataPos] * kernel[kernelPos];
403
- }
404
- }
405
- outputData[pos + c] = Math.max(0, Math.min(255, val));
406
- }
407
- outputData[pos + 3] = data[pos + 3]; // 保持透明度不变
408
- }
409
- }
410
- // 处理边缘像素
411
- for (let y = 0; y < height; y++) {
412
- for (let x = 0; x < width; x++) {
413
- if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
414
- const pos = (y * width + x) * 4;
415
- outputData[pos] = data[pos];
416
- outputData[pos + 1] = data[pos + 1];
417
- outputData[pos + 2] = data[pos + 2];
418
- outputData[pos + 3] = data[pos + 3];
419
- }
420
- }
421
- }
422
- // 创建新的ImageData对象
423
- return new ImageData(outputData, width, height);
424
- }
425
- /**
426
- * 对图像应用阈值操作,增强对比度
427
- *
428
- * @param imageData 原始图像数据
429
- * @param threshold 阈值 (0-255)
430
- * @returns 处理后的图像数据
431
- */
432
- static threshold(imageData, threshold = 128) {
433
- // 先转换为灰度图
434
- const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
435
- const data = grayscaleImage.data;
436
- const length = data.length;
437
- for (let i = 0; i < length; i += 4) {
438
- // 二值化处理
439
- const value = data[i] < threshold ? 0 : 255;
440
- data[i] = data[i + 1] = data[i + 2] = value;
441
- }
442
- return grayscaleImage;
443
- }
444
- /**
445
- * 将图像转换为黑白图像(二值化)
446
- *
447
- * @param imageData 原始图像数据
448
- * @returns 二值化后的图像数据
449
- */
450
- static toBinaryImage(imageData) {
451
- // 先转换为灰度图
452
- const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
453
- // 使用OTSU算法自动确定阈值
454
- const threshold = this.getOtsuThreshold(grayscaleImage);
455
- return this.threshold(grayscaleImage, threshold);
456
- }
457
- /**
458
- * 使用OTSU算法计算最佳阈值
459
- *
460
- * @param imageData 灰度图像数据
461
- * @returns 最佳阈值
462
- */
463
- static getOtsuThreshold(imageData) {
464
- const data = imageData.data;
465
- const histogram = new Array(256).fill(0);
466
- // 统计灰度直方图
467
- for (let i = 0; i < data.length; i += 4) {
468
- histogram[data[i]]++;
469
- }
470
- const total = imageData.width * imageData.height;
471
- let sum = 0;
472
- // 计算总灰度值和
473
- for (let i = 0; i < 256; i++) {
474
- sum += i * histogram[i];
475
- }
476
- let sumB = 0;
477
- let wB = 0;
478
- let wF = 0;
479
- let maxVariance = 0;
480
- let threshold = 0;
481
- // 遍历所有可能的阈值,找到最大类间方差
482
- for (let t = 0; t < 256; t++) {
483
- wB += histogram[t]; // 背景权重
484
- if (wB === 0)
485
- continue;
486
- wF = total - wB; // 前景权重
487
- if (wF === 0)
488
- break;
489
- sumB += t * histogram[t];
490
- const mB = sumB / wB; // 背景平均灰度
491
- const mF = (sum - sumB) / wF; // 前景平均灰度
492
- // 计算类间方差
493
- const variance = wB * wF * (mB - mF) * (mB - mF);
494
- if (variance > maxVariance) {
495
- maxVariance = variance;
496
- threshold = t;
497
- }
498
- }
499
- return threshold;
500
- }
501
- /**
502
- * 批量应用图像处理
503
- *
504
- * @param imageData 原始图像数据
505
- * @param options 处理选项
506
- * @returns 处理后的图像数据
507
- */
508
- static batchProcess(imageData, options) {
509
- let processedImage = new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height);
510
- // 应用亮度和对比度调整
511
- if (options.brightness !== undefined || options.contrast !== undefined) {
512
- processedImage = this.adjustBrightnessContrast(processedImage, options.brightness || 0, options.contrast || 0);
513
- }
514
- // 应用灰度转换
515
- if (options.grayscale) {
516
- processedImage = this.toGrayscale(processedImage);
517
- }
518
- // 应用锐化
519
- if (options.sharpen) {
520
- processedImage = this.sharpen(processedImage);
521
- }
522
- // 应用颜色反转
523
- if (options.invert) {
524
- const data = processedImage.data;
525
- for (let i = 0; i < data.length; i += 4) {
526
- // 反转RGB值
527
- data[i] = 255 - data[i];
528
- data[i + 1] = 255 - data[i + 1];
529
- data[i + 2] = 255 - data[i + 2];
530
- // Alpha通道保持不变
531
- }
532
- }
533
- return processedImage;
534
- }
535
- /**
536
- * 压缩图片文件
537
- *
538
- * @param file 图片文件
539
- * @param options 压缩选项
540
- * @returns Promise<File> 压缩后的文件
541
- */
542
- static async compressImage(file, options) {
543
- const defaultOptions = {
544
- maxSizeMB: 1,
545
- maxWidthOrHeight: 1920,
546
- useWebWorker: true,
547
- quality: 0.8,
548
- fileType: file.type || "image/jpeg",
549
- };
550
- const compressOptions = { ...defaultOptions, ...options };
551
- try {
552
- return await imageCompression(file, compressOptions);
553
- }
554
- catch (error) {
555
- console.error("图片压缩失败:", error);
556
- return file; // 如果压缩失败,返回原始文件
557
- }
558
- }
559
- /**
560
- * 从图片文件创建ImageData
561
- *
562
- * @param file 图片文件
563
- * @returns Promise<ImageData>
564
- */
565
- static async createImageDataFromFile(file) {
566
- return new Promise((resolve, reject) => {
567
- try {
568
- const img = new Image();
569
- const url = URL.createObjectURL(file);
570
- img.onload = () => {
571
- try {
572
- // 创建canvas元素
573
- const canvas = document.createElement("canvas");
574
- const ctx = canvas.getContext("2d");
575
- if (!ctx) {
576
- reject(new Error("无法创建2D上下文"));
577
- return;
578
- }
579
- canvas.width = img.width;
580
- canvas.height = img.height;
581
- // 绘制图片到canvas
582
- ctx.drawImage(img, 0, 0);
583
- // 获取图像数据
584
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
585
- // 释放资源
586
- URL.revokeObjectURL(url);
587
- resolve(imageData);
588
- }
589
- catch (e) {
590
- reject(e);
591
- }
592
- };
593
- img.onerror = () => {
594
- URL.revokeObjectURL(url);
595
- reject(new Error("图片加载失败"));
596
- };
597
- img.src = url;
598
- }
599
- catch (error) {
600
- reject(error);
601
- }
602
- });
603
- }
604
- /**
605
- * 将ImageData转换为File对象
606
- *
607
- * @param imageData ImageData对象
608
- * @param fileName 输出文件名
609
- * @param fileType 输出文件类型
610
- * @param quality 图片质量 (0-1)
611
- * @returns Promise<File>
612
- */
613
- static async imageDataToFile(imageData, fileName = "image.jpg", fileType = "image/jpeg", quality = 0.8) {
614
- return new Promise((resolve, reject) => {
615
- try {
616
- const canvas = document.createElement("canvas");
617
- canvas.width = imageData.width;
618
- canvas.height = imageData.height;
619
- const ctx = canvas.getContext("2d");
620
- if (!ctx) {
621
- reject(new Error("无法创建2D上下文"));
622
- return;
623
- }
624
- ctx.putImageData(imageData, 0, 0);
625
- canvas.toBlob((blob) => {
626
- if (!blob) {
627
- reject(new Error("无法创建图片Blob"));
628
- return;
629
- }
630
- const file = new File([blob], fileName, { type: fileType });
631
- resolve(file);
632
- }, fileType, quality);
633
- }
634
- catch (error) {
635
- reject(error);
636
- }
637
- });
638
- }
639
- /**
640
- * 调整图像大小
641
- *
642
- * @param imageData 原始图像数据
643
- * @param maxWidth 最大宽度
644
- * @param maxHeight 最大高度
645
- * @param maintainAspectRatio 是否保持宽高比
646
- * @returns ImageData 调整大小后的图像数据
647
- */
648
- static resizeImage(imageData, maxWidth, maxHeight, maintainAspectRatio = true) {
649
- const { width, height } = imageData;
650
- // 如果图像已经小于指定大小,则不需要调整
651
- if (width <= maxWidth && height <= maxHeight) {
652
- return imageData;
653
- }
654
- let newWidth = maxWidth;
655
- let newHeight = maxHeight;
656
- // 计算新的尺寸,保持宽高比
657
- if (maintainAspectRatio) {
658
- const ratio = Math.min(maxWidth / width, maxHeight / height);
659
- newWidth = Math.floor(width * ratio);
660
- newHeight = Math.floor(height * ratio);
661
- }
662
- // 创建用于调整大小的Canvas
663
- const canvas = document.createElement("canvas");
664
- canvas.width = newWidth;
665
- canvas.height = newHeight;
666
- const ctx = canvas.getContext("2d");
667
- if (!ctx) {
668
- throw new Error("无法创建2D上下文");
669
- }
670
- // 创建临时Canvas绘制原始ImageData
671
- const tempCanvas = document.createElement("canvas");
672
- tempCanvas.width = width;
673
- tempCanvas.height = height;
674
- const tempCtx = tempCanvas.getContext("2d");
675
- if (!tempCtx) {
676
- throw new Error("无法创建临时2D上下文");
677
- }
678
- tempCtx.putImageData(imageData, 0, 0);
679
- // 使用缩放平滑算法
680
- ctx.imageSmoothingEnabled = true;
681
- ctx.imageSmoothingQuality = "high";
682
- // 绘制调整大小的图像
683
- ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, newWidth, newHeight);
684
- // 获取新的ImageData
685
- return ctx.getImageData(0, 0, newWidth, newHeight);
686
- }
687
- /**
688
- * 边缘检测算法,用于识别图像中的边缘
689
- * 基于Sobel算子实现
690
- *
691
- * @param imageData 原始图像数据,应已转为灰度图
692
- * @param threshold 边缘阈值,默认为30
693
- * @returns 检测到边缘的图像数据
694
- */
695
- static detectEdges(imageData, threshold = 30) {
696
- // 确保输入图像是灰度图
697
- const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
698
- const width = grayscaleImage.width;
699
- const height = grayscaleImage.height;
700
- const inputData = grayscaleImage.data;
701
- const outputData = new Uint8ClampedArray(inputData.length);
702
- // Sobel算子 - 水平和垂直方向
703
- const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
704
- const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
705
- // 对每个像素应用Sobel算子
706
- for (let y = 1; y < height - 1; y++) {
707
- for (let x = 1; x < width - 1; x++) {
708
- let gx = 0;
709
- let gy = 0;
710
- // 应用卷积
711
- for (let ky = -1; ky <= 1; ky++) {
712
- for (let kx = -1; kx <= 1; kx++) {
713
- const pixelPos = ((y + ky) * width + (x + kx)) * 4;
714
- const pixelVal = inputData[pixelPos]; // 灰度值
715
- const kernelIdx = (ky + 1) * 3 + (kx + 1);
716
- gx += pixelVal * sobelX[kernelIdx];
717
- gy += pixelVal * sobelY[kernelIdx];
718
- }
719
- }
720
- // 计算梯度强度
721
- let magnitude = Math.sqrt(gx * gx + gy * gy);
722
- // 应用阈值
723
- magnitude = magnitude > threshold ? 255 : 0;
724
- // 设置输出像素
725
- const pos = (y * width + x) * 4;
726
- outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = magnitude;
727
- outputData[pos + 3] = 255; // 透明度保持完全不透明
728
- }
729
- }
730
- // 处理边缘像素
731
- for (let i = 0; i < width * 4; i++) {
732
- // 顶部和底部行
733
- outputData[i] = 0;
734
- outputData[(height - 1) * width * 4 + i] = 0;
735
- }
736
- for (let i = 0; i < height; i++) {
737
- // 左右两侧列
738
- const leftPos = i * width * 4;
739
- const rightPos = (i * width + width - 1) * 4;
740
- for (let j = 0; j < 4; j++) {
741
- outputData[leftPos + j] = 0;
742
- outputData[rightPos + j] = 0;
743
- }
744
- }
745
- return new ImageData(outputData, width, height);
746
- }
747
- /**
748
- * 卡尼-德里奇边缘检测
749
- * 相比Sobel更精确的边缘检测算法
750
- *
751
- * @param imageData 灰度图像数据
752
- * @param lowThreshold 低阈值
753
- * @param highThreshold 高阈值
754
- * @returns 边缘检测结果
755
- */
756
- static cannyEdgeDetection(imageData, lowThreshold = 20, highThreshold = 50) {
757
- const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
758
- // 1. 高斯模糊
759
- const blurredImage = this.gaussianBlur(grayscaleImage, 1.5);
760
- // 2. 使用Sobel算子计算梯度
761
- const { gradientMagnitude, gradientDirection } = this.computeGradients(blurredImage);
762
- // 3. 非极大值抛弃
763
- const nonMaxSuppressed = this.nonMaxSuppression(gradientMagnitude, gradientDirection, blurredImage.width, blurredImage.height);
764
- // 4. 双阈值处理
765
- const thresholdResult = this.hysteresisThresholding(nonMaxSuppressed, blurredImage.width, blurredImage.height, lowThreshold, highThreshold);
766
- // 创建输出图像
767
- const outputData = new Uint8ClampedArray(imageData.data.length);
768
- // 将结果转换为ImageData
769
- for (let i = 0; i < thresholdResult.length; i++) {
770
- const pos = i * 4;
771
- const value = thresholdResult[i] ? 255 : 0;
772
- outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
773
- outputData[pos + 3] = 255;
774
- }
775
- return new ImageData(outputData, blurredImage.width, blurredImage.height);
776
- }
777
- /**
778
- * 高斯模糊
779
- */
780
- static gaussianBlur(imageData, sigma = 1.5) {
781
- const width = imageData.width;
782
- const height = imageData.height;
783
- const inputData = imageData.data;
784
- const outputData = new Uint8ClampedArray(inputData.length);
785
- // 生成高斯核
786
- const kernelSize = Math.max(3, Math.floor(sigma * 3) * 2 + 1);
787
- const halfKernel = Math.floor(kernelSize / 2);
788
- const kernel = this.generateGaussianKernel(kernelSize, sigma);
789
- // 应用高斯核
790
- for (let y = 0; y < height; y++) {
791
- for (let x = 0; x < width; x++) {
792
- let sum = 0;
793
- let weightSum = 0;
794
- for (let ky = -halfKernel; ky <= halfKernel; ky++) {
795
- for (let kx = -halfKernel; kx <= halfKernel; kx++) {
796
- const pixelY = Math.min(Math.max(y + ky, 0), height - 1);
797
- const pixelX = Math.min(Math.max(x + kx, 0), width - 1);
798
- const pixelPos = (pixelY * width + pixelX) * 4;
799
- const kernelY = ky + halfKernel;
800
- const kernelX = kx + halfKernel;
801
- const weight = kernel[kernelY * kernelSize + kernelX];
802
- sum += inputData[pixelPos] * weight;
803
- weightSum += weight;
804
- }
805
- }
806
- const pos = (y * width + x) * 4;
807
- const value = Math.round(sum / weightSum);
808
- outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
809
- outputData[pos + 3] = 255;
810
- }
811
- }
812
- return new ImageData(outputData, width, height);
813
- }
814
- /**
815
- * 生成高斯核
816
- */
817
- static generateGaussianKernel(size, sigma) {
818
- const kernel = new Array(size * size);
819
- const center = Math.floor(size / 2);
820
- let sum = 0;
821
- for (let y = 0; y < size; y++) {
822
- for (let x = 0; x < size; x++) {
823
- const distance = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
824
- const value = Math.exp(-(distance ** 2) / (2 * sigma ** 2));
825
- kernel[y * size + x] = value;
826
- sum += value;
827
- }
828
- }
829
- // 归一化
830
- for (let i = 0; i < kernel.length; i++) {
831
- kernel[i] /= sum;
832
- }
833
- return kernel;
834
- }
835
- /**
836
- * 计算梯度强度和方向
837
- */
838
- static computeGradients(imageData) {
839
- const width = imageData.width;
840
- const height = imageData.height;
841
- const inputData = imageData.data;
842
- const gradientMagnitude = new Array(width * height);
843
- const gradientDirection = new Array(width * height);
844
- // Sobel算子
845
- const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
846
- const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
847
- for (let y = 1; y < height - 1; y++) {
848
- for (let x = 1; x < width - 1; x++) {
849
- let gx = 0;
850
- let gy = 0;
851
- for (let ky = -1; ky <= 1; ky++) {
852
- for (let kx = -1; kx <= 1; kx++) {
853
- const pixelPos = ((y + ky) * width + (x + kx)) * 4;
854
- const pixelVal = inputData[pixelPos];
855
- const kernelIdx = (ky + 1) * 3 + (kx + 1);
856
- gx += pixelVal * sobelX[kernelIdx];
857
- gy += pixelVal * sobelY[kernelIdx];
858
- }
859
- }
860
- const idx = y * width + x;
861
- gradientMagnitude[idx] = Math.sqrt(gx * gx + gy * gy);
862
- gradientDirection[idx] = Math.atan2(gy, gx);
863
- }
864
- }
865
- // 处理边界
866
- for (let y = 0; y < height; y++) {
867
- for (let x = 0; x < width; x++) {
868
- if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
869
- const idx = y * width + x;
870
- gradientMagnitude[idx] = 0;
871
- gradientDirection[idx] = 0;
872
- }
873
- }
874
- }
875
- return { gradientMagnitude, gradientDirection };
876
- }
877
- /**
878
- * 非极大值抛弃
879
- */
880
- static nonMaxSuppression(gradientMagnitude, gradientDirection, width, height) {
881
- const result = new Array(width * height).fill(0);
882
- for (let y = 1; y < height - 1; y++) {
883
- for (let x = 1; x < width - 1; x++) {
884
- const idx = y * width + x;
885
- const magnitude = gradientMagnitude[idx];
886
- const direction = gradientDirection[idx];
887
- // 将方向转化为角度
888
- const degrees = (direction * 180 / Math.PI + 180) % 180;
889
- // 获取相邻像素索引
890
- let neighbor1Idx, neighbor2Idx;
891
- // 将方向量化为四个方向: 0°, 45°, 90°, 135°
892
- if ((degrees >= 0 && degrees < 22.5) || (degrees >= 157.5 && degrees <= 180)) {
893
- // 水平方向
894
- neighbor1Idx = idx - 1;
895
- neighbor2Idx = idx + 1;
896
- }
897
- else if (degrees >= 22.5 && degrees < 67.5) {
898
- // 45度方向
899
- neighbor1Idx = (y - 1) * width + (x + 1);
900
- neighbor2Idx = (y + 1) * width + (x - 1);
901
- }
902
- else if (degrees >= 67.5 && degrees < 112.5) {
903
- // 垂直方向
904
- neighbor1Idx = (y - 1) * width + x;
905
- neighbor2Idx = (y + 1) * width + x;
906
- }
907
- else {
908
- // 135度方向
909
- neighbor1Idx = (y - 1) * width + (x - 1);
910
- neighbor2Idx = (y + 1) * width + (x + 1);
911
- }
912
- // 检查当前像素是否是最大值
913
- if (magnitude >= gradientMagnitude[neighbor1Idx] &&
914
- magnitude >= gradientMagnitude[neighbor2Idx]) {
915
- result[idx] = magnitude;
916
- }
917
- }
918
- }
919
- return result;
920
- }
921
- /**
922
- * 双阈值处理
923
- */
924
- static hysteresisThresholding(nonMaxSuppressed, width, height, lowThreshold, highThreshold) {
925
- const result = new Array(width * height).fill(false);
926
- const visited = new Array(width * height).fill(false);
927
- const stack = [];
928
- // 标记强边缘点
929
- for (let i = 0; i < nonMaxSuppressed.length; i++) {
930
- if (nonMaxSuppressed[i] >= highThreshold) {
931
- result[i] = true;
932
- stack.push(i);
933
- visited[i] = true;
934
- }
935
- }
936
- // 使用深度优先搜索连接弱边缘
937
- const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
938
- const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
939
- while (stack.length > 0) {
940
- const currentIdx = stack.pop();
941
- const currentX = currentIdx % width;
942
- const currentY = Math.floor(currentIdx / width);
943
- // 检查88个相邻方向
944
- for (let i = 0; i < 8; i++) {
945
- const newX = currentX + dx[i];
946
- const newY = currentY + dy[i];
947
- if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
948
- const newIdx = newY * width + newX;
949
- if (!visited[newIdx] && nonMaxSuppressed[newIdx] >= lowThreshold) {
950
- result[newIdx] = true;
951
- stack.push(newIdx);
952
- visited[newIdx] = true;
953
- }
954
- }
955
- }
956
- }
957
- return result;
958
- }
959
- }
960
-
961
- /**
962
- * @file 条形码扫描模块
963
- * @description 提供实时条形码扫描和识别功能
964
- * @module BarcodeScanner
965
- */
966
- /**
967
- * 条形码扫描器类
968
- *
969
- * 提供实时扫描和识别摄像头中的条形码的功能
970
- * 注意:当前实现是简化版,实际项目中建议集成专门的条形码识别库如ZXing或Quagga.js
971
- *
972
- * @example
973
- * ```typescript
974
- * // 创建条形码扫描器
975
- * const barcodeScanner = new BarcodeScanner({
976
- * scanInterval: 100, // 每100ms扫描一次
977
- * onScan: (result) => {
978
- * console.log('扫描到条形码:', result);
979
- * },
980
- * onError: (error) => {
981
- * console.error('扫描错误:', error);
982
- * }
983
- * });
984
- *
985
- * // 启动扫描
986
- * const videoElement = document.getElementById('video') as HTMLVideoElement;
987
- * await barcodeScanner.start(videoElement);
988
- *
989
- * // 停止扫描
990
- * barcodeScanner.stop();
991
- * ```
992
- */
993
- class BarcodeScanner {
994
- /**
995
- * 创建条形码扫描器实例
996
- *
997
- * @param {BarcodeScannerOptions} [options] - 扫描器配置选项
998
- */
999
- constructor(options = {}) {
1000
- this.options = options;
1001
- this.scanning = false;
1002
- this.scanTimer = null;
1003
- this.options = {
1004
- scanInterval: 200,
1005
- ...options,
1006
- };
1007
- this.camera = new Camera();
1008
- }
1009
- /**
1010
- * 启动条形码扫描
1011
- *
1012
- * 初始化相机并开始连续扫描视频帧中的条形码
1013
- *
1014
- * @param {HTMLVideoElement} videoElement - 用于显示相机画面的video元素
1015
- * @returns {Promise<void>} 启动完成的Promise
1016
- * @throws 如果无法访问相机,将通过onError回调报告错误
1017
- */
1018
- async start(videoElement) {
1019
- try {
1020
- await this.camera.initialize(videoElement);
1021
- this.scanning = true;
1022
- this.scan();
1023
- }
1024
- catch (error) {
1025
- if (this.options.onError) {
1026
- this.options.onError(error instanceof Error ? error : new Error(String(error)));
1027
- }
1028
- }
1029
- }
1030
- /**
1031
- * 执行一次条形码扫描
1032
- *
1033
- * 内部方法,捕获当前视频帧并尝试识别其中的条形码
1034
- *
1035
- * @private
1036
- */
1037
- scan() {
1038
- if (!this.scanning)
1039
- return;
1040
- const imageData = this.camera.captureFrame();
1041
- if (imageData) {
1042
- try {
1043
- // 图像预处理,提高识别率
1044
- const enhancedImage = ImageProcessor.adjustBrightnessContrast(ImageProcessor.toGrayscale(imageData), 10, // 亮度
1045
- 20 // 对比度
1046
- );
1047
- // 这里实际项目中可以集成第三方条形码扫描库
1048
- // 如 ZXing 或 QuaggaJS
1049
- // 简化实现,这里仅为示例
1050
- this.detectBarcode(enhancedImage);
1051
- }
1052
- catch (error) {
1053
- console.error("条形码扫描错误:", error);
1054
- }
1055
- }
1056
- this.scanTimer = window.setTimeout(() => this.scan(), this.options.scanInterval);
1057
- }
1058
- /**
1059
- * 条形码检测方法
1060
- *
1061
- * 注意:这是一个简化实现,实际需要集成专门的条形码识别库
1062
- *
1063
- * @private
1064
- * @param {ImageData} imageData - 要检测条形码的图像数据
1065
- */
1066
- detectBarcode(imageData) {
1067
- // 这里应集成条形码识别库
1068
- // 如 ZXing 或 QuaggaJS
1069
- // 简化示例,实际项目中请替换为真实实现
1070
- console.log("正在扫描条形码...");
1071
- // 模拟找到条形码
1072
- if (Math.random() > 0.95) {
1073
- const mockResult = "6901234567890"; // 模拟条形码结果
1074
- if (this.options.onScan) {
1075
- this.options.onScan(mockResult);
1076
- }
1077
- }
1078
- }
1079
- /**
1080
- * 停止条形码扫描
1081
- *
1082
- * 停止扫描循环并释放相机资源
1083
- */
1084
- stop() {
1085
- this.scanning = false;
1086
- if (this.scanTimer) {
1087
- clearTimeout(this.scanTimer);
1088
- this.scanTimer = null;
1089
- }
1090
- this.camera.release();
1091
- }
1092
- /**
1093
- * 处理图像数据中的条形码
1094
- *
1095
- * @param {ImageData} imageData - 要处理的图像数据
1096
- * @returns {string | null} 识别到的条形码内容,如未识别到则返回null
1097
- */
1098
- processImageData(imageData) {
1099
- try {
1100
- if (!imageData ||
1101
- !imageData.data ||
1102
- imageData.width <= 0 ||
1103
- imageData.height <= 0) {
1104
- throw new Error("无效的图像数据");
1105
- }
1106
- // 图像预处理,提高识别率
1107
- const enhancedImage = ImageProcessor.adjustBrightnessContrast(ImageProcessor.toGrayscale(imageData), 10, // 亮度
1108
- 20 // 对比度
1109
- );
1110
- // 注意:这里是简化实现
1111
- // 实际项目中,应该集成专门的条形码识别库如ZXing或Quagga.js
1112
- // 模拟条形码识别
1113
- // 在真实项目中,请替换为实际的条形码识别算法
1114
- const result = this.simulateBarcodeDetection(enhancedImage);
1115
- return result;
1116
- }
1117
- catch (error) {
1118
- if (this.options.onError) {
1119
- this.options.onError(error instanceof Error ? error : new Error(String(error)));
1120
- }
1121
- return null;
1122
- }
1123
- }
1124
- /**
1125
- * 模拟条形码检测
1126
- * 仅用于演示,实际使用时应该替换为真实的条形码识别算法
1127
- *
1128
- * @private
1129
- * @param {ImageData} imageData - 要检测条形码的图像数据
1130
- * @returns {string | null} 模拟的条形码识别结果
1131
- */
1132
- simulateBarcodeDetection(imageData) {
1133
- // 这里只是模拟,真实环境中应当使用条形码识别库进行识别
1134
- // 在中间区域检测到足够多垂直边缘时,认为可能存在条形码
1135
- const midX = Math.floor(imageData.width / 2);
1136
- const midY = Math.floor(imageData.height / 2);
1137
- const sampleWidth = Math.min(100, Math.floor(imageData.width / 3));
1138
- let edgeCount = 0;
1139
- let lastPixel = 0;
1140
- // 简单的边缘检测,统计中心水平线上像素变化次数
1141
- for (let x = midX - sampleWidth / 2; x < midX + sampleWidth / 2; x++) {
1142
- const pixelPos = (midY * imageData.width + x) * 4;
1143
- const pixelValue = imageData.data[pixelPos];
1144
- if (Math.abs(pixelValue - lastPixel) > 30) {
1145
- edgeCount++;
1146
- }
1147
- lastPixel = pixelValue;
1148
- }
1149
- // 如果边缘变化次数在合理范围内,认为是条形码
1150
- // 实际的条形码具有规律的宽窄条纹
1151
- if (edgeCount > 10 && edgeCount < 50) {
1152
- // 生成一个模拟的条形码结果
1153
- return "690" + Math.floor(Math.random() * 10000000000);
1154
- }
1155
- return null;
1156
- }
1157
- }
1158
-
1159
- /**
1160
- * @file 性能优化工具类
1161
- * @description 提供节流、防抖、缓存等性能优化功能
1162
- * @module PerformanceUtils
1163
- */
1164
- /**
1165
- * 节流函数:限制函数在一定时间内只能执行一次
1166
- *
1167
- * @param fn 需要节流的函数
1168
- * @param delay 延迟时间(毫秒)
1169
- * @returns 节流处理后的函数
1170
- */
1171
- function throttle(fn, delay) {
1172
- let lastCall = 0;
1173
- let timeoutId = null;
1174
- return function (...args) {
1175
- const now = Date.now();
1176
- const remaining = delay - (now - lastCall);
1177
- if (remaining <= 0) {
1178
- if (timeoutId) {
1179
- clearTimeout(timeoutId);
1180
- timeoutId = null;
1181
- }
1182
- lastCall = now;
1183
- fn.apply(this, args);
1184
- }
1185
- else if (!timeoutId) {
1186
- timeoutId = window.setTimeout(() => {
1187
- lastCall = Date.now();
1188
- timeoutId = null;
1189
- fn.apply(this, args);
1190
- }, remaining);
1191
- }
1192
- };
1193
- }
1194
- /**
1195
- * LRU缓存类 - 使用最近最少使用策略的缓存实现
1196
- */
1197
- class LRUCache {
1198
- /**
1199
- * 构造LRU缓存
1200
- * @param maxSize 缓存最大容量
1201
- */
1202
- constructor(maxSize = 100) {
1203
- this.maxSize = maxSize;
1204
- this.cache = new Map();
1205
- }
1206
- /**
1207
- * 获取缓存项
1208
- * @param key 缓存键
1209
- * @returns 缓存值或undefined
1210
- */
1211
- get(key) {
1212
- if (!this.cache.has(key)) {
1213
- return undefined;
1214
- }
1215
- // 获取值
1216
- const value = this.cache.get(key);
1217
- // 将项移至最新位置(删除后重新添加)
1218
- this.cache.delete(key);
1219
- this.cache.set(key, value);
1220
- return value;
1221
- }
1222
- /**
1223
- * 设置缓存项
1224
- * @param key 缓存键
1225
- * @param value 缓存值
1226
- */
1227
- set(key, value) {
1228
- // 如果键已存在,需要先删除
1229
- if (this.cache.has(key)) {
1230
- this.cache.delete(key);
1231
- }
1232
- // 如果缓存已满,移除最老的项
1233
- if (this.cache.size >= this.maxSize) {
1234
- const oldestKey = this.cache.keys().next().value;
1235
- if (oldestKey !== undefined) {
1236
- this.cache.delete(oldestKey);
1237
- }
1238
- }
1239
- // 添加新项
1240
- this.cache.set(key, value);
1241
- }
1242
- /**
1243
- * 删除缓存项
1244
- * @param key 缓存键
1245
- * @returns 是否成功删除
1246
- */
1247
- delete(key) {
1248
- return this.cache.delete(key);
1249
- }
1250
- /**
1251
- * 清空缓存
1252
- */
1253
- clear() {
1254
- this.cache.clear();
1255
- }
1256
- /**
1257
- * 获取当前缓存大小
1258
- */
1259
- get size() {
1260
- return this.cache.size;
1261
- }
1262
- /**
1263
- * 检查键是否存在
1264
- * @param key 缓存键
1265
- */
1266
- has(key) {
1267
- return this.cache.has(key);
1268
- }
1269
- }
1270
- /**
1271
- * 图像指纹计算函数 - 用于检测相同或相似图像
1272
- *
1273
- * @param imageData 图像数据
1274
- * @param size 指纹尺寸(默认8x8)
1275
- * @returns 图像指纹字符串
1276
- */
1277
- function calculateImageFingerprint(imageData, size = 8) {
1278
- // 1. 缩小图像到指定尺寸
1279
- const canvas = document.createElement('canvas');
1280
- canvas.width = size;
1281
- canvas.height = size;
1282
- const ctx = canvas.getContext('2d');
1283
- if (!ctx) {
1284
- return '';
1285
- }
1286
- // 创建一个临时canvas来绘制原始imageData
1287
- const tempCanvas = document.createElement('canvas');
1288
- tempCanvas.width = imageData.width;
1289
- tempCanvas.height = imageData.height;
1290
- const tempCtx = tempCanvas.getContext('2d');
1291
- if (!tempCtx) {
1292
- return '';
1293
- }
1294
- tempCtx.putImageData(imageData, 0, 0);
1295
- // 缩小到目标尺寸
1296
- ctx.drawImage(tempCanvas, 0, 0, imageData.width, imageData.height, 0, 0, size, size);
1297
- // 2. 转换为灰度
1298
- const smallImgData = ctx.getImageData(0, 0, size, size);
1299
- const grayValues = [];
1300
- for (let i = 0; i < smallImgData.data.length; i += 4) {
1301
- const r = smallImgData.data[i];
1302
- const g = smallImgData.data[i + 1];
1303
- const b = smallImgData.data[i + 2];
1304
- // 转为灰度: 0.299r + 0.587g + 0.114b
1305
- const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
1306
- grayValues.push(gray);
1307
- }
1308
- // 3. 计算平均值
1309
- const avg = grayValues.reduce((sum, val) => sum + val, 0) / grayValues.length;
1310
- // 4. 比较每个像素与平均值,生成二进制指纹
1311
- let fingerprint = '';
1312
- for (const gray of grayValues) {
1313
- fingerprint += gray >= avg ? '1' : '0';
1314
- }
1315
- return fingerprint;
1316
- }
1317
-
1318
- /**
1319
- * @file 身份证检测模块
1320
- * @description 提供自动检测和定位图像中的身份证功能
1321
- * @module IDCardDetector
1322
- * @version 1.3.2
1323
- */
1324
- /**
1325
- * 身份证检测器类
1326
- *
1327
- * 通过图像处理和计算机视觉技术,实时检测视频流中的身份证,并提取身份证区域
1328
- * 注意:当前实现是简化版,实际项目中建议使用OpenCV.js进行更精确的检测
1329
- *
1330
- * @example
1331
- * ```typescript
1332
- * // 创建身份证检测器
1333
- * const detector = new IDCardDetector((result) => {
1334
- * if (result.success && result.croppedImage) {
1335
- * console.log('检测到身份证!');
1336
- * // 对裁剪出的身份证图像进行处理
1337
- * processIDCardImage(result.croppedImage);
1338
- * }
1339
- * });
1340
- *
1341
- * // 启动检测
1342
- * const videoElement = document.getElementById('video') as HTMLVideoElement;
1343
- * await detector.start(videoElement);
1344
- *
1345
- * // 停止检测
1346
- * detector.stop();
1347
- * ```
1348
- */
1349
- class IDCardDetector {
1350
- /**
1351
- * 创建身份证检测器实例
1352
- *
1353
- * @param options 身份证检测器配置选项,或者检测回调函数
1354
- */
1355
- constructor(options) {
1356
- this.detecting = false;
1357
- this.detectTimer = null;
1358
- this.frameCount = 0;
1359
- this.lastDetectionTime = 0;
1360
- this.camera = new Camera();
1361
- if (typeof options === "function") {
1362
- // 兼容旧的构造函数方式
1363
- this.onDetected = options;
1364
- this.options = {
1365
- detectionInterval: 200,
1366
- maxImageDimension: 800,
1367
- enableCache: true,
1368
- cacheSize: 20,
1369
- logger: console.log,
1370
- };
1371
- }
1372
- else if (options) {
1373
- // 使用新的选项对象方式
1374
- this.options = {
1375
- detectionInterval: 200,
1376
- maxImageDimension: 800,
1377
- enableCache: true,
1378
- cacheSize: 20,
1379
- logger: console.log,
1380
- ...options,
1381
- };
1382
- this.onDetected = options.onDetection;
1383
- this.onError = options.onError;
1384
- }
1385
- else {
1386
- this.options = {
1387
- detectionInterval: 200,
1388
- maxImageDimension: 800,
1389
- enableCache: true,
1390
- cacheSize: 20,
1391
- logger: console.log,
1392
- };
1393
- }
1394
- this.detectionInterval = this.options.detectionInterval;
1395
- this.maxImageDimension = this.options.maxImageDimension;
1396
- // 初始化结果缓存
1397
- this.resultCache = new LRUCache(this.options.cacheSize);
1398
- // 创建节流版本的检测函数
1399
- this.throttledDetect = throttle(this.performDetection.bind(this), this.detectionInterval);
1400
- }
1401
- /**
1402
- * 启动身份证检测
1403
- *
1404
- * 初始化相机并开始连续检测视频帧中的身份证
1405
- *
1406
- * @param {HTMLVideoElement} videoElement - 用于显示相机画面的video元素
1407
- * @returns {Promise<void>} 启动完成的Promise
1408
- */
1409
- async start(videoElement) {
1410
- await this.camera.initialize(videoElement);
1411
- this.detecting = true;
1412
- this.frameCount = 0;
1413
- this.lastDetectionTime = 0;
1414
- this.detect();
1415
- }
1416
- /**
1417
- * 停止身份证检测
1418
- */
1419
- stop() {
1420
- this.detecting = false;
1421
- if (this.detectTimer !== null) {
1422
- cancelAnimationFrame(this.detectTimer);
1423
- this.detectTimer = null;
1424
- }
1425
- }
1426
- /**
1427
- * 持续检测视频帧
1428
- *
1429
- * @private
1430
- */
1431
- detect() {
1432
- if (!this.detecting)
1433
- return;
1434
- this.detectTimer = requestAnimationFrame(() => {
1435
- try {
1436
- this.frameCount++;
1437
- const now = performance.now();
1438
- // 帧率控制 - 只有满足时间间隔的帧才进行检测
1439
- // 这样可以显著减少CPU使用率,同时保持良好的用户体验
1440
- if (this.frameCount % 3 === 0 ||
1441
- now - this.lastDetectionTime >= this.detectionInterval) {
1442
- this.throttledDetect();
1443
- this.lastDetectionTime = now;
1444
- }
1445
- // 继续下一帧检测
1446
- this.detect();
1447
- }
1448
- catch (error) {
1449
- if (this.onError) {
1450
- this.onError(error);
1451
- }
1452
- else {
1453
- console.error("身份证检测错误:", error);
1454
- }
1455
- // 出错后延迟重试
1456
- setTimeout(() => {
1457
- if (this.detecting) {
1458
- this.detect();
1459
- }
1460
- }, 1000);
1461
- }
1462
- });
1463
- }
1464
- /**
1465
- * 执行单帧检测
1466
- *
1467
- * @private
1468
- */
1469
- async performDetection() {
1470
- if (!this.detecting || !this.camera)
1471
- return;
1472
- // 获取当前视频帧
1473
- const frame = this.camera.captureFrame();
1474
- if (!frame)
1475
- return;
1476
- // 检查缓存
1477
- if (this.options.enableCache) {
1478
- const fingerprint = calculateImageFingerprint(frame, 16); // 使用更大的尺寸提高特征区分度
1479
- const cachedResult = this.resultCache.get(fingerprint);
1480
- if (cachedResult) {
1481
- this.options.logger?.("使用缓存的检测结果");
1482
- // 使用缓存结果,但更新图像数据以确保最新
1483
- const updatedResult = {
1484
- ...cachedResult,
1485
- imageData: frame,
1486
- };
1487
- if (this.onDetected) {
1488
- this.onDetected(updatedResult);
1489
- }
1490
- return;
1491
- }
1492
- }
1493
- // 降低分辨率以提高性能
1494
- const downsampledFrame = ImageProcessor.resizeImage(frame, this.maxImageDimension, this.maxImageDimension);
1495
- try {
1496
- // 检测身份证
1497
- const result = await this.detectIDCard(downsampledFrame);
1498
- // 如果检测成功,将原始图像添加到结果中
1499
- if (result.success) {
1500
- result.imageData = frame;
1501
- // 缓存结果
1502
- if (this.options.enableCache) {
1503
- const fingerprint = calculateImageFingerprint(frame, 16);
1504
- this.resultCache.set(fingerprint, result);
1505
- }
1506
- }
1507
- // 处理检测结果
1508
- if (this.onDetected) {
1509
- this.onDetected(result);
1510
- }
1511
- }
1512
- catch (error) {
1513
- if (this.onError) {
1514
- this.onError(error);
1515
- }
1516
- else {
1517
- console.error("身份证检测错误:", error);
1518
- }
1519
- }
1520
- }
1521
- /**
1522
- * 检测图像中的身份证
1523
- *
1524
- * @private
1525
- * @param {ImageData} imageData - 要分析的图像数据
1526
- * @returns {Promise<DetectionResult>} 检测结果
1527
- */
1528
- async detectIDCard(imageData) {
1529
- // 1. 图像预处理
1530
- const grayscale = ImageProcessor.toGrayscale(imageData);
1531
- // 2. 使用Sobel边缘检测算法检测边缘
1532
- const edgeData = ImageProcessor.detectEdges(grayscale);
1533
- // 3. 检测矩形和边缘
1534
- // 使用基于边缘的矩形检测
1535
- const rectangles = this.detectRectangles(edgeData);
1536
- // 4. 评估检测结果 - 检查是否找到了合适的矩形
1537
- const idCardRect = this.findIdCardRectangle(rectangles, imageData.width, imageData.height);
1538
- const detectionResult = {
1539
- success: idCardRect !== null,
1540
- message: idCardRect ? "身份证检测成功" : "未检测到身份证",
1541
- };
1542
- if (detectionResult.success && idCardRect) {
1543
- // 使用实际检测到的身份证区域
1544
- const rectWidth = idCardRect.width;
1545
- const rectHeight = idCardRect.height;
1546
- const rectX = idCardRect.x;
1547
- const rectY = idCardRect.y;
1548
- // 添加四个角点
1549
- detectionResult.corners = [
1550
- { x: rectX, y: rectY },
1551
- { x: rectX + rectWidth, y: rectY },
1552
- { x: rectX + rectWidth, y: rectY + rectHeight },
1553
- { x: rectX, y: rectY + rectHeight },
1554
- ];
1555
- // 添加边界框
1556
- detectionResult.boundingBox = {
1557
- x: rectX,
1558
- y: rectY,
1559
- width: rectWidth,
1560
- height: rectHeight,
1561
- };
1562
- // 裁剪身份证图像
1563
- const canvas = document.createElement("canvas");
1564
- canvas.width = rectWidth;
1565
- canvas.height = rectHeight;
1566
- const ctx = canvas.getContext("2d");
1567
- if (ctx) {
1568
- const tempCanvas = ImageProcessor.imageDataToCanvas(imageData);
1569
- ctx.drawImage(tempCanvas, rectX, rectY, rectWidth, rectHeight, 0, 0, rectWidth, rectHeight);
1570
- detectionResult.croppedImage = ctx.getImageData(0, 0, rectWidth, rectHeight);
1571
- }
1572
- // 设置置信度 - 基于边缘强度和矩形形状评分
1573
- detectionResult.confidence = this.calculateConfidence(idCardRect, edgeData);
1574
- }
1575
- return detectionResult;
1576
- }
1577
- /**
1578
- * 清除检测结果缓存
1579
- */
1580
- clearCache() {
1581
- this.resultCache.clear();
1582
- this.options.logger?.("检测结果缓存已清除");
1583
- }
1584
- /**
1585
- * 释放资源
1586
- */
1587
- dispose() {
1588
- this.stop();
1589
- this.camera.release();
1590
- this.resultCache.clear();
1591
- }
1592
- /**
1593
- * 从边缘图像中检测矩形
1594
- * @param edgeData 边缘检测后的图像数据
1595
- * @returns 检测到的矩形数组
1596
- */
1597
- detectRectangles(edgeData) {
1598
- const width = edgeData.width;
1599
- const height = edgeData.height;
1600
- const minSize = Math.min(width, height) * 0.2; // 最小矩形尺寸
1601
- const rectangles = [];
1602
- // 使用积分图像加速边缘密度计算
1603
- const integralImg = new Uint32Array(width * height);
1604
- // 计算积分图像
1605
- for (let y = 0; y < height; y++) {
1606
- for (let x = 0; x < width; x++) {
1607
- const idx = y * width + x;
1608
- const pixel = (edgeData.data[idx * 4] > 128) ? 1 : 0; // 边缘为白色
1609
- // 计算积分图
1610
- const above = y > 0 ? integralImg[(y - 1) * width + x] : 0;
1611
- const left = x > 0 ? integralImg[y * width + (x - 1)] : 0;
1612
- const diagonal = (x > 0 && y > 0) ? integralImg[(y - 1) * width + (x - 1)] : 0;
1613
- integralImg[idx] = pixel + above + left - diagonal;
1614
- }
1615
- }
1616
- // 滑动窗口检测矩形
1617
- for (let h = minSize; h < height * 0.9; h += Math.max(2, Math.floor(h * 0.05))) {
1618
- // 计算当前高度下,按照标准身份证比例的宽度
1619
- const w = Math.round(h * IDCardDetector.ID_CARD_ASPECT_RATIO);
1620
- if (w > width * 0.9)
1621
- continue;
1622
- for (let y = 0; y < height - h; y += Math.max(2, Math.floor(h * 0.1))) {
1623
- for (let x = 0; x < width - w; x += Math.max(2, Math.floor(w * 0.1))) {
1624
- // 计算矩形区域内的边缘密度
1625
- const edgeCount = this.calculateRectSum(integralImg, x, y, w, h, width);
1626
- const avgEdgeDensity = edgeCount / (w * h);
1627
- // 计算矩形边界的边缘密度
1628
- const perimeterEdgeCount = this.calculateRectPerimeter(integralImg, x, y, w, h, width);
1629
- const perimeterLength = 2 * (w + h);
1630
- const perimeterDensity = perimeterEdgeCount / perimeterLength;
1631
- // 矩形得分 - 边界边缘密度高且内部适中
1632
- const rectScore = perimeterDensity * 0.7 + (0.3 - Math.abs(0.15 - avgEdgeDensity)) * 0.3;
1633
- if (rectScore > 0.4) { // 阈值可根据实际项目调整
1634
- rectangles.push({
1635
- x,
1636
- y,
1637
- width: w,
1638
- height: h,
1639
- confidence: rectScore
1640
- });
1641
- }
1642
- }
1643
- }
1644
- }
1645
- // 按得分排序
1646
- return rectangles.sort((a, b) => b.confidence - a.confidence);
1647
- }
1648
- /**
1649
- * 使用积分图计算矩形区域内的总和
1650
- */
1651
- calculateRectSum(integral, x, y, w, h, stride) {
1652
- const x2 = Math.min(x + w - 1, stride - 1);
1653
- const y2 = Math.min(y + h - 1, integral.length / stride - 1);
1654
- const topLeft = (x > 0 && y > 0) ? integral[(y - 1) * stride + (x - 1)] : 0;
1655
- const topRight = y > 0 ? integral[(y - 1) * stride + x2] : 0;
1656
- const bottomLeft = x > 0 ? integral[y2 * stride + (x - 1)] : 0;
1657
- const bottomRight = integral[y2 * stride + x2];
1658
- return bottomRight - topRight - bottomLeft + topLeft;
1659
- }
1660
- /**
1661
- * 计算矩形周长上的边缘点数量
1662
- */
1663
- calculateRectPerimeter(integral, x, y, w, h, stride) {
1664
- // 上边缘
1665
- const topEdgeSum = this.calculateRectSum(integral, x, y, w, 1, stride);
1666
- // 下边缘
1667
- const bottomEdgeSum = this.calculateRectSum(integral, x, y + h - 1, w, 1, stride);
1668
- // 左边缘
1669
- const leftEdgeSum = this.calculateRectSum(integral, x, y, 1, h, stride);
1670
- // 右边缘
1671
- const rightEdgeSum = this.calculateRectSum(integral, x + w - 1, y, 1, h, stride);
1672
- return topEdgeSum + bottomEdgeSum + leftEdgeSum + rightEdgeSum;
1673
- }
1674
- /**
1675
- * 从检测到的矩形中找出最可能是身份证的矩形
1676
- */
1677
- findIdCardRectangle(rectangles, imageWidth, imageHeight) {
1678
- if (rectangles.length === 0)
1679
- return null;
1680
- // 筛选符合身份证宽高比的矩形
1681
- const filteredRects = rectangles.filter(rect => {
1682
- const aspectRatio = rect.width / rect.height;
1683
- return Math.abs(aspectRatio - IDCardDetector.ID_CARD_ASPECT_RATIO) < 0.2; // 允许20%的误差
1684
- });
1685
- if (filteredRects.length === 0)
1686
- return null;
1687
- // 返回得分最高的矩形
1688
- return filteredRects[0];
1689
- }
1690
- /**
1691
- * 计算身份证检测的置信度
1692
- */
1693
- calculateConfidence(rect, edgeData) {
1694
- if (!rect)
1695
- return 0;
1696
- // 基本得分来自矩形检测
1697
- let score = rect.confidence;
1698
- // 额外因素:矩形大小相对于图像
1699
- const relativeSize = (rect.width * rect.height) / (edgeData.width * edgeData.height);
1700
- if (relativeSize > 0.1 && relativeSize < 0.7) {
1701
- score += 0.1; // 身份证通常占据图像的合理比例
1702
- }
1703
- // 范围限制在0-1之间
1704
- return Math.min(Math.max(score, 0), 1);
1705
- }
1706
- }
1707
- // 身份证标准宽高比(近似黄金比例)
1708
- IDCardDetector.ID_CARD_ASPECT_RATIO = 1.58; // 标准身份证宽高比
1709
-
1710
- /**
1711
- * @file Web Worker辅助工具类
1712
- * @description 提供Worker线程管理功能,用于将计算密集型任务移至后台线程
1713
- * @module WorkerUtils
1714
- */
1715
- /**
1716
- * 创建Worker线程并处理消息通信
1717
- *
1718
- * @param workerFunction 要在Worker中执行的函数
1719
- * @returns 返回包含发送消息方法的Worker控制对象
1720
- */
1721
- function createWorker(workerFunction) {
1722
- // 将函数转换为字符串,然后创建一个Blob URL
1723
- const workerCode = `
1724
- self.onmessage = async function(e) {
1725
- try {
1726
- const result = await (${workerFunction.toString()})(e.data);
1727
- self.postMessage({ success: true, result });
1728
- } catch (error) {
1729
- self.postMessage({
1730
- success: false,
1731
- error: { message: error.message, stack: error.stack }
1732
- });
1733
- }
1734
- }
1735
- `;
1736
- const blob = new Blob([workerCode], { type: 'application/javascript' });
1737
- const workerUrl = URL.createObjectURL(blob);
1738
- const worker = new Worker(workerUrl);
1739
- // 创建一个映射来存储待解析的Promise
1740
- const promiseMap = new Map();
1741
- let messageCounter = 0;
1742
- worker.onmessage = (e) => {
1743
- // 释放Blob URL
1744
- if (promiseMap.size === 0) {
1745
- URL.revokeObjectURL(workerUrl);
1746
- }
1747
- const { id, success, result, error } = e.data;
1748
- const promiseHandlers = promiseMap.get(id);
1749
- if (promiseHandlers) {
1750
- promiseMap.delete(id);
1751
- if (success) {
1752
- promiseHandlers.resolve(result);
1753
- }
1754
- else {
1755
- const workerError = new Error(error.message);
1756
- workerError.stack = error.stack;
1757
- promiseHandlers.reject(workerError);
1758
- }
1759
- }
1760
- };
1761
- return {
1762
- postMessage: (data) => {
1763
- return new Promise((resolve, reject) => {
1764
- const id = messageCounter++;
1765
- promiseMap.set(id, { resolve, reject });
1766
- worker.postMessage({ id, data });
1767
- });
1768
- },
1769
- terminate: () => {
1770
- worker.terminate();
1771
- promiseMap.clear();
1772
- URL.revokeObjectURL(workerUrl);
1773
- }
1774
- };
1775
- }
1776
- /**
1777
- * 判断浏览器是否支持Web Workers
1778
- *
1779
- * @returns 是否支持Web Workers
1780
- */
1781
- function isWorkerSupported() {
1782
- return typeof Worker !== 'undefined';
1783
- }
1784
-
1785
- /**
1786
- * @file OCR Worker处理模块
1787
- * @description 用于在Web Worker中执行OCR处理
1788
- * @module OCRWorker
1789
- */
1790
- /**
1791
- * 在Web Worker中执行OCR处理的函数
1792
- *
1793
- * 该函数用于在使用 createWorker 创建的 Worker 中执行
1794
- *
1795
- * @param input OCR处理输入数据
1796
- * @returns OCR处理结果
1797
- */
1798
- async function processOCRInWorker(input) {
1799
- // 计时开始
1800
- const startTime = performance.now();
1801
- // 加载Tesseract.js (Worker 环境下动态导入)
1802
- const { createWorker } = await import('tesseract.js');
1803
- // 创建OCR Worker
1804
- const worker = (await createWorker(input.tessWorkerOptions || {
1805
- logger: (m) => console.log(m),
1806
- })); // 添加类型断言,避免TypeScript错误
1807
- try {
1808
- // 初始化OCR引擎
1809
- await worker.load();
1810
- await worker.loadLanguage("chi_sim");
1811
- await worker.initialize("chi_sim");
1812
- await worker.setParameters({
1813
- tessedit_char_whitelist: "0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期",
1814
- });
1815
- // 识别图像
1816
- const { data } = await worker.recognize(input.imageBase64);
1817
- // 解析识别结果
1818
- const idCardInfo = parseIDCardText(data.text);
1819
- // 处理完成后终止worker
1820
- await worker.terminate();
1821
- // 计算处理时间
1822
- const processingTime = performance.now() - startTime;
1823
- // 返回处理结果
1824
- return {
1825
- idCardInfo,
1826
- processingTime,
1827
- };
1828
- }
1829
- catch (error) {
1830
- // 确保资源被释放
1831
- await worker.terminate();
1832
- throw error;
1833
- }
1834
- }
1835
- /**
1836
- * 解析身份证文本信息
1837
- *
1838
- * 从OCR识别到的文本中提取结构化的身份证信息
1839
- *
1840
- * @private
1841
- * @param {string} text - OCR识别到的文本
1842
- * @returns {IDCardInfo} 提取到的身份证信息对象
1843
- */
1844
- function parseIDCardText(text) {
1845
- const info = {};
1846
- // 拆分为行
1847
- const lines = text.split("\n").filter((line) => line.trim());
1848
- // 解析身份证号码(最容易识别的部分)
1849
- const idNumberRegex = /(\d{17}[\dX])/;
1850
- const idNumberMatch = text.match(idNumberRegex);
1851
- if (idNumberMatch) {
1852
- info.idNumber = idNumberMatch[1];
1853
- }
1854
- // 解析姓名
1855
- for (const line of lines) {
1856
- if (line.includes("姓名") ||
1857
- (line.length < 10 && line.length > 1 && !/\d/.test(line))) {
1858
- info.name = line.replace("姓名", "").trim();
1859
- break;
1860
- }
1861
- }
1862
- // 解析性别和民族
1863
- const genderNationalityRegex = /(男|女).*(族)/;
1864
- const genderMatch = text.match(genderNationalityRegex);
1865
- if (genderMatch) {
1866
- info.gender = genderMatch[1];
1867
- const nationalityText = genderMatch[0];
1868
- info.nationality = nationalityText
1869
- .substring(nationalityText.indexOf(genderMatch[1]) + 1)
1870
- .trim();
1871
- }
1872
- // 解析出生日期
1873
- const birthDateRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/;
1874
- const birthDateMatch = text.match(birthDateRegex);
1875
- if (birthDateMatch) {
1876
- info.birthDate = `${birthDateMatch[1]}-${birthDateMatch[2]}-${birthDateMatch[3]}`;
1877
- }
1878
- // 解析地址
1879
- const addressRegex = /住址([\s\S]*?)公民身份号码/;
1880
- const addressMatch = text.match(addressRegex);
1881
- if (addressMatch) {
1882
- info.address = addressMatch[1].replace(/\n/g, "").trim();
1883
- }
1884
- // 解析签发机关
1885
- const authorityRegex = /签发机关([\s\S]*?)有效期/;
1886
- const authorityMatch = text.match(authorityRegex);
1887
- if (authorityMatch) {
1888
- info.issuingAuthority = authorityMatch[1].replace(/\n/g, "").trim();
1889
- }
1890
- // 解析有效期限
1891
- const validPeriodRegex = /有效期限([\s\S]*?)(-|至)/;
1892
- const validPeriodMatch = text.match(validPeriodRegex);
1893
- if (validPeriodMatch) {
1894
- info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim();
1895
- }
1896
- return info;
1897
- }
1898
-
1899
- /**
1900
- * @file OCR处理模块
1901
- * @description 提供身份证文字识别和信息提取功能
1902
- * @module OCRProcessor
1903
- * @version 1.3.2
1904
- */
1905
- /**
1906
- * OCR处理器类
1907
- *
1908
- * 使用Tesseract.js实现对身份证图像的OCR文字识别和信息提取功能
1909
- *
1910
- * @example
1911
- * ```typescript
1912
- * // 创建OCR处理器
1913
- * const ocrProcessor = new OCRProcessor();
1914
- *
1915
- * // 初始化OCR引擎
1916
- * await ocrProcessor.initialize();
1917
- *
1918
- * // 处理身份证图像
1919
- * const idInfo = await ocrProcessor.processIDCard(idCardImageData);
1920
- * console.log('识别到的身份证信息:', idInfo);
1921
- *
1922
- * // 使用结束后释放资源
1923
- * await ocrProcessor.terminate();
1924
- * ```
1925
- */
1926
- class OCRProcessor {
1927
- /**
1928
- * 创建OCR处理器实例
1929
- *
1930
- * @param options OCR处理器选项
1931
- */
1932
- constructor(options = {}) {
1933
- this.worker = null;
1934
- this.ocrWorker = null;
1935
- this.initialized = false;
1936
- this.options = {
1937
- useWorker: isWorkerSupported(),
1938
- enableCache: true,
1939
- cacheSize: 50,
1940
- maxImageDimension: 1000,
1941
- logger: console.log,
1942
- ...options,
1943
- };
1944
- // 初始化缓存
1945
- this.resultCache = new LRUCache(this.options.cacheSize);
1946
- }
1947
- /**
1948
- * 初始化OCR引擎
1949
- *
1950
- * 加载Tesseract OCR引擎和中文简体语言包,并设置适合身份证识别的参数
1951
- *
1952
- * @returns {Promise<void>} 初始化完成的Promise
1953
- */
1954
- async initialize() {
1955
- if (this.initialized)
1956
- return;
1957
- if (this.options.useWorker) {
1958
- // 使用自定义Worker线程处理OCR
1959
- this.ocrWorker = createWorker(processOCRInWorker);
1960
- this.initialized = true;
1961
- this.options.logger?.("OCR Worker 初始化完成");
1962
- }
1963
- else {
1964
- // 使用主线程处理OCR
1965
- this.worker = tesseract_js.createWorker({
1966
- logger: this.options.logger,
1967
- });
1968
- await this.worker.load();
1969
- await this.worker.loadLanguage("chi_sim");
1970
- await this.worker.initialize("chi_sim");
1971
- await this.worker.setParameters({
1972
- tessedit_char_whitelist: "0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期",
1973
- });
1974
- this.initialized = true;
1975
- this.options.logger?.("OCR引擎初始化完成");
1976
- }
1977
- }
1978
- /**
1979
- * 处理身份证图像并提取信息
1980
- * @param imageData 要处理的身份证图像数据
1981
- * @returns 提取的身份证信息
1982
- */
1983
- async processIDCard(imageData) {
1984
- if (!this.initialized) {
1985
- await this.initialize();
1986
- }
1987
- // 计算图像指纹,用于缓存查找
1988
- if (this.options.enableCache) {
1989
- const fingerprint = calculateImageFingerprint(imageData);
1990
- // 检查缓存中是否有结果
1991
- const cachedResult = this.resultCache.get(fingerprint);
1992
- if (cachedResult) {
1993
- this.options.logger?.("使用缓存的OCR结果");
1994
- return cachedResult;
1995
- }
1996
- }
1997
- // 调整图像大小以提高性能和准确性
1998
- const downsampledImage = ImageProcessor.resizeImage(imageData, this.options.maxImageDimension || 1000, this.options.maxImageDimension || 1000, true // 保持宽高比
1999
- );
2000
- // 提高图像质量以获得更好的OCR结果
2001
- const enhancedImage = ImageProcessor.batchProcess(downsampledImage, {
2002
- brightness: this.options.brightness || 15,
2003
- contrast: this.options.contrast || 25,
2004
- sharpen: true,
2005
- });
2006
- // 转换为base64供Tesseract处理
2007
- // 创建一个canvas元素
2008
- const canvas = document.createElement("canvas");
2009
- canvas.width = enhancedImage.width;
2010
- canvas.height = enhancedImage.height;
2011
- const ctx = canvas.getContext("2d");
2012
- if (!ctx) {
2013
- throw new Error("无法创建canvas上下文");
2014
- }
2015
- // 将ImageData绘制到canvas
2016
- ctx.putImageData(enhancedImage, 0, 0);
2017
- // 转换为Base64
2018
- const base64Image = canvas.toDataURL("image/jpeg", 0.7);
2019
- // OCR识别
2020
- try {
2021
- let idCardInfo;
2022
- if (this.options.useWorker && this.ocrWorker) {
2023
- // 使用Worker线程处理
2024
- const result = await this.ocrWorker.postMessage({
2025
- imageBase64: base64Image,
2026
- tessWorkerOptions: {
2027
- logger: this.options.logger,
2028
- },
2029
- });
2030
- idCardInfo = result.idCardInfo;
2031
- this.options.logger?.(`OCR处理完成,用时: ${result.processingTime.toFixed(2)}ms`);
2032
- }
2033
- else {
2034
- // 使用主线程处理
2035
- const startTime = performance.now();
2036
- // 转换ImageData为Canvas
2037
- const canvas = ImageProcessor.imageDataToCanvas(enhancedImage);
2038
- const { data } = await this.worker.recognize(canvas);
2039
- // 解析身份证信息
2040
- idCardInfo = this.parseIDCardText(data.text);
2041
- const processingTime = performance.now() - startTime;
2042
- this.options.logger?.(`OCR处理完成,用时: ${processingTime.toFixed(2)}ms`);
2043
- }
2044
- // 缓存结果
2045
- if (this.options.enableCache) {
2046
- const fingerprint = calculateImageFingerprint(imageData);
2047
- this.resultCache.set(fingerprint, idCardInfo);
2048
- }
2049
- return idCardInfo;
2050
- }
2051
- catch (error) {
2052
- this.options.logger?.(`OCR识别错误: ${error}`);
2053
- return {};
2054
- }
2055
- }
2056
- /**
2057
- * 解析身份证文本信息
2058
- *
2059
- * 从OCR识别到的文本中提取结构化的身份证信息
2060
- *
2061
- * @private
2062
- * @param {string} text - OCR识别到的文本
2063
- * @returns {IDCardInfo} 提取到的身份证信息对象
2064
- */
2065
- /**
2066
- * 格式化日期字符串为标准格式 (YYYY-MM-DD)
2067
- * @param dateStr 原始日期字符串
2068
- * @returns 格式化后的日期字符串
2069
- */
2070
- formatDateString(dateStr) {
2071
- // 先尝试提取年月日
2072
- const dateMatch = dateStr.match(/(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/);
2073
- if (dateMatch) {
2074
- const year = dateMatch[1];
2075
- const month = dateMatch[2].padStart(2, '0');
2076
- const day = dateMatch[3].padStart(2, '0');
2077
- return `${year}-${month}-${day}`;
2078
- }
2079
- // 如果是纯数字格式如 20220101
2080
- if (/^\d{8}$/.test(dateStr)) {
2081
- const year = dateStr.substring(0, 4);
2082
- const month = dateStr.substring(4, 6);
2083
- const day = dateStr.substring(6, 8);
2084
- return `${year}-${month}-${day}`;
2085
- }
2086
- // 如果无法格式化,返回原始字符串
2087
- return dateStr;
2088
- }
2089
- /**
2090
- * 验证身份证号是否符合规则
2091
- * @param idNumber 身份证号
2092
- * @returns 是否有效
2093
- */
2094
- validateIDNumber(idNumber) {
2095
- // 基本验证,校验位有效性和长度
2096
- if (!idNumber || idNumber.length !== 18) {
2097
- return false;
2098
- }
2099
- // 检查格式,前17位必须为数字,最后一位可以是数字或'X'
2100
- const pattern = /^\d{17}[\dX]$/;
2101
- if (!pattern.test(idNumber)) {
2102
- return false;
2103
- }
2104
- // 检查日期部分
2105
- parseInt(idNumber.substr(6, 4));
2106
- const month = parseInt(idNumber.substr(10, 2));
2107
- const day = parseInt(idNumber.substr(12, 2));
2108
- if (month < 1 || month > 12 || day < 1 || day > 31) {
2109
- return false;
2110
- }
2111
- // 更详细的检查可以添加校验位的验证等逻辑...
2112
- return true;
2113
- }
2114
- parseIDCardText(text) {
2115
- const info = {};
2116
- // 预处理文本,清除多余空白
2117
- const processedText = text.replace(/\s+/g, " ").trim();
2118
- // 拆分为行,并过滤空行
2119
- const lines = processedText.split("\n").filter((line) => line.trim());
2120
- // 解析身份证号码 - 多种模式匹配
2121
- // 1. 普通18位身份证号模式
2122
- const idNumberRegex = /(\d{17}[\dX])/;
2123
- // 2. 带前缀的模式
2124
- const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/;
2125
- // 尝试所有模式
2126
- let idNumber = null;
2127
- const basicMatch = processedText.match(idNumberRegex);
2128
- const prefixMatch = processedText.match(idNumberWithPrefixRegex);
2129
- if (prefixMatch && prefixMatch[1]) {
2130
- idNumber = prefixMatch[1]; // 首选带前缀的匹配,因为最可靠
2131
- }
2132
- else if (basicMatch && basicMatch[1]) {
2133
- idNumber = basicMatch[1]; // 其次是常规匹配
2134
- }
2135
- if (idNumber) {
2136
- info.idNumber = idNumber;
2137
- }
2138
- // 解析姓名 - 使用多种策略
2139
- // 1. 直接匹配姓名标签近的内容
2140
- const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/;
2141
- const nameMatch = processedText.match(nameWithLabelRegex);
2142
- // 2. 分析行文本寻找姓名
2143
- if (nameMatch && nameMatch[1]) {
2144
- info.name = nameMatch[1].trim();
2145
- }
2146
- else {
2147
- // 备用方案:查找短行且内容全是汉字
2148
- for (const line of lines) {
2149
- if (line.length >= 2 && line.length <= 5 && /^[一-龥]+$/.test(line) && !/性别|民族|住址|公民|签发|有效/.test(line)) {
2150
- info.name = line.trim();
2151
- break;
2152
- }
2153
- }
2154
- }
2155
- // 解析性别和民族 - 多种模式匹配
2156
- // 1. 标准格式匹配
2157
- const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/;
2158
- const genderNationalityMatch = processedText.match(genderAndNationalityRegex);
2159
- // 2. 只匹配性别
2160
- const genderOnlyRegex = /性别[\s\:]*([男女])/;
2161
- const genderOnlyMatch = processedText.match(genderOnlyRegex);
2162
- // 3. 只匹配民族
2163
- const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/;
2164
- const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex);
2165
- if (genderNationalityMatch) {
2166
- info.gender = genderNationalityMatch[1];
2167
- info.nationality = genderNationalityMatch[2];
2168
- }
2169
- else {
2170
- // 分开获取
2171
- if (genderOnlyMatch)
2172
- info.gender = genderOnlyMatch[1];
2173
- if (nationalityOnlyMatch)
2174
- info.nationality = nationalityOnlyMatch[1];
2175
- }
2176
- // 解析出生日期 - 支持多种格式
2177
- // 1. 标准格式:YYYY年MM月DD日
2178
- const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/;
2179
- // 2. 美式日期格式:YYYY-MM-DD或YYYY/MM/DD
2180
- const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/;
2181
- // 3. 带前缀的格式
2182
- const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/;
2183
- let birthDateMatch = processedText.match(birthDateRegex1) ||
2184
- processedText.match(birthDateRegex2) ||
2185
- processedText.match(birthDateRegex3);
2186
- // 4. 从身份证号码中提取出生日期(如果上述方法失败)
2187
- if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) {
2188
- const year = info.idNumber.substring(6, 10);
2189
- const month = info.idNumber.substring(10, 12);
2190
- const day = info.idNumber.substring(12, 14);
2191
- info.birthDate = `${year}-${month}-${day}`;
2192
- }
2193
- else if (birthDateMatch) {
2194
- // 确保月份和日期是两位数
2195
- const year = birthDateMatch[1];
2196
- const month = birthDateMatch[2].padStart(2, '0');
2197
- const day = birthDateMatch[3].padStart(2, '0');
2198
- info.birthDate = `${year}-${month}-${day}`;
2199
- }
2200
- // 解析地址 - 改进的正则匹配
2201
- // 1. 常规模式
2202
- const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/;
2203
- // 2. 更宽松的模式
2204
- const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/;
2205
- const addressMatch = processedText.match(addressRegex1) || processedText.match(addressRegex2);
2206
- if (addressMatch && addressMatch[1]) {
2207
- // 清理地址中的常见错误和多余空格
2208
- info.address = addressMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim();
2209
- // 限制地址长度并判断地址合理性
2210
- if (info.address.length > 70) {
2211
- info.address = info.address.substring(0, 70);
2212
- }
2213
- // 确保地址是合理的(不仅仅包含符号或数字)
2214
- if (!/[一-龥]/.test(info.address)) {
2215
- info.address = ""; // 如果没有中文字符,可能不是有效地址
2216
- }
2217
- }
2218
- // 解析签发机关
2219
- const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/;
2220
- const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/;
2221
- const authorityMatch = processedText.match(authorityRegex1) || processedText.match(authorityRegex2);
2222
- if (authorityMatch && authorityMatch[1]) {
2223
- info.issuingAuthority = authorityMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim();
2224
- }
2225
- // 解析有效期限 - 支持多种格式
2226
- // 1. 常规格式:YYYY.MM.DD-YYYY.MM.DD
2227
- const validPeriodRegex1 = /有效期限[\s\:]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日\s]*)[-\s]*(至|-)[-\s]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日]*|[永久长期]*)/;
2228
- // 2. 简化格式:YYYYMMDD-YYYYMMDD
2229
- const validPeriodRegex2 = /有效期限[\s\:]*(\d{8})[-\s]*(至|-)[-\s]*(\d{8}|[永久长期]*)/;
2230
- const validPeriodMatch = processedText.match(validPeriodRegex1) || processedText.match(validPeriodRegex2);
2231
- if (validPeriodMatch) {
2232
- // 格式化为统一的有效期限形式
2233
- if (validPeriodMatch[1] && validPeriodMatch[3]) {
2234
- const startDate = this.formatDateString(validPeriodMatch[1]);
2235
- const endDate = /\d/.test(validPeriodMatch[3]) ?
2236
- this.formatDateString(validPeriodMatch[3]) :
2237
- '长期有效';
2238
- info.validPeriod = `${startDate}-${endDate}`;
2239
- }
2240
- else {
2241
- info.validPeriod = validPeriodMatch[0].replace('有效期限', '').trim();
2242
- }
2243
- }
2244
- return info;
2245
- }
2246
- /**
2247
- * 清除结果缓存
2248
- */
2249
- clearCache() {
2250
- this.resultCache.clear();
2251
- this.options.logger?.("OCR结果缓存已清除");
2252
- }
2253
- /**
2254
- * 终止OCR引擎并释放资源
2255
- *
2256
- * @returns {Promise<void>} 终止完成的Promise
2257
- */
2258
- async terminate() {
2259
- if (this.worker) {
2260
- await this.worker.terminate();
2261
- this.worker = null;
2262
- }
2263
- if (this.ocrWorker) {
2264
- this.ocrWorker.terminate();
2265
- this.ocrWorker = null;
2266
- }
2267
- this.initialized = false;
2268
- this.options.logger?.("OCR引擎已终止");
2269
- }
2270
- /**
2271
- * 释放资源
2272
- */
2273
- dispose() {
2274
- return this.terminate();
2275
- }
2276
- }
2277
-
2278
- /**
2279
- * @file 数据提取工具类
2280
- * @description 提供身份证信息的验证和格式化功能
2281
- * @module DataExtractor
2282
- */
2283
- /**
2284
- * 数据提取工具类
2285
- *
2286
- * 提供身份证信息的验证、提取和增强功能,可以从身份证号码中提取出生日期、性别等信息,
2287
- * 并对OCR识别结果进行补充和验证
2288
- *
2289
- * @example
2290
- * ```typescript
2291
- * // 验证身份证号码
2292
- * const isValid = DataExtractor.validateIDNumber('110101199001011234');
2293
- *
2294
- * // 从身份证号码提取出生日期
2295
- * const birthDate = DataExtractor.extractBirthDateFromID('110101199001011234');
2296
- * // 结果: '1990-01-01'
2297
- *
2298
- * // 增强OCR识别结果
2299
- * const enhancedInfo = DataExtractor.enhanceIDCardInfo({
2300
- * name: '张三',
2301
- * idNumber: '110101199001011234'
2302
- * });
2303
- * // 结果会自动补充性别和出生日期
2304
- * ```
2305
- */
2306
- class DataExtractor {
2307
- /**
2308
- * 验证身份证号码格式
2309
- *
2310
- * 检查身份证号码的长度、格式和出生日期部分是否有效
2311
- *
2312
- * @param {string} idNumber - 要验证的身份证号码
2313
- * @returns {boolean} 是否是有效的身份证号码
2314
- */
2315
- static validateIDNumber(idNumber) {
2316
- // 简单校验长度
2317
- if (!idNumber || idNumber.length !== 18) {
2318
- return false;
2319
- }
2320
- // 校验格式 (前17位必须是数字,最后一位可以是数字或X)
2321
- const pattern = /^\d{17}[\dX]$/;
2322
- if (!pattern.test(idNumber)) {
2323
- return false;
2324
- }
2325
- // 校验出生日期
2326
- const year = parseInt(idNumber.substr(6, 4));
2327
- const month = parseInt(idNumber.substr(10, 2));
2328
- const day = parseInt(idNumber.substr(12, 2));
2329
- const date = new Date(year, month - 1, day);
2330
- if (date.getFullYear() !== year ||
2331
- date.getMonth() + 1 !== month ||
2332
- date.getDate() !== day) {
2333
- return false;
2334
- }
2335
- // 简单的校验规则,实际项目中可以加入更完善的验证
2336
- return true;
2337
- }
2338
- /**
2339
- * 从身份证号码提取出生日期
2340
- *
2341
- * @param {string} idNumber - 身份证号码
2342
- * @returns {string|null} 格式化的出生日期(YYYY-MM-DD),如果身份证号码无效则返回null
2343
- */
2344
- static extractBirthDateFromID(idNumber) {
2345
- if (!this.validateIDNumber(idNumber)) {
2346
- return null;
2347
- }
2348
- const year = idNumber.substr(6, 4);
2349
- const month = idNumber.substr(10, 2);
2350
- const day = idNumber.substr(12, 2);
2351
- return `${year}-${month}-${day}`;
2352
- }
2353
- /**
2354
- * 从身份证号码提取性别
2355
- *
2356
- * 根据身份证号码第17位判断性别,奇数为男,偶数为女
2357
- *
2358
- * @param {string} idNumber - 身份证号码
2359
- * @returns {string|null} '男'或'女',如果身份证号码无效则返回null
2360
- */
2361
- static extractGenderFromID(idNumber) {
2362
- if (!this.validateIDNumber(idNumber)) {
2363
- return null;
2364
- }
2365
- // 第17位,奇数为男,偶数为女
2366
- const genderCode = parseInt(idNumber.charAt(16));
2367
- return genderCode % 2 === 1 ? '男' : '女';
2368
- }
2369
- /**
2370
- * 从身份证号码提取地区编码
2371
- *
2372
- * @param {string} idNumber - 身份证号码
2373
- * @returns {string|null} 地区编码(前6位),如果身份证号码无效则返回null
2374
- */
2375
- static extractRegionFromID(idNumber) {
2376
- if (!this.validateIDNumber(idNumber)) {
2377
- return null;
2378
- }
2379
- return idNumber.substr(0, 6);
2380
- }
2381
- /**
2382
- * 合并并优化身份证信息
2383
- *
2384
- * 使用多个来源的数据进行交叉验证和补充,如果OCR识别结果缺少某些信息,
2385
- * 但有身份证号码,则可以从号码中提取出生日期和性别等信息
2386
- *
2387
- * @param {IDCardInfo} ocrInfo - OCR识别到的身份证信息
2388
- * @param {string} [idNumber] - 可选的外部提供的身份证号码,优先级高于OCR识别结果
2389
- * @returns {IDCardInfo} 增强后的身份证信息
2390
- */
2391
- static enhanceIDCardInfo(ocrInfo, idNumber) {
2392
- const result = { ...ocrInfo };
2393
- // 如果OCR识别出身份证号,但没有识别出生日期或性别,则从身份证号码提取
2394
- if (result.idNumber && this.validateIDNumber(result.idNumber)) {
2395
- // 从身份证号提取出生日期
2396
- if (!result.birthDate) {
2397
- result.birthDate = this.extractBirthDateFromID(result.idNumber) || undefined;
2398
- }
2399
- // 从身份证号提取性别
2400
- if (!result.gender) {
2401
- result.gender = this.extractGenderFromID(result.idNumber) || undefined;
2402
- }
2403
- }
2404
- // 如果外部传入了身份证号,则优先使用它并提取信息
2405
- if (idNumber && this.validateIDNumber(idNumber)) {
2406
- result.idNumber = idNumber;
2407
- // 使用身份证号码再次验证或补充信息
2408
- const birthDate = this.extractBirthDateFromID(idNumber);
2409
- if (birthDate) {
2410
- result.birthDate = birthDate;
2411
- }
2412
- const gender = this.extractGenderFromID(idNumber);
2413
- if (gender) {
2414
- result.gender = gender;
2415
- }
2416
- }
2417
- return result;
2418
- }
2419
- /**
2420
- * 提取并验证身份证信息
2421
- *
2422
- * @param idCardInfo 初步提取的身份证信息
2423
- * @returns 验证和增强后的身份证信息
2424
- */
2425
- extractAndValidate(idCardInfo) {
2426
- const enhancedInfo = { ...idCardInfo };
2427
- // 验证和规范化身份证号
2428
- if (enhancedInfo.idNumber) {
2429
- enhancedInfo.idNumber = this.normalizeIDNumber(enhancedInfo.idNumber);
2430
- // 如果身份证号有效,推断出生日期
2431
- if (this.validateIDNumber(enhancedInfo.idNumber)) {
2432
- if (!enhancedInfo.birthDate) {
2433
- enhancedInfo.birthDate = this.extractBirthDateFromID(enhancedInfo.idNumber);
2434
- }
2435
- // 推断性别
2436
- if (!enhancedInfo.gender) {
2437
- enhancedInfo.gender = this.extractGenderFromID(enhancedInfo.idNumber);
2438
- }
2439
- }
2440
- }
2441
- // 规范化日期格式
2442
- if (enhancedInfo.birthDate) {
2443
- enhancedInfo.birthDate = this.normalizeDate(enhancedInfo.birthDate);
2444
- }
2445
- // 规范化地址信息
2446
- if (enhancedInfo.address) {
2447
- enhancedInfo.address = this.normalizeAddress(enhancedInfo.address);
2448
- }
2449
- return enhancedInfo;
2450
- }
2451
- /**
2452
- * 规范化身份证号码
2453
- */
2454
- normalizeIDNumber(idNumber) {
2455
- // 移除空格和特殊字符
2456
- return idNumber.replace(/[\s\-]/g, '').toUpperCase();
2457
- }
2458
- /**
2459
- * 验证身份证号码是否有效
2460
- */
2461
- validateIDNumber(idNumber) {
2462
- // 简单验证身份证号码长度和格式
2463
- const idRegex = /(^\d{15}$)|(^\d{17}([0-9]|X)$)/;
2464
- return idRegex.test(idNumber);
2465
- }
2466
- /**
2467
- * 从身份证号中提取出生日期
2468
- */
2469
- extractBirthDateFromID(idNumber) {
2470
- if (idNumber.length === 18) {
2471
- return `${idNumber.substring(6, 10)}-${idNumber.substring(10, 12)}-${idNumber.substring(12, 14)}`;
2472
- }
2473
- else if (idNumber.length === 15) {
2474
- return `19${idNumber.substring(6, 8)}-${idNumber.substring(8, 10)}-${idNumber.substring(10, 12)}`;
2475
- }
2476
- return '';
2477
- }
2478
- /**
2479
- * 从身份证号中提取性别信息
2480
- */
2481
- extractGenderFromID(idNumber) {
2482
- let sexCode;
2483
- if (idNumber.length === 18) {
2484
- sexCode = parseInt(idNumber.charAt(16));
2485
- }
2486
- else {
2487
- sexCode = parseInt(idNumber.charAt(14));
2488
- }
2489
- return sexCode % 2 === 1 ? '男' : '女';
2490
- }
2491
- /**
2492
- * 规范化日期格式
2493
- */
2494
- normalizeDate(date) {
2495
- // 简单的日期格式化逻辑
2496
- return date.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
2497
- }
2498
- /**
2499
- * 规范化地址信息
2500
- */
2501
- normalizeAddress(address) {
2502
- // 地址格式化逻辑
2503
- return address.trim();
2504
- }
2505
- }
2506
-
2507
- /**
2508
- * @file 身份证防伪检测模块
2509
- * @description 提供身份证防伪特征识别功能,区分真假身份证
2510
- * @module AntiFakeDetector
2511
- * @version 1.3.2
2512
- */
2513
- /**
2514
- * 身份证防伪特征检测器
2515
- *
2516
- * 基于图像分析技术检测身份证中的多种防伪特征,包括:
2517
- * 1. 荧光油墨特征
2518
- * 2. 微缩文字
2519
- * 3. 光变图案
2520
- * 4. 雕刻凹印
2521
- * 5. 隐形图案
2522
- *
2523
- * @example
2524
- * ```typescript
2525
- * // 创建防伪检测器
2526
- * const antiFakeDetector = new AntiFakeDetector({
2527
- * sensitivity: 0.8,
2528
- * enableCache: true
2529
- * });
2530
- *
2531
- * // 分析身份证图像
2532
- * const imageData = await ImageProcessor.createImageDataFromFile(idCardFile);
2533
- * const result = await antiFakeDetector.detect(imageData);
2534
- *
2535
- * if (result.isAuthentic) {
2536
- * console.log('身份证真实,检测到防伪特征:', result.detectedFeatures);
2537
- * } else {
2538
- * console.log('警告!', result.message);
2539
- * }
2540
- * ```
2541
- */
2542
- class AntiFakeDetector {
2543
- /**
2544
- * 创建身份证防伪检测器实例
2545
- *
2546
- * @param options 防伪检测器配置
2547
- */
2548
- constructor(options = {}) {
2549
- this.options = {
2550
- sensitivity: 0.7,
2551
- enableCache: true,
2552
- cacheSize: 50,
2553
- logger: console.log,
2554
- ...options,
2555
- };
2556
- // 初始化缓存
2557
- this.resultCache = new LRUCache(this.options.cacheSize);
2558
- }
2559
- /**
2560
- * 检测身份证图像的防伪特征
2561
- *
2562
- * @param imageData 身份证图像数据
2563
- * @returns 防伪检测结果
2564
- */
2565
- async detect(imageData) {
2566
- const startTime = performance.now();
2567
- // 检查缓存
2568
- if (this.options.enableCache) {
2569
- const fingerprint = calculateImageFingerprint(imageData);
2570
- const cachedResult = this.resultCache.get(fingerprint);
2571
- if (cachedResult) {
2572
- this.options.logger("使用缓存的防伪检测结果");
2573
- return cachedResult;
2574
- }
2575
- }
2576
- // 图像预处理增强防伪特征
2577
- const enhancedImage = this.enhanceAntiFakeFeatures(imageData);
2578
- // 执行多种防伪特征检测
2579
- const featureResults = await Promise.all([
2580
- this.detectUVInkFeatures(enhancedImage),
2581
- this.detectMicroText(enhancedImage),
2582
- this.detectOpticalVariable(enhancedImage),
2583
- this.detectIntaglioPrinting(enhancedImage),
2584
- this.detectGhostImage(enhancedImage),
2585
- ]);
2586
- // 汇总检测结果
2587
- const detectedFeatures = [];
2588
- let totalConfidence = 0;
2589
- for (const [feature, detected, confidence] of featureResults) {
2590
- if (detected && confidence > 0.5) {
2591
- detectedFeatures.push(feature);
2592
- totalConfidence += confidence;
2593
- }
2594
- }
2595
- // 计算最终结果
2596
- const normalizedConfidence = featureResults.length > 0 ? totalConfidence / featureResults.length : 0;
2597
- // 根据敏感度和检测到的特征决定是否通过验证
2598
- const isAuthentic = normalizedConfidence >= this.options.sensitivity &&
2599
- detectedFeatures.length >= 2;
2600
- // 生成结果消息
2601
- let message = isAuthentic
2602
- ? `身份证真实,检测到${detectedFeatures.length}个防伪特征`
2603
- : detectedFeatures.length > 0
2604
- ? `可疑身份证,仅检测到${detectedFeatures.length}个防伪特征,置信度不足`
2605
- : "未检测到有效防伪特征,可能为伪造证件";
2606
- const result = {
2607
- isAuthentic,
2608
- confidence: normalizedConfidence,
2609
- detectedFeatures,
2610
- message,
2611
- processingTime: performance.now() - startTime,
2612
- };
2613
- // 缓存结果
2614
- if (this.options.enableCache) {
2615
- const fingerprint = calculateImageFingerprint(imageData);
2616
- this.resultCache.set(fingerprint, result);
2617
- }
2618
- return result;
2619
- }
2620
- /**
2621
- * 增强身份证图像中的防伪特征
2622
- *
2623
- * @param imageData 原始图像数据
2624
- * @returns 增强后的图像数据
2625
- * @private
2626
- */
2627
- enhanceAntiFakeFeatures(imageData) {
2628
- // 应用特定的图像处理增强防伪特征
2629
- return ImageProcessor.batchProcess(imageData, {
2630
- contrast: 30, // 增强对比度
2631
- brightness: 10, // 轻微提高亮度
2632
- sharpen: true, // 锐化图像突出细节
2633
- });
2634
- }
2635
- /**
2636
- * 检测荧光油墨特征
2637
- *
2638
- * @param imageData 图像数据
2639
- * @returns [特征名称, 是否检测到, 置信度]
2640
- * @private
2641
- */
2642
- async detectUVInkFeatures(imageData) {
2643
- // 在真实身份证上,荧光油墨会在特定反光条件下呈现特定颜色特征
2644
- // 在普通可见光下,我们分析蓝色和紫外色通道分布特征
2645
- // 1. 提取蓝色通道并增强对比度
2646
- const blueChannel = this.extractColorChannel(imageData, 'blue');
2647
- // 2. 分析蓝色通道的分布特征
2648
- const { peaks, variance } = this.analyzeChannelDistribution(blueChannel);
2649
- // 3. 分析特定区域的颜色模式
2650
- const patternScore = this.detectUVColorPattern(imageData);
2651
- // 4. 计算综合得分
2652
- // 特征分析:荧光油墨在蓝色通道通常有显著峰值,且分布更聚集
2653
- let score = 0;
2654
- // 过多的峰值表明可能是真实身份证上的荧光特征
2655
- if (peaks > 3 && peaks < 10) {
2656
- score += 0.4;
2657
- }
2658
- // 方差越大,表示颜色对比度越高,更可能有荧光特征
2659
- if (variance > 1000) {
2660
- score += 0.3;
2661
- }
2662
- // 颜色模式得分
2663
- score += patternScore * 0.3;
2664
- // 重要区域分析
2665
- // 身份证头像区域通常不应具有荧光特征
2666
- const hasPortraitAreaFeatures = this.analyzePortraitArea(imageData);
2667
- if (hasPortraitAreaFeatures) {
2668
- // 头像区域不应该有荧光特征,如果有可能是伪造的
2669
- score -= 0.2;
2670
- }
2671
- // 求出最终分数并限制在[0,1]范围内
2672
- const confidence = Math.max(0, Math.min(1, score));
2673
- const detected = confidence > 0.55;
2674
- return ["荧光油墨", detected, confidence];
2675
- }
2676
- /**
2677
- * 从图像数据中提取指定颜色通道
2678
- * @param imageData 原始图像数据
2679
- * @param channel 通道名称(red, green, blue)
2680
- */
2681
- extractColorChannel(imageData, channel) {
2682
- const { data, width, height } = imageData;
2683
- const channelOffset = channel === 'red' ? 0 : channel === 'green' ? 1 : 2;
2684
- const channelData = new Uint8ClampedArray(width * height);
2685
- for (let i = 0; i < data.length; i += 4) {
2686
- const pixelIndex = i / 4;
2687
- channelData[pixelIndex] = data[i + channelOffset];
2688
- }
2689
- return channelData;
2690
- }
2691
- /**
2692
- * 分析颜色通道分布特征
2693
- * @param channelData 颜色通道数据
2694
- */
2695
- analyzeChannelDistribution(channelData) {
2696
- // 计算直方图
2697
- const histogram = new Array(256).fill(0);
2698
- for (let i = 0; i < channelData.length; i++) {
2699
- histogram[channelData[i]]++;
2700
- }
2701
- // 平滑直方图以减少噪声
2702
- const smoothedHistogram = this.smoothHistogram(histogram, 3);
2703
- // 计算峰值数量
2704
- let peaks = 0;
2705
- for (let i = 1; i < 255; i++) {
2706
- if (smoothedHistogram[i] > smoothedHistogram[i - 1] &&
2707
- smoothedHistogram[i] > smoothedHistogram[i + 1] &&
2708
- smoothedHistogram[i] > channelData.length * 0.01) { // 只计算显著峰值
2709
- peaks++;
2710
- }
2711
- }
2712
- // 计算方差
2713
- let mean = 0;
2714
- for (let i = 0; i < channelData.length; i++) {
2715
- mean += channelData[i];
2716
- }
2717
- mean /= channelData.length;
2718
- let variance = 0;
2719
- for (let i = 0; i < channelData.length; i++) {
2720
- variance += Math.pow(channelData[i] - mean, 2);
2721
- }
2722
- variance /= channelData.length;
2723
- return { peaks, variance };
2724
- }
2725
- /**
2726
- * 平滑直方图以减少噪声
2727
- */
2728
- smoothHistogram(histogram, windowSize) {
2729
- const result = new Array(histogram.length).fill(0);
2730
- const halfWindow = Math.floor(windowSize / 2);
2731
- for (let i = 0; i < histogram.length; i++) {
2732
- let sum = 0;
2733
- let count = 0;
2734
- for (let j = Math.max(0, i - halfWindow); j <= Math.min(histogram.length - 1, i + halfWindow); j++) {
2735
- sum += histogram[j];
2736
- count++;
2737
- }
2738
- result[i] = sum / count;
2739
- }
2740
- return result;
2741
- }
2742
- /**
2743
- * 检测图像中的荧光颜色模式
2744
- */
2745
- detectUVColorPattern(imageData) {
2746
- // 分析特定组合颜色的出现频率,荧光油墨在可见光下也具有特定的颜色特征
2747
- const { data, width, height } = imageData;
2748
- let uvColorCount = 0;
2749
- // 寻找可能为荧光油墨的特定颜色模式
2750
- // 这些颜色通常是特定的蓝紫色调和高对比度
2751
- for (let i = 0; i < data.length; i += 4) {
2752
- const r = data[i];
2753
- const g = data[i + 1];
2754
- const b = data[i + 2];
2755
- // 检查是否是荧光油墨特有的颜色范围
2756
- // 这里使用简化的追踪条件,实际应用中应使用更复杂的颜色模型
2757
- if (b > 1.5 * r && b > 1.3 * g && b > 100) {
2758
- uvColorCount++;
2759
- }
2760
- }
2761
- // 计算荧光颜色像素占比
2762
- const totalPixels = width * height;
2763
- const uvColorRatio = uvColorCount / totalPixels;
2764
- // 对于真实身份证,荧光颜色的占比应该在一定范围内
2765
- // 如果占比过高或过低,可能是伪造的
2766
- const idealRatio = 0.05; // 理想占比
2767
- const deviation = Math.abs(uvColorRatio - idealRatio) / idealRatio;
2768
- // 将差异转换为0-1的置信度分数
2769
- return Math.max(0, 1 - Math.min(1, deviation * 2));
2770
- }
2771
- /**
2772
- * 分析头像区域是否存在荧光特征
2773
- * 这个方法用于检测伪造的身份证,因为头像区域不应该有荧光特征
2774
- */
2775
- analyzePortraitArea(imageData) {
2776
- // 假设头像区域大约占据图片右上方四分之一的区域
2777
- const { width, height, data } = imageData;
2778
- const portraitX = Math.floor(width * 0.6);
2779
- const portraitY = Math.floor(height * 0.2);
2780
- const portraitWidth = Math.floor(width * 0.3);
2781
- const portraitHeight = Math.floor(height * 0.3);
2782
- let uvFeatureCount = 0;
2783
- let totalPixels = 0;
2784
- // 检查头像区域的荧光特征
2785
- for (let y = portraitY; y < portraitY + portraitHeight; y++) {
2786
- for (let x = portraitX; x < portraitX + portraitWidth; x++) {
2787
- if (x >= 0 && x < width && y >= 0 && y < height) {
2788
- const i = (y * width + x) * 4;
2789
- const r = data[i];
2790
- const g = data[i + 1];
2791
- const b = data[i + 2];
2792
- // 使用与上面相同的荧光颜色检测标准
2793
- if (b > 1.5 * r && b > 1.3 * g && b > 100) {
2794
- uvFeatureCount++;
2795
- }
2796
- totalPixels++;
2797
- }
2798
- }
2799
- }
2800
- // 如果头像区域的荧光特征占比过高,可能是伪造的
2801
- return totalPixels > 0 && (uvFeatureCount / totalPixels) > 0.1;
2802
- }
2803
- /**
2804
- * 检测微缩文字
2805
- *
2806
- * @param imageData 图像数据
2807
- * @returns [特征名称, 是否检测到, 置信度]
2808
- * @private
2809
- */
2810
- async detectMicroText(imageData) {
2811
- // 微缩文字检测 - 身份证上的微缩文字是重要的防伪特征
2812
- // 这些文字很小,但会呈现规则的线条和高频组件
2813
- // 1. 转换图像为灰度图
2814
- const grayscale = ImageProcessor.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
2815
- // 2. 执行边缘检测突出微缩文字
2816
- const edgeData = ImageProcessor.detectEdges(grayscale, 40); // 强化的边缘检测
2817
- // 3. 分析频率特征 - 微缩文字呈现高频的边缘过渡
2818
- const frequencyFeatures = this.analyzeFrequencyFeatures(edgeData);
2819
- // 4. 检测微缩文字的具体区域
2820
- const microTextRegions = this.detectMicroTextRegions(edgeData);
2821
- // 5. 综合分析结果计算置信度
2822
- let score = 0;
2823
- // 频率特征分数
2824
- score += frequencyFeatures.score * 0.6;
2825
- // 区域特征分数
2826
- if (microTextRegions.count > 0) {
2827
- // 过多的区域也可能表示噪声,因此有一个最佳范围
2828
- const normalizedCount = Math.min(microTextRegions.count, 5) / 5;
2829
- score += normalizedCount * 0.4;
2830
- }
2831
- // 对置信度进行最终调整
2832
- const confidence = Math.max(0, Math.min(1, score));
2833
- const detected = confidence > 0.5;
2834
- return ["微缩文字", detected, confidence];
2835
- }
2836
- /**
2837
- * 分析边缘图像的频率特征
2838
- * 微缩文字呈现高频的边缘过渡
2839
- */
2840
- analyzeFrequencyFeatures(edgeData) {
2841
- const { data, width, height } = edgeData;
2842
- // 计算高频边缘分布
2843
- // 统计边缘过渡的变化频率
2844
- let highFreqTransitions = 0;
2845
- // 检测行方向的边缘变化
2846
- for (let y = 0; y < height; y++) {
2847
- let prevEdge = false;
2848
- let transitions = 0;
2849
- for (let x = 0; x < width; x++) {
2850
- const i = (y * width + x) * 4;
2851
- const isEdge = data[i] > 200;
2852
- if (isEdge !== prevEdge) {
2853
- transitions++;
2854
- prevEdge = isEdge;
2855
- }
2856
- }
2857
- // 每行的过渡频率
2858
- if (transitions > width * 0.1) { // 高频过渡行
2859
- highFreqTransitions++;
2860
- }
2861
- }
2862
- // 计算列方向的边缘变化
2863
- let colHighFreqTransitions = 0;
2864
- for (let x = 0; x < width; x++) {
2865
- let prevEdge = false;
2866
- let transitions = 0;
2867
- for (let y = 0; y < height; y++) {
2868
- const i = (y * width + x) * 4;
2869
- const isEdge = data[i] > 200;
2870
- if (isEdge !== prevEdge) {
2871
- transitions++;
2872
- prevEdge = isEdge;
2873
- }
2874
- }
2875
- // 每列的过渡频率
2876
- if (transitions > height * 0.1) { // 高频过渡列
2877
- colHighFreqTransitions++;
2878
- }
2879
- }
2880
- // 综合计算高频特征比例
2881
- const rowHighFreqRatio = highFreqTransitions / height;
2882
- const colHighFreqRatio = colHighFreqTransitions / width;
2883
- const highFreqRatio = (rowHighFreqRatio + colHighFreqRatio) / 2;
2884
- // 计算最终分数
2885
- // 真实的微缩文字应该有适度的高频特征,而不是极端的高或低
2886
- const idealRatio = 0.15; // 理想的高频比例
2887
- const deviationFactor = Math.abs(highFreqRatio - idealRatio) / idealRatio;
2888
- const score = Math.max(0, 1 - Math.min(1, deviationFactor * 3));
2889
- return { score, highFreqRatio };
2890
- }
2891
- /**
2892
- * 检测微缩文字区域
2893
- * 微缩文字通常呈现呈现规则的组合排列
2894
- */
2895
- detectMicroTextRegions(edgeData) {
2896
- const { data, width, height } = edgeData;
2897
- const visitedMap = new Array(width * height).fill(false);
2898
- const regions = [];
2899
- // 使用满足条件的连通区域寻找微缩文字区域
2900
- for (let y = 0; y < height; y++) {
2901
- for (let x = 0; x < width; x++) {
2902
- const idx = y * width + x;
2903
- const i = idx * 4;
2904
- // 如果是边缘像素且未访问过
2905
- if (data[i] > 200 && !visitedMap[idx]) {
2906
- // 使用深度优先搜索找到连通的边缘区域
2907
- const regionPoints = this.floodFillEdge(edgeData, x, y, visitedMap);
2908
- // 分析区域
2909
- if (regionPoints.length > 10) { // 小区域忽略
2910
- const [minX, minY, maxX, maxY] = this.getBoundingBox(regionPoints);
2911
- const regionWidth = maxX - minX + 1;
2912
- const regionHeight = maxY - minY + 1;
2913
- // 检查区域大小和纹理特征
2914
- if (regionWidth > 5 && regionHeight > 5 &&
2915
- regionWidth < width * 0.2 && regionHeight < height * 0.2) {
2916
- // 计算区域密度
2917
- const density = regionPoints.length / (regionWidth * regionHeight);
2918
- // 检查并添加符合微缩文字特征的区域
2919
- if (density > 0.1 && density < 0.5) { // 合适的密度范围
2920
- regions.push({
2921
- x: minX,
2922
- y: minY,
2923
- w: regionWidth,
2924
- h: regionHeight
2925
- });
2926
- }
2927
- }
2928
- }
2929
- }
2930
- }
2931
- }
2932
- return { count: regions.length, regions };
2933
- }
2934
- /**
2935
- * 深度优先搜索连通的边缘区域
2936
- */
2937
- floodFillEdge(edgeData, startX, startY, visitedMap) {
2938
- const { data, width, height } = edgeData;
2939
- const stack = [];
2940
- const points = [];
2941
- const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
2942
- const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
2943
- // 起始点
2944
- stack.push({ x: startX, y: startY });
2945
- visitedMap[startY * width + startX] = true;
2946
- while (stack.length > 0) {
2947
- const { x, y } = stack.pop();
2948
- points.push({ x, y });
2949
- // 检查88个相邻方向
2950
- for (let i = 0; i < 8; i++) {
2951
- const nx = x + dx[i];
2952
- const ny = y + dy[i];
2953
- if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
2954
- const nidx = ny * width + nx;
2955
- const ni = nidx * 4;
2956
- if (data[ni] > 200 && !visitedMap[nidx]) {
2957
- stack.push({ x: nx, y: ny });
2958
- visitedMap[nidx] = true;
2959
- }
2960
- }
2961
- }
2962
- }
2963
- return points;
2964
- }
2965
- /**
2966
- * 获取点集的外接矩形
2967
- */
2968
- getBoundingBox(points) {
2969
- let minX = Number.MAX_SAFE_INTEGER;
2970
- let minY = Number.MAX_SAFE_INTEGER;
2971
- let maxX = 0;
2972
- let maxY = 0;
2973
- for (const { x, y } of points) {
2974
- minX = Math.min(minX, x);
2975
- minY = Math.min(minY, y);
2976
- maxX = Math.max(maxX, x);
2977
- maxY = Math.max(maxY, y);
2978
- }
2979
- return [minX, minY, maxX, maxY];
2980
- }
2981
- /**
2982
- * 检测光变图案
2983
- *
2984
- * @param imageData 图像数据
2985
- * @returns [特征名称, 是否检测到, 置信度]
2986
- * @private
2987
- */
2988
- async detectOpticalVariable(imageData) {
2989
- // 提取特定区域并分析颜色变化
2990
- // 在实际实现中需要定位光变图案区域并分析其特征
2991
- // 这里使用模拟实现
2992
- // 模拟检测: 65%的概率检测到,置信度0.6-0.9
2993
- const detected = Math.random() > 0.35;
2994
- const confidence = detected ? 0.6 + Math.random() * 0.3 : 0;
2995
- return ["光变图案", detected, confidence];
2996
- }
2997
- /**
2998
- * 检测凹印雕刻特征
2999
- *
3000
- * @param imageData 图像数据
3001
- * @returns [特征名称, 是否检测到, 置信度]
3002
- * @private
3003
- */
3004
- async detectIntaglioPrinting(imageData) {
3005
- // 使用特定滤镜增强凹印效果
3006
- // 在实际实现中应分析阴影和纹理模式
3007
- // 这里使用模拟实现
3008
- // 模拟检测: 75%的概率检测到,置信度0.65-0.9
3009
- const detected = Math.random() > 0.25;
3010
- const confidence = detected ? 0.65 + Math.random() * 0.25 : 0;
3011
- return ["雕刻凹印", detected, confidence];
3012
- }
3013
- /**
3014
- * 检测隐形图案(幽灵图像)
3015
- *
3016
- * @param imageData 图像数据
3017
- * @returns [特征名称, 是否检测到, 置信度]
3018
- * @private
3019
- */
3020
- async detectGhostImage(imageData) {
3021
- // 调整对比度和亮度显现隐形图案
3022
- // 在实际实现中应使用特定滤镜和图像处理算法
3023
- // 这里使用模拟实现
3024
- // 模拟检测: 60%的概率检测到,置信度0.55-0.85
3025
- const detected = Math.random() > 0.4;
3026
- const confidence = detected ? 0.55 + Math.random() * 0.3 : 0;
3027
- return ["隐形图案", detected, confidence];
3028
- }
3029
- /**
3030
- * 清除结果缓存
3031
- */
3032
- clearCache() {
3033
- this.resultCache.clear();
3034
- this.options.logger("防伪检测结果缓存已清除");
3035
- }
3036
- /**
3037
- * 释放资源
3038
- */
3039
- dispose() {
3040
- this.resultCache.clear();
3041
- }
3042
- }
3043
-
3044
- /**
3045
- * @file ID扫描识别库主入口文件
3046
- * @description 提供身份证识别与二维码、条形码扫描功能的纯前端TypeScript库
3047
- * @module IDScannerLib
3048
- * @version 1.3.0
3049
- * @license MIT
3050
- */
3051
- /**
3052
- * IDScanner 主类
3053
- *
3054
- * 整合二维码、条形码扫描和身份证识别功能,提供统一的接口
3055
- * 使用动态导入实现按需加载
3056
- */
3057
- let IDScanner$1 = class IDScanner {
3058
- /**
3059
- * 构造函数
3060
- * @param options 配置选项
3061
- */
3062
- constructor(options = {}) {
3063
- this.options = options;
3064
- this.scanMode = "qr";
3065
- this.videoElement = null;
3066
- this.scanning = false;
3067
- this.qrModule = null;
3068
- this.ocrModule = null;
3069
- this.scanTimer = null;
3070
- this.isQRModuleLoaded = false;
3071
- this.isOCRModuleLoaded = false;
3072
- // 新增防伪检测器
3073
- this.antiFakeDetector = null;
3074
- this.isAntiFakeModuleLoaded = false;
3075
- this.camera = new Camera(options.cameraOptions);
3076
- }
3077
- /**
3078
- * 初始化模块
3079
- * 根据需要初始化OCR引擎和防伪检测模块
3080
- */
3081
- async initialize() {
3082
- try {
3083
- // 预加载OCR模块但不初始化
3084
- if (!this.isOCRModuleLoaded) {
3085
- // 动态导入OCR模块
3086
- const OCRModule = await Promise.resolve().then(function () { return ocrModule; }).then((m) => m.OCRModule);
3087
- this.ocrModule = new OCRModule({
3088
- cameraOptions: this.options.cameraOptions,
3089
- onIDCardScanned: this.options.onIDCardScanned,
3090
- onError: this.options.onError,
3091
- });
3092
- this.isOCRModuleLoaded = true;
3093
- // 初始化OCR模块
3094
- await this.ocrModule.initialize();
3095
- }
3096
- // 初始化防伪检测模块
3097
- if (!this.isAntiFakeModuleLoaded) {
3098
- this.antiFakeDetector = new AntiFakeDetector();
3099
- this.isAntiFakeModuleLoaded = true;
3100
- }
3101
- console.log("IDScanner初始化完成");
3102
- }
3103
- catch (error) {
3104
- console.error("初始化失败:", error);
3105
- this.handleError(error);
3106
- throw error;
3107
- }
3108
- }
3109
- /**
3110
- * 初始化OCR模块
3111
- */
3112
- async initOCRModule() {
3113
- if (this.isOCRModuleLoaded)
3114
- return;
3115
- try {
3116
- // 动态导入OCR模块
3117
- const OCRModule = await Promise.resolve().then(function () { return ocrModule; }).then((m) => m.OCRModule);
3118
- this.ocrModule = new OCRModule({
3119
- cameraOptions: this.options.cameraOptions,
3120
- onIDCardScanned: this.options.onIDCardScanned,
3121
- onError: this.options.onError,
3122
- });
3123
- this.isOCRModuleLoaded = true;
3124
- // 初始化OCR模块
3125
- await this.ocrModule.initialize();
3126
- }
3127
- catch (error) {
3128
- console.error("OCR模块初始化失败:", error);
3129
- throw error;
3130
- }
3131
- }
3132
- /**
3133
- * 启动二维码扫描
3134
- * @param videoElement HTML视频元素
3135
- */
3136
- async startQRScanner(videoElement) {
3137
- this.stop();
3138
- this.videoElement = videoElement;
3139
- this.scanMode = "qr";
3140
- try {
3141
- // 动态加载二维码模块
3142
- if (!this.isQRModuleLoaded) {
3143
- const ScannerModule = await Promise.resolve().then(function () { return qrModule; }).then((m) => m.ScannerModule);
3144
- this.qrModule = new ScannerModule({
3145
- cameraOptions: this.options.cameraOptions,
3146
- qrScannerOptions: this.options.qrScannerOptions,
3147
- barcodeScannerOptions: this.options.barcodeScannerOptions,
3148
- onQRCodeScanned: this.options.onQRCodeScanned,
3149
- onBarcodeScanned: this.options.onBarcodeScanned,
3150
- onError: this.options.onError,
3151
- });
3152
- this.isQRModuleLoaded = true;
3153
- }
3154
- await this.qrModule.startQRScanner(videoElement);
3155
- }
3156
- catch (error) {
3157
- this.handleError(error);
3158
- }
3159
- }
3160
- /**
3161
- * 启动条形码扫描
3162
- * @param videoElement HTML视频元素
3163
- */
3164
- async startBarcodeScanner(videoElement) {
3165
- this.stop();
3166
- this.videoElement = videoElement;
3167
- this.scanMode = "barcode";
3168
- try {
3169
- // 动态加载二维码模块
3170
- if (!this.isQRModuleLoaded) {
3171
- const ScannerModule = await Promise.resolve().then(function () { return qrModule; }).then((m) => m.ScannerModule);
3172
- this.qrModule = new ScannerModule({
3173
- cameraOptions: this.options.cameraOptions,
3174
- qrScannerOptions: this.options.qrScannerOptions,
3175
- barcodeScannerOptions: this.options.barcodeScannerOptions,
3176
- onQRCodeScanned: this.options.onQRCodeScanned,
3177
- onBarcodeScanned: this.options.onBarcodeScanned,
3178
- onError: this.options.onError,
3179
- });
3180
- this.isQRModuleLoaded = true;
3181
- }
3182
- await this.qrModule.startBarcodeScanner(videoElement);
3183
- }
3184
- catch (error) {
3185
- this.handleError(error);
3186
- }
3187
- }
3188
- /**
3189
- * 启动身份证扫描
3190
- * @param videoElement HTML视频元素
3191
- */
3192
- async startIDCardScanner(videoElement) {
3193
- this.stop();
3194
- this.videoElement = videoElement;
3195
- this.scanMode = "idcard";
3196
- try {
3197
- // 检查OCR模块是否已加载,若未加载则自动初始化
3198
- if (!this.isOCRModuleLoaded) {
3199
- await this.initialize();
3200
- }
3201
- await this.ocrModule.startIDCardScanner(videoElement);
3202
- }
3203
- catch (error) {
3204
- this.handleError(error);
3205
- }
3206
- }
3207
- /**
3208
- * 停止扫描
3209
- */
3210
- stop() {
3211
- if (this.scanMode === "qr" || this.scanMode === "barcode") {
3212
- if (this.qrModule) {
3213
- this.qrModule.stop();
3214
- }
3215
- }
3216
- else if (this.scanMode === "idcard") {
3217
- if (this.ocrModule) {
3218
- this.ocrModule.stop();
3219
- }
3220
- }
3221
- }
3222
- /**
3223
- * 处理错误
3224
- */
3225
- handleError(error) {
3226
- if (this.options.onError) {
3227
- this.options.onError(error);
3228
- }
3229
- else {
3230
- console.error("IDScanner错误:", error);
3231
- }
3232
- }
3233
- /**
3234
- * 释放资源
3235
- */
3236
- async terminate() {
3237
- this.stop();
3238
- // 释放OCR资源
3239
- if (this.isOCRModuleLoaded && this.ocrModule) {
3240
- await this.ocrModule.terminate();
3241
- this.ocrModule = null;
3242
- this.isOCRModuleLoaded = false;
3243
- }
3244
- // 释放QR扫描资源
3245
- if (this.isQRModuleLoaded && this.qrModule) {
3246
- this.qrModule = null;
3247
- this.isQRModuleLoaded = false;
3248
- }
3249
- // 释放防伪检测资源
3250
- if (this.antiFakeDetector) {
3251
- this.antiFakeDetector.dispose();
3252
- this.antiFakeDetector = null;
3253
- this.isAntiFakeModuleLoaded = false;
3254
- }
3255
- }
3256
- /**
3257
- * 处理图片中的二维码
3258
- * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
3259
- * @returns 返回Promise,解析为扫描结果
3260
- */
3261
- async processQRCodeImage(imageSource) {
3262
- try {
3263
- // 动态加载二维码模块
3264
- if (!this.isQRModuleLoaded) {
3265
- const ScannerModule = await Promise.resolve().then(function () { return qrModule; }).then((m) => m.ScannerModule);
3266
- this.qrModule = new ScannerModule({
3267
- qrScannerOptions: this.options.qrScannerOptions,
3268
- onQRCodeScanned: this.options.onQRCodeScanned,
3269
- onError: this.options.onError,
3270
- });
3271
- this.isQRModuleLoaded = true;
3272
- }
3273
- // 处理不同类型的图片源
3274
- let imageElement;
3275
- if (imageSource instanceof File) {
3276
- // 如果是File对象,创建新的Image元素并加载图片
3277
- imageElement = new Image();
3278
- imageElement.crossOrigin = "anonymous"; // 处理跨域图片
3279
- const url = URL.createObjectURL(imageSource);
3280
- await new Promise((resolve, reject) => {
3281
- imageElement.onload = resolve;
3282
- imageElement.onerror = reject;
3283
- imageElement.src = url;
3284
- });
3285
- // 使用后释放URL对象
3286
- URL.revokeObjectURL(url);
3287
- }
3288
- else if (typeof imageSource === "string") {
3289
- // 如果是URL字符串,创建新的Image元素并加载图片
3290
- imageElement = new Image();
3291
- imageElement.crossOrigin = "anonymous"; // 处理跨域图片
3292
- await new Promise((resolve, reject) => {
3293
- imageElement.onload = resolve;
3294
- imageElement.onerror = reject;
3295
- imageElement.src = imageSource;
3296
- });
3297
- }
3298
- else if (imageSource instanceof HTMLImageElement) {
3299
- // 如果已经是Image元素,直接使用
3300
- imageElement = imageSource;
3301
- }
3302
- else if (imageSource instanceof HTMLCanvasElement) {
3303
- // 如果是Canvas元素,创建Image并从Canvas获取数据
3304
- imageElement = new Image();
3305
- imageElement.src = imageSource.toDataURL();
3306
- await new Promise((resolve) => {
3307
- imageElement.onload = resolve;
3308
- });
3309
- }
3310
- else {
3311
- throw new Error("不支持的图片源类型");
3312
- }
3313
- // 获取图像数据
3314
- const canvas = document.createElement("canvas");
3315
- canvas.width = imageElement.naturalWidth;
3316
- canvas.height = imageElement.naturalHeight;
3317
- const ctx = canvas.getContext("2d");
3318
- if (!ctx) {
3319
- throw new Error("无法创建Canvas上下文");
3320
- }
3321
- ctx.drawImage(imageElement, 0, 0);
3322
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
3323
- // 使用QR模块处理图像
3324
- return this.qrModule.processQRCodeImage(imageData);
3325
- }
3326
- catch (error) {
3327
- this.handleError(error);
3328
- throw error;
3329
- }
3330
- }
3331
- /**
3332
- * 处理图片中的条形码
3333
- * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
3334
- * @returns 返回Promise,解析为扫描结果
3335
- */
3336
- async processBarcodeImage(imageSource) {
3337
- try {
3338
- // 动态加载二维码模块
3339
- if (!this.isQRModuleLoaded) {
3340
- const ScannerModule = await Promise.resolve().then(function () { return qrModule; }).then((m) => m.ScannerModule);
3341
- this.qrModule = new ScannerModule({
3342
- barcodeScannerOptions: this.options.barcodeScannerOptions,
3343
- onBarcodeScanned: this.options.onBarcodeScanned,
3344
- onError: this.options.onError,
3345
- });
3346
- this.isQRModuleLoaded = true;
3347
- }
3348
- // 处理不同类型的图片源
3349
- let imageElement;
3350
- if (imageSource instanceof File) {
3351
- // 如果是File对象,创建新的Image元素并加载图片
3352
- imageElement = new Image();
3353
- imageElement.crossOrigin = "anonymous"; // 处理跨域图片
3354
- const url = URL.createObjectURL(imageSource);
3355
- await new Promise((resolve, reject) => {
3356
- imageElement.onload = resolve;
3357
- imageElement.onerror = reject;
3358
- imageElement.src = url;
3359
- });
3360
- // 使用后释放URL对象
3361
- URL.revokeObjectURL(url);
3362
- }
3363
- else if (typeof imageSource === "string") {
3364
- // 如果是URL字符串,创建新的Image元素并加载图片
3365
- imageElement = new Image();
3366
- imageElement.crossOrigin = "anonymous"; // 处理跨域图片
3367
- await new Promise((resolve, reject) => {
3368
- imageElement.onload = resolve;
3369
- imageElement.onerror = reject;
3370
- imageElement.src = imageSource;
3371
- });
3372
- }
3373
- else if (imageSource instanceof HTMLImageElement) {
3374
- // 如果已经是Image元素,直接使用
3375
- imageElement = imageSource;
3376
- }
3377
- else if (imageSource instanceof HTMLCanvasElement) {
3378
- // 如果是Canvas元素,创建Image并从Canvas获取数据
3379
- imageElement = new Image();
3380
- imageElement.src = imageSource.toDataURL();
3381
- await new Promise((resolve) => {
3382
- imageElement.onload = resolve;
3383
- });
3384
- }
3385
- else {
3386
- throw new Error("不支持的图片源类型");
3387
- }
3388
- // 获取图像数据
3389
- const canvas = document.createElement("canvas");
3390
- canvas.width = imageElement.naturalWidth;
3391
- canvas.height = imageElement.naturalHeight;
3392
- const ctx = canvas.getContext("2d");
3393
- if (!ctx) {
3394
- throw new Error("无法创建Canvas上下文");
3395
- }
3396
- ctx.drawImage(imageElement, 0, 0);
3397
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
3398
- // 使用Barcode模块处理图像
3399
- return this.qrModule.processBarcodeImage(imageData);
3400
- }
3401
- catch (error) {
3402
- this.handleError(error);
3403
- throw error;
3404
- }
3405
- }
3406
- /**
3407
- * 处理图片中的身份证
3408
- * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
3409
- * @returns 返回Promise,解析为身份证信息
3410
- */
3411
- async processIDCardImage(imageSource) {
3412
- if (!this.isOCRModuleLoaded) {
3413
- await this.initOCRModule();
3414
- }
3415
- try {
3416
- // 处理不同类型的图片源
3417
- let imageElement;
3418
- if (imageSource instanceof File) {
3419
- // 如果是File对象,先进行压缩
3420
- const compressedFile = await ImageProcessor.compressImage(imageSource, {
3421
- maxSizeMB: 2, // 最大2MB
3422
- maxWidthOrHeight: 1800, // 最大尺寸
3423
- useWebWorker: true,
3424
- });
3425
- // 创建新的Image元素并加载图片
3426
- imageElement = new Image();
3427
- imageElement.crossOrigin = "anonymous"; // 处理跨域图片
3428
- const url = URL.createObjectURL(compressedFile);
3429
- await new Promise((resolve, reject) => {
3430
- imageElement.onload = resolve;
3431
- imageElement.onerror = reject;
3432
- imageElement.src = url;
3433
- });
3434
- // 使用后释放URL对象
3435
- URL.revokeObjectURL(url);
3436
- }
3437
- else if (typeof imageSource === "string") {
3438
- // 如果是URL字符串,创建新的Image元素并加载图片
3439
- imageElement = new Image();
3440
- imageElement.crossOrigin = "anonymous"; // 处理跨域图片
3441
- await new Promise((resolve, reject) => {
3442
- imageElement.onload = resolve;
3443
- imageElement.onerror = reject;
3444
- imageElement.src = imageSource;
3445
- });
3446
- }
3447
- else if (imageSource instanceof HTMLImageElement) {
3448
- // 如果已经是Image元素,直接使用
3449
- imageElement = imageSource;
3450
- }
3451
- else if (imageSource instanceof HTMLCanvasElement) {
3452
- // 如果是Canvas元素,创建Image并从Canvas获取数据
3453
- imageElement = new Image();
3454
- imageElement.src = imageSource.toDataURL();
3455
- await new Promise((resolve) => {
3456
- imageElement.onload = resolve;
3457
- });
3458
- }
3459
- else {
3460
- throw new Error("不支持的图片源类型");
3461
- }
3462
- // 获取图像数据
3463
- const canvas = document.createElement("canvas");
3464
- canvas.width = imageElement.naturalWidth;
3465
- canvas.height = imageElement.naturalHeight;
3466
- const ctx = canvas.getContext("2d");
3467
- if (!ctx) {
3468
- throw new Error("无法创建Canvas上下文");
3469
- }
3470
- ctx.drawImage(imageElement, 0, 0);
3471
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
3472
- // 对图像进行预处理,提高识别率
3473
- const enhancedImageData = ImageProcessor.batchProcess(imageData, {
3474
- brightness: 10,
3475
- contrast: 15,
3476
- sharpen: true,
3477
- });
3478
- // 使用OCR模块处理图像
3479
- const idInfo = await this.ocrModule.processIDCard(enhancedImageData);
3480
- // 进行防伪检测并将结果添加到身份证信息中
3481
- if (this.isAntiFakeModuleLoaded && this.antiFakeDetector) {
3482
- try {
3483
- const result = await this.antiFakeDetector.detect(enhancedImageData);
3484
- // 将防伪检测结果添加到身份证信息对象中
3485
- const extendedInfo = idInfo;
3486
- extendedInfo.antiFakeResult = result;
3487
- // 触发防伪检测回调
3488
- if (this.options.onAntiFakeDetected) {
3489
- this.options.onAntiFakeDetected(result);
3490
- }
3491
- }
3492
- catch (error) {
3493
- console.warn("身份证防伪检测失败:", error);
3494
- }
3495
- }
3496
- return idInfo;
3497
- }
3498
- catch (error) {
3499
- this.handleError(error);
3500
- throw error;
3501
- }
3502
- }
3503
- /**
3504
- * 批量处理图像
3505
- * @param imageSource 图片源,可以是Image元素、Canvas元素、URL字符串或File对象
3506
- * @param options 图像处理选项
3507
- * @param outputFormat 输出格式,'imagedata'或'file'
3508
- * @returns 返回Promise,解析为处理后的ImageData或File
3509
- */
3510
- async processImage(imageSource, options = {}, outputFormat = "imagedata") {
3511
- try {
3512
- // 处理不同类型的图片源
3513
- let imageData;
3514
- if (imageSource instanceof File) {
3515
- // 如果是File对象,先进行压缩
3516
- const compressedFile = await ImageProcessor.compressImage(imageSource, {
3517
- maxSizeMB: 2, // 最大2MB
3518
- maxWidthOrHeight: 1920, // 最大尺寸
3519
- useWebWorker: true,
3520
- });
3521
- // 从File创建ImageData
3522
- imageData = await ImageProcessor.createImageDataFromFile(compressedFile);
3523
- }
3524
- else if (typeof imageSource === "string") {
3525
- // 如果是URL字符串,创建新的Image元素并加载图片
3526
- const imageElement = new Image();
3527
- imageElement.crossOrigin = "anonymous"; // 处理跨域图片
3528
- await new Promise((resolve, reject) => {
3529
- imageElement.onload = resolve;
3530
- imageElement.onerror = reject;
3531
- imageElement.src = imageSource;
3532
- });
3533
- // 获取图像数据
3534
- const canvas = document.createElement("canvas");
3535
- canvas.width = imageElement.naturalWidth;
3536
- canvas.height = imageElement.naturalHeight;
3537
- const ctx = canvas.getContext("2d");
3538
- if (!ctx) {
3539
- throw new Error("无法创建Canvas上下文");
3540
- }
3541
- ctx.drawImage(imageElement, 0, 0);
3542
- imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
3543
- }
3544
- else if (imageSource instanceof HTMLImageElement) {
3545
- // 如果是Image元素,从它创建ImageData
3546
- const canvas = document.createElement("canvas");
3547
- canvas.width = imageSource.naturalWidth;
3548
- canvas.height = imageSource.naturalHeight;
3549
- const ctx = canvas.getContext("2d");
3550
- if (!ctx) {
3551
- throw new Error("无法创建Canvas上下文");
3552
- }
3553
- ctx.drawImage(imageSource, 0, 0);
3554
- imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
3555
- }
3556
- else if (imageSource instanceof HTMLCanvasElement) {
3557
- // 如果是Canvas元素,直接获取其ImageData
3558
- const ctx = imageSource.getContext("2d");
3559
- if (!ctx) {
3560
- throw new Error("无法获取Canvas上下文");
3561
- }
3562
- imageData = ctx.getImageData(0, 0, imageSource.width, imageSource.height);
3563
- }
3564
- else {
3565
- throw new Error("不支持的图片源类型");
3566
- }
3567
- // 进行图像处理
3568
- const processedImageData = ImageProcessor.batchProcess(imageData, options);
3569
- // 根据需要的输出格式返回结果
3570
- if (outputFormat === "file") {
3571
- // 将ImageData转换为File
3572
- const file = await ImageProcessor.imageDataToFile(processedImageData, "processed_image.jpg", "image/jpeg", 0.85);
3573
- // 触发回调
3574
- if (this.options.onImageProcessed) {
3575
- this.options.onImageProcessed(file);
3576
- }
3577
- return file;
3578
- }
3579
- else {
3580
- // 触发回调
3581
- if (this.options.onImageProcessed) {
3582
- this.options.onImageProcessed(processedImageData);
3583
- }
3584
- return processedImageData;
3585
- }
3586
- }
3587
- catch (error) {
3588
- this.handleError(error);
3589
- throw error;
3590
- }
3591
- }
3592
- /**
3593
- * 压缩图片
3594
- * @param file 要压缩的图片文件
3595
- * @param options 压缩选项
3596
- * @returns 返回Promise,解析为压缩后的文件
3597
- */
3598
- async compressImage(file, options) {
3599
- try {
3600
- return await ImageProcessor.compressImage(file, options);
3601
- }
3602
- catch (error) {
3603
- this.handleError(error);
3604
- throw error;
3605
- }
3606
- }
3607
- /**
3608
- * 身份证防伪检测
3609
- * @param imageSource 图片源
3610
- * @returns 防伪检测结果
3611
- */
3612
- async detectIDCardAntiFake(imageSource) {
3613
- if (!this.isAntiFakeModuleLoaded || !this.antiFakeDetector) {
3614
- await this.initialize();
3615
- if (!this.antiFakeDetector) {
3616
- throw new Error("防伪检测模块初始化失败");
3617
- }
3618
- }
3619
- try {
3620
- // 转换输入为ImageData
3621
- let imageData;
3622
- if (typeof imageSource === "string") {
3623
- // 处理URL或Base64
3624
- const img = new Image();
3625
- await new Promise((resolve, reject) => {
3626
- img.onload = () => resolve();
3627
- img.onerror = () => reject(new Error("图像加载失败"));
3628
- img.src = imageSource;
3629
- });
3630
- const canvas = document.createElement("canvas");
3631
- canvas.width = img.width;
3632
- canvas.height = img.height;
3633
- const ctx = canvas.getContext("2d");
3634
- if (!ctx) {
3635
- throw new Error("无法创建Canvas上下文");
3636
- }
3637
- ctx.drawImage(img, 0, 0);
3638
- imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
3639
- }
3640
- else if (imageSource instanceof File) {
3641
- // 处理文件
3642
- imageData = await ImageProcessor.createImageDataFromFile(imageSource);
3643
- }
3644
- else if (imageSource instanceof HTMLImageElement) {
3645
- // 处理Image元素
3646
- const canvas = document.createElement("canvas");
3647
- canvas.width = imageSource.width;
3648
- canvas.height = imageSource.height;
3649
- const ctx = canvas.getContext("2d");
3650
- if (!ctx) {
3651
- throw new Error("无法创建Canvas上下文");
3652
- }
3653
- ctx.drawImage(imageSource, 0, 0);
3654
- imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
3655
- }
3656
- else {
3657
- // 处理Canvas元素
3658
- const ctx = imageSource.getContext("2d");
3659
- if (!ctx) {
3660
- throw new Error("无法获取Canvas上下文");
3661
- }
3662
- imageData = ctx.getImageData(0, 0, imageSource.width, imageSource.height);
3663
- }
3664
- // 执行防伪检测
3665
- const result = await this.antiFakeDetector.detect(imageData);
3666
- // 触发回调
3667
- if (this.options.onAntiFakeDetected) {
3668
- this.options.onAntiFakeDetected(result);
3669
- }
3670
- return result;
3671
- }
3672
- catch (error) {
3673
- this.handleError(error);
3674
- throw error;
3675
- }
3676
- }
3677
- };
3678
-
3679
- class IDScannerDemo {
3680
- constructor(videoElementId, resultContainerId, switchButtonId, imageInputId) {
3681
- this.imageInput = null;
3682
- this.currentMode = "qr";
3683
- // 获取DOM元素
3684
- this.videoElement = document.getElementById(videoElementId);
3685
- this.resultContainer = document.getElementById(resultContainerId);
3686
- this.switchButton = document.getElementById(switchButtonId);
3687
- // 如果提供了图片输入元素ID,初始化图片输入功能
3688
- if (imageInputId) {
3689
- this.imageInput = document.getElementById(imageInputId);
3690
- if (this.imageInput) {
3691
- this.imageInput.addEventListener("change", this.handleImageInput.bind(this));
3692
- }
3693
- }
3694
- try {
3695
- // 创建IDScanner实例
3696
- this.scanner = new IDScanner$1({
3697
- onQRCodeScanned: this.handleQRCodeResult.bind(this),
3698
- onBarcodeScanned: this.handleQRCodeResult.bind(this), // 复用QR码结果处理
3699
- onIDCardScanned: this.handleIDCardResult.bind(this),
3700
- onError: this.handleError.bind(this),
3701
- });
3702
- }
3703
- catch (error) {
3704
- console.error("创建IDScanner实例失败:", error);
3705
- this.handleError(error instanceof Error ? error : new Error("初始化失败"));
3706
- // 创建一个空对象以避免空引用错误
3707
- this.scanner = {};
3708
- }
3709
- // 添加模式切换按钮事件监听
3710
- this.switchButton.addEventListener("click", this.toggleScanMode.bind(this));
3711
- }
3712
- async initialize() {
3713
- try {
3714
- await this.scanner.initialize();
3715
- await this.scanner.startQRScanner(this.videoElement);
3716
- this.switchButton.textContent = "切换到身份证模式";
3717
- this.currentMode = "qr";
3718
- }
3719
- catch (error) {
3720
- this.handleError(error instanceof Error ? error : new Error("初始化失败"));
3721
- }
3722
- }
3723
- async toggleScanMode() {
3724
- try {
3725
- this.scanner.stop();
3726
- if (this.currentMode === "qr") {
3727
- this.currentMode = "barcode";
3728
- await this.scanner.startBarcodeScanner(this.videoElement);
3729
- this.switchButton.textContent = "切换到身份证模式";
3730
- }
3731
- else if (this.currentMode === "barcode") {
3732
- this.currentMode = "idcard";
3733
- await this.scanner.startIDCardScanner(this.videoElement);
3734
- this.switchButton.textContent = "切换到二维码模式";
3735
- }
3736
- else {
3737
- this.currentMode = "qr";
3738
- await this.scanner.startQRScanner(this.videoElement);
3739
- this.switchButton.textContent = "切换到条形码模式";
3740
- }
3741
- this.resultContainer.innerHTML = "";
3742
- }
3743
- catch (error) {
3744
- this.handleError(error instanceof Error ? error : new Error("切换模式失败"));
3745
- }
3746
- }
3747
- /**
3748
- * 处理图片输入
3749
- * 支持从文件选择器获取图片并进行识别
3750
- */
3751
- async handleImageInput(event) {
3752
- const input = event.target;
3753
- if (!input.files || input.files.length === 0)
3754
- return;
3755
- const file = input.files[0];
3756
- try {
3757
- // 检查文件类型
3758
- if (!file.type.startsWith("image/")) {
3759
- throw new Error("请选择图片文件");
3760
- }
3761
- // 创建一个本地URL以显示图片
3762
- const imageUrl = URL.createObjectURL(file);
3763
- // 显示处理中的提示
3764
- this.resultContainer.innerHTML = `
3765
- <h3>正在处理图片...</h3>
3766
- <img src="${imageUrl}" style="max-width: 100%; max-height: 300px; margin-bottom: 10px;">
3767
- `;
3768
- // 根据当前模式处理图片
3769
- try {
3770
- if (this.currentMode === "qr") {
3771
- const result = await this.scanner.processQRCodeImage(imageUrl);
3772
- if (result) {
3773
- this.handleQRCodeResult(result);
3774
- }
3775
- }
3776
- else if (this.currentMode === "barcode") {
3777
- const result = await this.scanner.processBarcodeImage(imageUrl);
3778
- if (result) {
3779
- this.handleQRCodeResult(result);
3780
- }
3781
- }
3782
- else if (this.currentMode === "idcard") {
3783
- const result = await this.scanner.processIDCardImage(imageUrl);
3784
- if (result) {
3785
- this.handleIDCardResult(result);
3786
- }
3787
- }
3788
- }
3789
- catch (error) {
3790
- // 如果处理失败,显示错误
3791
- this.resultContainer.innerHTML = `
3792
- <h3>识别结果:</h3>
3793
- <img src="${imageUrl}" style="max-width: 100%; max-height: 300px; margin-bottom: 10px;">
3794
- <p class="error">未能识别内容,请尝试其他图片或调整图片质量</p>
3795
- <p class="error">${error instanceof Error ? error.message : "未知错误"}</p>
3796
- `;
3797
- }
3798
- // 清除文件选择,允许再次选择相同的文件
3799
- input.value = "";
3800
- }
3801
- catch (error) {
3802
- this.handleError(error instanceof Error ? error : new Error(String(error)));
3803
- // 清除文件选择
3804
- input.value = "";
3805
- }
3806
- }
3807
- handleQRCodeResult(result) {
3808
- this.resultContainer.innerHTML = `
3809
- <h3>扫描结果:</h3>
3810
- <p>${result}</p>
3811
- `;
3812
- }
3813
- handleIDCardResult(info) {
3814
- this.resultContainer.innerHTML = `
3815
- <h3>身份证信息:</h3>
3816
- <p>姓名: ${info.name || "未识别"}</p>
3817
- <p>性别: ${info.gender || "未识别"}</p>
3818
- <p>民族: ${info.nationality || "未识别"}</p>
3819
- <p>出生日期: ${info.birthDate || "未识别"}</p>
3820
- <p>地址: ${info.address || "未识别"}</p>
3821
- <p>身份证号: ${info.idNumber || "未识别"}</p>
3822
- <p>签发机关: ${info.issuingAuthority || "未识别"}</p>
3823
- <p>有效期限: ${info.validPeriod || "未识别"}</p>
3824
- `;
3825
- }
3826
- handleError(error) {
3827
- console.error("扫描错误:", error);
3828
- this.resultContainer.innerHTML = `
3829
- <div class="error">
3830
- <h3>错误:</h3>
3831
- <p>${error.message}</p>
3832
- </div>
3833
- `;
3834
- }
3835
- stop() {
3836
- if (this.scanner && typeof this.scanner.stop === "function") {
3837
- this.scanner.stop();
3838
- }
3839
- if (this.scanner && typeof this.scanner.terminate === "function") {
3840
- this.scanner.terminate();
3841
- }
3842
- }
3843
- }
3844
-
3845
- /**
3846
- * @file ID扫描识别库UMD格式入口文件
3847
- * @description 专门为UMD格式构建的入口,使用静态导入而非动态导入
3848
- * @module IDScannerLib
3849
- * @version 1.1.0
3850
- * @license MIT
3851
- */
3852
- /**
3853
- * IDScanner 主类
3854
- * UMD版本使用静态导入实现
3855
- */
3856
- class IDScanner {
3857
- /**
3858
- * 构造函数
3859
- * @param options 配置选项
3860
- */
3861
- constructor(options = {}) {
3862
- this.options = options;
3863
- this.qrScanner = null;
3864
- this.barcodeScanner = null;
3865
- this.idDetector = null;
3866
- this.ocrProcessor = null;
3867
- this.dataExtractor = null;
3868
- this.scanMode = "qr";
3869
- this.videoElement = null;
3870
- this.camera = new Camera(options.cameraOptions);
3871
- }
3872
- /**
3873
- * 初始化模块
3874
- * 根据需要初始化OCR引擎
3875
- */
3876
- async initialize() {
3877
- try {
3878
- // 初始化OCR模块
3879
- this.ocrProcessor = new OCRProcessor();
3880
- this.dataExtractor = new DataExtractor();
3881
- await this.ocrProcessor.initialize();
3882
- console.log("IDScanner initialized");
3883
- }
3884
- catch (error) {
3885
- this.handleError(error);
3886
- throw error;
3887
- }
3888
- }
3889
- /**
3890
- * 启动二维码扫描
3891
- * @param videoElement HTML视频元素
3892
- */
3893
- async startQRScanner(videoElement) {
3894
- this.stop();
3895
- this.videoElement = videoElement;
3896
- this.scanMode = "qr";
3897
- try {
3898
- if (!this.qrScanner) {
3899
- this.qrScanner = new QRScanner({
3900
- ...this.options.qrScannerOptions,
3901
- onScan: this.handleQRScan.bind(this),
3902
- });
3903
- }
3904
- await this.camera.start(videoElement);
3905
- this.qrScanner.start(videoElement);
3906
- }
3907
- catch (error) {
3908
- this.handleError(error);
3909
- }
3910
- }
3911
- /**
3912
- * 启动条形码扫描
3913
- * @param videoElement HTML视频元素
3914
- */
3915
- async startBarcodeScanner(videoElement) {
3916
- this.stop();
3917
- this.videoElement = videoElement;
3918
- this.scanMode = "barcode";
3919
- try {
3920
- if (!this.barcodeScanner) {
3921
- this.barcodeScanner = new BarcodeScanner({
3922
- ...this.options.barcodeScannerOptions,
3923
- onScan: this.handleBarcodeScan.bind(this),
3924
- });
3925
- }
3926
- await this.camera.start(videoElement);
3927
- this.barcodeScanner.start(videoElement);
3928
- }
3929
- catch (error) {
3930
- this.handleError(error);
3931
- }
3932
- }
3933
- /**
3934
- * 启动身份证扫描
3935
- * @param videoElement HTML视频元素
3936
- */
3937
- async startIDCardScanner(videoElement) {
3938
- this.stop();
3939
- this.videoElement = videoElement;
3940
- this.scanMode = "idcard";
3941
- try {
3942
- if (!this.ocrProcessor) {
3943
- await this.initialize();
3944
- }
3945
- if (!this.idDetector) {
3946
- this.idDetector = new IDCardDetector({
3947
- onDetection: this.handleIDDetection.bind(this),
3948
- onError: this.handleError.bind(this),
3949
- });
3950
- }
3951
- await this.camera.start(videoElement);
3952
- this.idDetector.start(videoElement);
3953
- }
3954
- catch (error) {
3955
- this.handleError(error);
3956
- }
3957
- }
3958
- /**
3959
- * 停止扫描
3960
- */
3961
- stop() {
3962
- if (this.scanMode === "qr" && this.qrScanner) {
3963
- this.qrScanner.stop();
3964
- }
3965
- else if (this.scanMode === "barcode" && this.barcodeScanner) {
3966
- this.barcodeScanner.stop();
3967
- }
3968
- else if (this.scanMode === "idcard" && this.idDetector) {
3969
- this.idDetector.stop();
3970
- }
3971
- this.camera.stop();
3972
- }
3973
- /**
3974
- * 处理二维码扫描结果
3975
- */
3976
- handleQRScan(result) {
3977
- if (this.options.onQRCodeScanned) {
3978
- this.options.onQRCodeScanned(result);
3979
- }
3980
- }
3981
- /**
3982
- * 处理条形码扫描结果
3983
- */
3984
- handleBarcodeScan(result) {
3985
- if (this.options.onBarcodeScanned) {
3986
- this.options.onBarcodeScanned(result);
3987
- }
3988
- }
3989
- /**
3990
- * 处理身份证检测结果
3991
- */
3992
- async handleIDDetection(result) {
3993
- if (!this.ocrProcessor || !this.dataExtractor)
3994
- return;
3995
- try {
3996
- // 检查 imageData 是否存在
3997
- if (!result.imageData) {
3998
- this.handleError(new Error("无效的图像数据"));
3999
- return;
4000
- }
4001
- const idCardInfo = await this.ocrProcessor.processIDCard(result.imageData);
4002
- const extractedInfo = this.dataExtractor.extractAndValidate(idCardInfo);
4003
- if (this.options.onIDCardScanned) {
4004
- this.options.onIDCardScanned(extractedInfo);
4005
- }
4006
- }
4007
- catch (error) {
4008
- this.handleError(error);
4009
- }
4010
- }
4011
- /**
4012
- * 处理错误
4013
- */
4014
- handleError(error) {
4015
- if (this.options.onError) {
4016
- this.options.onError(error);
4017
- }
4018
- else {
4019
- console.error("IDScanner error:", error);
4020
- }
4021
- }
4022
- /**
4023
- * 释放资源
4024
- */
4025
- async terminate() {
4026
- this.stop();
4027
- if (this.ocrProcessor) {
4028
- await this.ocrProcessor.terminate();
4029
- this.ocrProcessor = null;
4030
- }
4031
- this.qrScanner = null;
4032
- this.barcodeScanner = null;
4033
- this.idDetector = null;
4034
- this.dataExtractor = null;
4035
- }
4036
- /**
4037
- * 处理图片中的二维码
4038
- * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
4039
- * @returns 返回Promise,解析为扫描结果
4040
- */
4041
- async processQRCodeImage(imageSource) {
4042
- try {
4043
- if (!this.qrScanner) {
4044
- this.qrScanner = new QRScanner({
4045
- ...this.options.qrScannerOptions,
4046
- onScan: this.handleQRScan.bind(this),
4047
- });
4048
- }
4049
- // 处理不同类型的图片源
4050
- let imageElement;
4051
- if (typeof imageSource === "string") {
4052
- // 如果是URL字符串,创建新的Image元素并加载图片
4053
- imageElement = new Image();
4054
- imageElement.crossOrigin = "anonymous"; // 处理跨域图片
4055
- await new Promise((resolve, reject) => {
4056
- imageElement.onload = resolve;
4057
- imageElement.onerror = reject;
4058
- imageElement.src = imageSource;
4059
- });
4060
- }
4061
- else if (imageSource instanceof HTMLImageElement) {
4062
- // 如果已经是Image元素,直接使用
4063
- imageElement = imageSource;
4064
- }
4065
- else if (imageSource instanceof HTMLCanvasElement) {
4066
- // 如果是Canvas元素,创建新的Image元素并从Canvas获取数据
4067
- const dataURL = imageSource.toDataURL();
4068
- imageElement = new Image();
4069
- await new Promise((resolve, reject) => {
4070
- imageElement.onload = resolve;
4071
- imageElement.onerror = reject;
4072
- imageElement.src = dataURL;
4073
- });
4074
- }
4075
- else {
4076
- throw new Error("不支持的图片源类型");
4077
- }
4078
- // 创建Canvas处理图片
4079
- const canvas = document.createElement("canvas");
4080
- const ctx = canvas.getContext("2d");
4081
- if (!ctx) {
4082
- throw new Error("无法创建Canvas上下文");
4083
- }
4084
- // 设置Canvas尺寸与图片相同
4085
- canvas.width = imageElement.naturalWidth;
4086
- canvas.height = imageElement.naturalHeight;
4087
- ctx.drawImage(imageElement, 0, 0);
4088
- // 获取图像数据并处理
4089
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
4090
- return new Promise((resolve, reject) => {
4091
- try {
4092
- const result = this.qrScanner?.processImageData(imageData);
4093
- if (result) {
4094
- resolve(result);
4095
- }
4096
- else {
4097
- reject(new Error("未检测到二维码"));
4098
- }
4099
- }
4100
- catch (error) {
4101
- reject(error);
4102
- }
4103
- });
4104
- }
4105
- catch (error) {
4106
- this.handleError(error);
4107
- throw error;
4108
- }
4109
- }
4110
- /**
4111
- * 处理图片中的条形码
4112
- * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
4113
- * @returns 返回Promise,解析为扫描结果
4114
- */
4115
- async processBarcodeImage(imageSource) {
4116
- try {
4117
- if (!this.barcodeScanner) {
4118
- this.barcodeScanner = new BarcodeScanner({
4119
- ...this.options.barcodeScannerOptions,
4120
- onScan: this.handleBarcodeScan.bind(this),
4121
- });
4122
- }
4123
- // 处理不同类型的图片源
4124
- let imageElement;
4125
- if (typeof imageSource === "string") {
4126
- // 如果是URL字符串,创建新的Image元素并加载图片
4127
- imageElement = new Image();
4128
- imageElement.crossOrigin = "anonymous"; // 处理跨域图片
4129
- await new Promise((resolve, reject) => {
4130
- imageElement.onload = resolve;
4131
- imageElement.onerror = reject;
4132
- imageElement.src = imageSource;
4133
- });
4134
- }
4135
- else if (imageSource instanceof HTMLImageElement) {
4136
- // 如果已经是Image元素,直接使用
4137
- imageElement = imageSource;
4138
- }
4139
- else if (imageSource instanceof HTMLCanvasElement) {
4140
- // 如果是Canvas元素,创建新的Image元素并从Canvas获取数据
4141
- const dataURL = imageSource.toDataURL();
4142
- imageElement = new Image();
4143
- await new Promise((resolve, reject) => {
4144
- imageElement.onload = resolve;
4145
- imageElement.onerror = reject;
4146
- imageElement.src = dataURL;
4147
- });
4148
- }
4149
- else {
4150
- throw new Error("不支持的图片源类型");
4151
- }
4152
- // 创建Canvas处理图片
4153
- const canvas = document.createElement("canvas");
4154
- const ctx = canvas.getContext("2d");
4155
- if (!ctx) {
4156
- throw new Error("无法创建Canvas上下文");
4157
- }
4158
- // 设置Canvas尺寸与图片相同
4159
- canvas.width = imageElement.naturalWidth;
4160
- canvas.height = imageElement.naturalHeight;
4161
- ctx.drawImage(imageElement, 0, 0);
4162
- // 获取图像数据并处理
4163
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
4164
- return new Promise((resolve, reject) => {
4165
- try {
4166
- const result = this.barcodeScanner?.processImageData(imageData);
4167
- if (result) {
4168
- resolve(result);
4169
- }
4170
- else {
4171
- reject(new Error("未检测到条形码"));
4172
- }
4173
- }
4174
- catch (error) {
4175
- reject(error);
4176
- }
4177
- });
4178
- }
4179
- catch (error) {
4180
- this.handleError(error);
4181
- throw error;
4182
- }
4183
- }
4184
- /**
4185
- * 处理图片中的身份证
4186
- * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
4187
- * @returns 返回Promise,解析为身份证信息
4188
- */
4189
- async processIDCardImage(imageSource) {
4190
- try {
4191
- if (!this.ocrProcessor || !this.dataExtractor) {
4192
- await this.initialize();
4193
- }
4194
- // 处理不同类型的图片源
4195
- let imageElement;
4196
- if (typeof imageSource === "string") {
4197
- // 如果是URL字符串,创建新的Image元素并加载图片
4198
- imageElement = new Image();
4199
- imageElement.crossOrigin = "anonymous"; // 处理跨域图片
4200
- await new Promise((resolve, reject) => {
4201
- imageElement.onload = resolve;
4202
- imageElement.onerror = reject;
4203
- imageElement.src = imageSource;
4204
- });
4205
- }
4206
- else if (imageSource instanceof HTMLImageElement) {
4207
- // 如果已经是Image元素,直接使用
4208
- imageElement = imageSource;
4209
- }
4210
- else if (imageSource instanceof HTMLCanvasElement) {
4211
- // 如果是Canvas元素,创建新的Image元素并从Canvas获取数据
4212
- const dataURL = imageSource.toDataURL();
4213
- imageElement = new Image();
4214
- await new Promise((resolve, reject) => {
4215
- imageElement.onload = resolve;
4216
- imageElement.onerror = reject;
4217
- imageElement.src = dataURL;
4218
- });
4219
- }
4220
- else {
4221
- throw new Error("不支持的图片源类型");
4222
- }
4223
- // 创建Canvas处理图片
4224
- const canvas = document.createElement("canvas");
4225
- const ctx = canvas.getContext("2d");
4226
- if (!ctx) {
4227
- throw new Error("无法创建Canvas上下文");
4228
- }
4229
- // 设置Canvas尺寸与图片相同
4230
- canvas.width = imageElement.naturalWidth;
4231
- canvas.height = imageElement.naturalHeight;
4232
- ctx.drawImage(imageElement, 0, 0);
4233
- // 获取图像数据并处理
4234
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
4235
- // 调用OCR处理器处理身份证图像
4236
- const ocrResult = await this.ocrProcessor.processIDCard(imageData);
4237
- const extractedInfo = this.dataExtractor.extractAndValidate(ocrResult);
4238
- if (this.options.onIDCardScanned) {
4239
- this.options.onIDCardScanned(extractedInfo);
4240
- }
4241
- return extractedInfo;
4242
- }
4243
- catch (error) {
4244
- this.handleError(error);
4245
- throw error;
4246
- }
4247
- }
4248
- }
4249
- // 添加静态属性IDScannerDemo,使其能被通过IDScanner.IDScannerDemo访问
4250
- IDScanner.IDScannerDemo = IDScannerDemo;
4251
-
4252
- /**
4253
- * @file OCR模块入口文件
4254
- * @description 包含身份证OCR识别相关功能
4255
- * @module IDScannerOCR
4256
- * @version 1.0.0
4257
- * @license MIT
4258
- */
4259
- /**
4260
- * OCR模块类
4261
- *
4262
- * 提供身份证检测和OCR文字识别功能
4263
- */
4264
- class OCRModule {
4265
- /**
4266
- * 构造函数
4267
- * @param options 配置选项
4268
- */
4269
- constructor(options = {}) {
4270
- this.options = options;
4271
- this.isRunning = false;
4272
- this.videoElement = null;
4273
- this.camera = new Camera(options.cameraOptions);
4274
- this.idDetector = new IDCardDetector({
4275
- onDetection: this.handleIDDetection.bind(this),
4276
- onError: this.handleError.bind(this),
4277
- });
4278
- this.ocrProcessor = new OCRProcessor();
4279
- this.dataExtractor = new DataExtractor();
4280
- }
4281
- /**
4282
- * 初始化OCR引擎
4283
- *
4284
- * @returns Promise<void>
4285
- */
4286
- async initialize() {
4287
- try {
4288
- await this.ocrProcessor.initialize();
4289
- console.log("OCR engine initialized");
4290
- }
4291
- catch (error) {
4292
- this.handleError(error);
4293
- throw error;
4294
- }
4295
- }
4296
- /**
4297
- * 启动身份证扫描
4298
- * @param videoElement HTML视频元素
4299
- */
4300
- async startIDCardScanner(videoElement) {
4301
- if (!this.ocrProcessor) {
4302
- throw new Error("OCR engine not initialized. Call initialize() first.");
4303
- }
4304
- this.videoElement = videoElement;
4305
- this.isRunning = true;
4306
- await this.camera.start(videoElement);
4307
- this.idDetector.start(videoElement);
4308
- }
4309
- /**
4310
- * 停止扫描
4311
- */
4312
- stop() {
4313
- this.isRunning = false;
4314
- this.idDetector.stop();
4315
- this.camera.stop();
4316
- }
4317
- /**
4318
- * 处理身份证检测结果
4319
- */
4320
- async handleIDDetection(result) {
4321
- if (!this.isRunning)
4322
- return;
4323
- try {
4324
- // 检查 imageData 是否存在
4325
- if (!result.imageData) {
4326
- this.handleError(new Error("无效的图像数据"));
4327
- return;
4328
- }
4329
- const idCardInfo = await this.ocrProcessor.processIDCard(result.imageData);
4330
- const extractedInfo = this.dataExtractor.extractAndValidate(idCardInfo);
4331
- if (this.options.onIDCardScanned) {
4332
- this.options.onIDCardScanned(extractedInfo);
4333
- }
4334
- }
4335
- catch (error) {
4336
- this.handleError(error);
4337
- }
4338
- }
4339
- /**
4340
- * 处理错误
4341
- */
4342
- handleError(error) {
4343
- if (this.options.onError) {
4344
- this.options.onError(error);
4345
- }
4346
- else {
4347
- console.error("OCRModule error:", error);
4348
- }
4349
- }
4350
- /**
4351
- * 释放资源
4352
- */
4353
- async terminate() {
4354
- this.stop();
4355
- await this.ocrProcessor.terminate();
4356
- }
4357
- /**
4358
- * 直接处理图像数据中的身份证
4359
- * @param imageData 要处理的图像数据
4360
- * @returns 返回Promise,解析为身份证信息
4361
- */
4362
- async processIDCard(imageData) {
4363
- try {
4364
- if (!this.ocrProcessor) {
4365
- throw new Error("OCR engine not initialized. Call initialize() first.");
4366
- }
4367
- // 检查图像数据有效性
4368
- if (!imageData ||
4369
- !imageData.data ||
4370
- imageData.width <= 0 ||
4371
- imageData.height <= 0) {
4372
- throw new Error("无效的图像数据");
4373
- }
4374
- // 进行图像预处理,提高识别率
4375
- const processedImage = ImageProcessor.adjustBrightnessContrast(imageData, 5, // 轻微提高亮度
4376
- 10 // 适度提高对比度
4377
- );
4378
- // 调用OCR处理器进行文字识别
4379
- const idCardInfo = await this.ocrProcessor.processIDCard(processedImage);
4380
- // 提取和验证身份证信息
4381
- const extractedInfo = this.dataExtractor.extractAndValidate(idCardInfo);
4382
- // 如果有回调,触发回调
4383
- if (this.options.onIDCardScanned) {
4384
- this.options.onIDCardScanned(extractedInfo);
4385
- }
4386
- return extractedInfo;
4387
- }
4388
- catch (error) {
4389
- this.handleError(error);
4390
- throw error;
4391
- }
4392
- }
4393
- }
4394
-
4395
- var ocrModule = /*#__PURE__*/Object.freeze({
4396
- __proto__: null,
4397
- DataExtractor: DataExtractor,
4398
- IDCardDetector: IDCardDetector,
4399
- OCRModule: OCRModule,
4400
- OCRProcessor: OCRProcessor
4401
- });
4402
-
4403
- /**
4404
- * @file 二维码和条形码扫描模块
4405
- * @description 包含二维码和条形码扫描功能
4406
- * @module IDScannerQR
4407
- * @version 1.0.0
4408
- * @license MIT
4409
- */
4410
- /**
4411
- * 扫描模块类
4412
- *
4413
- * 提供独立的二维码和条形码扫描功能
4414
- */
4415
- class ScannerModule {
4416
- /**
4417
- * 构造函数
4418
- * @param options 配置选项
4419
- */
4420
- constructor(options = {}) {
4421
- this.options = options;
4422
- this.scanMode = null;
4423
- this.videoElement = null;
4424
- this.camera = new Camera(options.cameraOptions);
4425
- this.qrScanner = new QRScanner({
4426
- ...options.qrScannerOptions,
4427
- onScan: this.handleQRScan.bind(this),
4428
- });
4429
- this.barcodeScanner = new BarcodeScanner({
4430
- ...options.barcodeScannerOptions,
4431
- onScan: this.handleBarcodeScan.bind(this),
4432
- });
4433
- }
4434
- /**
4435
- * 启动二维码扫描
4436
- * @param videoElement HTML视频元素
4437
- */
4438
- async startQRScanner(videoElement) {
4439
- this.stop(); // 确保先停止可能正在运行的扫描
4440
- this.videoElement = videoElement;
4441
- this.scanMode = "qr";
4442
- await this.camera.start(videoElement);
4443
- this.qrScanner.start(videoElement);
4444
- }
4445
- /**
4446
- * 启动条形码扫描
4447
- * @param videoElement HTML视频元素
4448
- */
4449
- async startBarcodeScanner(videoElement) {
4450
- this.stop(); // 确保先停止可能正在运行的扫描
4451
- this.videoElement = videoElement;
4452
- this.scanMode = "barcode";
4453
- await this.camera.start(videoElement);
4454
- this.barcodeScanner.start(videoElement);
4455
- }
4456
- /**
4457
- * 停止扫描
4458
- */
4459
- stop() {
4460
- if (this.scanMode === "qr") {
4461
- this.qrScanner.stop();
4462
- }
4463
- else if (this.scanMode === "barcode") {
4464
- this.barcodeScanner.stop();
4465
- }
4466
- if (this.videoElement) {
4467
- this.camera.stop();
4468
- }
4469
- this.scanMode = null;
4470
- }
4471
- /**
4472
- * 处理二维码扫描结果
4473
- */
4474
- handleQRScan(result) {
4475
- if (this.options.onQRCodeScanned) {
4476
- this.options.onQRCodeScanned(result);
4477
- }
4478
- }
4479
- /**
4480
- * 处理条形码扫描结果
4481
- */
4482
- handleBarcodeScan(result) {
4483
- if (this.options.onBarcodeScanned) {
4484
- this.options.onBarcodeScanned(result);
4485
- }
4486
- }
4487
- /**
4488
- * 处理错误
4489
- */
4490
- handleError(error) {
4491
- if (this.options.onError) {
4492
- this.options.onError(error);
4493
- }
4494
- else {
4495
- console.error("ScannerModule error:", error);
4496
- }
4497
- }
4498
- /**
4499
- * 处理图像数据中的二维码
4500
- * @param imageData 要处理的图像数据
4501
- * @returns 返回Promise,解析为扫描结果
4502
- */
4503
- async processQRCodeImage(imageData) {
4504
- try {
4505
- const result = this.qrScanner.processImageData(imageData);
4506
- if (result) {
4507
- // 如果需要,触发回调
4508
- if (this.options.onQRCodeScanned) {
4509
- this.options.onQRCodeScanned(result);
4510
- }
4511
- return result;
4512
- }
4513
- throw new Error("未检测到二维码");
4514
- }
4515
- catch (error) {
4516
- this.handleError(error);
4517
- throw error;
4518
- }
4519
- }
4520
- /**
4521
- * 处理图像数据中的条形码
4522
- * @param imageData 要处理的图像数据
4523
- * @returns 返回Promise,解析为扫描结果
4524
- */
4525
- async processBarcodeImage(imageData) {
4526
- try {
4527
- const result = this.barcodeScanner.processImageData(imageData);
4528
- if (result) {
4529
- // 如果需要,触发回调
4530
- if (this.options.onBarcodeScanned) {
4531
- this.options.onBarcodeScanned(result);
4532
- }
4533
- return result;
4534
- }
4535
- throw new Error("未检测到条形码");
4536
- }
4537
- catch (error) {
4538
- this.handleError(error);
4539
- throw error;
4540
- }
4541
- }
4542
- }
4543
-
4544
- var qrModule = /*#__PURE__*/Object.freeze({
4545
- __proto__: null,
4546
- BarcodeScanner: BarcodeScanner,
4547
- Camera: Camera,
4548
- QRScanner: QRScanner,
4549
- ScannerModule: ScannerModule
4550
- });
4551
-
4552
- exports.BarcodeScanner = BarcodeScanner;
4553
- exports.DataExtractor = DataExtractor;
4554
- exports.IDCardDetector = IDCardDetector;
4555
- exports.IDScanner = IDScanner;
4556
- exports.IDScannerDemo = IDScannerDemo;
4557
- exports.ImageProcessor = ImageProcessor;
4558
- exports.OCRProcessor = OCRProcessor;
4559
- exports.QRScanner = QRScanner;
4560
-
4561
- }));