id-scanner-lib 1.2.2 → 1.3.2
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.
- package/LICENSE +1 -1
- package/README.md +375 -363
- package/dist/id-scanner-core.esm.js +427 -221
- package/dist/id-scanner-core.esm.js.map +1 -1
- package/dist/id-scanner-core.js +427 -221
- package/dist/id-scanner-core.js.map +1 -1
- package/dist/id-scanner-core.min.js +1 -9
- package/dist/id-scanner-core.min.js.map +1 -1
- package/dist/id-scanner-ocr.esm.js +451 -276
- package/dist/id-scanner-ocr.esm.js.map +1 -1
- package/dist/id-scanner-ocr.js +451 -276
- package/dist/id-scanner-ocr.js.map +1 -1
- package/dist/id-scanner-ocr.min.js +1 -9
- package/dist/id-scanner-ocr.min.js.map +1 -1
- package/dist/id-scanner-qr.esm.js +483 -233
- package/dist/id-scanner-qr.esm.js.map +1 -1
- package/dist/id-scanner-qr.js +482 -232
- package/dist/id-scanner-qr.js.map +1 -1
- package/dist/id-scanner-qr.min.js +1 -9
- package/dist/id-scanner-qr.min.js.map +1 -1
- package/dist/id-scanner.js +2138 -358
- package/dist/id-scanner.js.map +1 -1
- package/dist/id-scanner.min.js +1 -9
- package/dist/id-scanner.min.js.map +1 -1
- package/package.json +27 -7
- package/src/demo/demo.ts +178 -62
- package/src/id-recognition/anti-fake-detector.ts +317 -0
- package/src/id-recognition/id-detector.ts +184 -155
- package/src/id-recognition/ocr-processor.ts +193 -146
- package/src/id-recognition/ocr-worker.ts +82 -72
- package/src/index-umd.ts +347 -110
- package/src/index.ts +866 -91
- package/src/ocr-module.ts +108 -60
- package/src/qr-module.ts +104 -54
- package/src/scanner/barcode-scanner.ts +145 -58
- package/src/scanner/qr-scanner.ts +86 -47
- package/src/utils/image-processing.ts +479 -294
- package/dist/core.d.ts +0 -77
- package/dist/demo/demo.d.ts +0 -14
- package/dist/id-recognition/data-extractor.d.ts +0 -105
- package/dist/id-recognition/id-detector.d.ts +0 -100
- package/dist/id-recognition/ocr-processor.d.ts +0 -64
- package/dist/id-scanner.esm.js +0 -94656
- package/dist/id-scanner.esm.js.map +0 -1
- package/dist/index-umd.d.ts +0 -96
- package/dist/index.d.ts +0 -78
- package/dist/ocr-module.d.ts +0 -67
- package/dist/qr-module.d.ts +0 -68
- package/dist/scanner/barcode-scanner.d.ts +0 -90
- package/dist/scanner/qr-scanner.d.ts +0 -80
- package/dist/types/core.d.ts +0 -77
- package/dist/types/demo/demo.d.ts +0 -14
- package/dist/types/id-recognition/data-extractor.d.ts +0 -105
- package/dist/types/id-recognition/id-detector.d.ts +0 -100
- package/dist/types/id-recognition/ocr-processor.d.ts +0 -64
- package/dist/types/index-umd.d.ts +0 -96
- package/dist/types/index.d.ts +0 -78
- package/dist/types/ocr-module.d.ts +0 -67
- package/dist/types/qr-module.d.ts +0 -68
- package/dist/types/scanner/barcode-scanner.d.ts +0 -90
- package/dist/types/scanner/qr-scanner.d.ts +0 -80
- package/dist/types/utils/camera.d.ts +0 -81
- package/dist/types/utils/image-processing.d.ts +0 -75
- package/dist/types/utils/types.d.ts +0 -65
- package/dist/utils/camera.d.ts +0 -81
- package/dist/utils/image-processing.d.ts +0 -75
- package/dist/utils/types.d.ts +0 -65
package/dist/id-scanner.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
(function (global, factory) {
|
|
2
|
-
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('
|
|
3
|
-
typeof define === 'function' && define.amd ? define(['exports', '
|
|
4
|
-
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.IDScanner = {}, global.
|
|
5
|
-
})(this, (function (exports,
|
|
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
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @file 相机工具类
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
this.scanTimer = null;
|
|
186
186
|
this.options = {
|
|
187
187
|
scanInterval: 200,
|
|
188
|
-
...options
|
|
188
|
+
...options,
|
|
189
189
|
};
|
|
190
190
|
this.camera = new Camera();
|
|
191
191
|
}
|
|
@@ -242,31 +242,53 @@
|
|
|
242
242
|
}
|
|
243
243
|
this.camera.release();
|
|
244
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
|
+
}
|
|
245
272
|
}
|
|
246
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
|
+
|
|
247
283
|
/**
|
|
248
284
|
* @file 图像处理工具类
|
|
249
|
-
* @description
|
|
285
|
+
* @description 提供图像预处理功能,用于提高OCR识别率
|
|
250
286
|
* @module ImageProcessor
|
|
251
287
|
*/
|
|
252
288
|
/**
|
|
253
289
|
* 图像处理工具类
|
|
254
290
|
*
|
|
255
|
-
*
|
|
256
|
-
* 这些功能可用于增强图像质量,提高OCR和扫描的识别率。
|
|
257
|
-
*
|
|
258
|
-
* @example
|
|
259
|
-
* ```typescript
|
|
260
|
-
* // 使用图像处理功能增强图像
|
|
261
|
-
* const enhancedImage = ImageProcessor.adjustBrightnessContrast(
|
|
262
|
-
* originalImageData,
|
|
263
|
-
* 15, // 增加亮度
|
|
264
|
-
* 25 // 增加对比度
|
|
265
|
-
* );
|
|
266
|
-
*
|
|
267
|
-
* // 转换为灰度图像
|
|
268
|
-
* const grayImage = ImageProcessor.toGrayscale(originalImageData);
|
|
269
|
-
* ```
|
|
291
|
+
* 提供各种图像处理功能,用于优化识别效果
|
|
270
292
|
*/
|
|
271
293
|
class ImageProcessor {
|
|
272
294
|
/**
|
|
@@ -276,10 +298,10 @@
|
|
|
276
298
|
* @returns {HTMLCanvasElement} 包含图像的Canvas元素
|
|
277
299
|
*/
|
|
278
300
|
static imageDataToCanvas(imageData) {
|
|
279
|
-
const canvas = document.createElement(
|
|
301
|
+
const canvas = document.createElement("canvas");
|
|
280
302
|
canvas.width = imageData.width;
|
|
281
303
|
canvas.height = imageData.height;
|
|
282
|
-
const ctx = canvas.getContext(
|
|
304
|
+
const ctx = canvas.getContext("2d");
|
|
283
305
|
if (ctx) {
|
|
284
306
|
ctx.putImageData(imageData, 0, 0);
|
|
285
307
|
}
|
|
@@ -292,254 +314,374 @@
|
|
|
292
314
|
* @returns {ImageData|null} Canvas的图像数据,如果获取失败则返回null
|
|
293
315
|
*/
|
|
294
316
|
static canvasToImageData(canvas) {
|
|
295
|
-
const ctx = canvas.getContext(
|
|
317
|
+
const ctx = canvas.getContext("2d");
|
|
296
318
|
return ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
|
|
297
319
|
}
|
|
298
320
|
/**
|
|
299
321
|
* 调整图像亮度和对比度
|
|
300
322
|
*
|
|
301
|
-
* @param
|
|
302
|
-
* @param
|
|
303
|
-
* @param
|
|
304
|
-
* @returns
|
|
323
|
+
* @param imageData 原始图像数据
|
|
324
|
+
* @param brightness 亮度调整值 (-100到100)
|
|
325
|
+
* @param contrast 对比度调整值 (-100到100)
|
|
326
|
+
* @returns 处理后的图像数据
|
|
305
327
|
*/
|
|
306
328
|
static adjustBrightnessContrast(imageData, brightness = 0, contrast = 0) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
data[i +
|
|
321
|
-
// Alpha不变
|
|
329
|
+
// 将亮度和对比度范围限制在 -100 到 100 之间
|
|
330
|
+
brightness = Math.max(-100, Math.min(100, brightness));
|
|
331
|
+
contrast = Math.max(-100, Math.min(100, contrast));
|
|
332
|
+
// 将范围转换为适合计算的值
|
|
333
|
+
const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
|
|
334
|
+
const briAdjust = (brightness / 100) * 255;
|
|
335
|
+
const data = imageData.data;
|
|
336
|
+
const length = data.length;
|
|
337
|
+
for (let i = 0; i < length; i += 4) {
|
|
338
|
+
// 分别处理 RGB 三个通道
|
|
339
|
+
for (let j = 0; j < 3; j++) {
|
|
340
|
+
// 应用亮度和对比度调整公式
|
|
341
|
+
const newValue = factor * (data[i + j] + briAdjust - 128) + 128;
|
|
342
|
+
data[i + j] = Math.max(0, Math.min(255, newValue));
|
|
322
343
|
}
|
|
323
|
-
|
|
324
|
-
return imgData;
|
|
344
|
+
// Alpha 通道保持不变
|
|
325
345
|
}
|
|
326
346
|
return imageData;
|
|
327
347
|
}
|
|
328
348
|
/**
|
|
329
|
-
*
|
|
349
|
+
* 将图像转换为灰度图
|
|
330
350
|
*
|
|
331
|
-
* @
|
|
332
|
-
* @
|
|
333
|
-
* @returns {number} 截断后的值,范围为0-255
|
|
351
|
+
* @param imageData 原始图像数据
|
|
352
|
+
* @returns 灰度图像数据
|
|
334
353
|
*/
|
|
335
|
-
static
|
|
336
|
-
|
|
354
|
+
static toGrayscale(imageData) {
|
|
355
|
+
const data = imageData.data;
|
|
356
|
+
const length = data.length;
|
|
357
|
+
for (let i = 0; i < length; i += 4) {
|
|
358
|
+
// 使用加权平均法将 RGB 转换为灰度值
|
|
359
|
+
const gray = data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11;
|
|
360
|
+
data[i] = data[i + 1] = data[i + 2] = gray;
|
|
361
|
+
}
|
|
362
|
+
return imageData;
|
|
337
363
|
}
|
|
338
364
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
* 灰度转换可以简化图像,提高OCR和条形码识别的准确率
|
|
365
|
+
* 锐化图像
|
|
342
366
|
*
|
|
343
|
-
* @param
|
|
344
|
-
* @
|
|
367
|
+
* @param imageData 原始图像数据
|
|
368
|
+
* @param amount 锐化程度,默认为2
|
|
369
|
+
* @returns 锐化后的图像数据
|
|
345
370
|
*/
|
|
346
|
-
static
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
371
|
+
static sharpen(imageData, amount = 2) {
|
|
372
|
+
if (!imageData || !imageData.data)
|
|
373
|
+
return imageData;
|
|
374
|
+
const width = imageData.width;
|
|
375
|
+
const height = imageData.height;
|
|
376
|
+
const data = imageData.data;
|
|
377
|
+
const outputData = new Uint8ClampedArray(data.length);
|
|
378
|
+
// 锐化卷积核
|
|
379
|
+
const kernel = [
|
|
380
|
+
0,
|
|
381
|
+
-amount,
|
|
382
|
+
0,
|
|
383
|
+
-amount,
|
|
384
|
+
1 + 4 * amount,
|
|
385
|
+
-amount,
|
|
386
|
+
0,
|
|
387
|
+
-amount,
|
|
388
|
+
0,
|
|
389
|
+
];
|
|
390
|
+
// 应用卷积
|
|
391
|
+
for (let y = 1; y < height - 1; y++) {
|
|
392
|
+
for (let x = 1; x < width - 1; x++) {
|
|
393
|
+
const pos = (y * width + x) * 4;
|
|
394
|
+
// 对每个通道应用卷积
|
|
395
|
+
for (let c = 0; c < 3; c++) {
|
|
396
|
+
let val = 0;
|
|
397
|
+
for (let ky = -1; ky <= 1; ky++) {
|
|
398
|
+
for (let kx = -1; kx <= 1; kx++) {
|
|
399
|
+
const kernelPos = (ky + 1) * 3 + (kx + 1);
|
|
400
|
+
const dataPos = ((y + ky) * width + (x + kx)) * 4 + c;
|
|
401
|
+
val += data[dataPos] * kernel[kernelPos];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
outputData[pos + c] = Math.max(0, Math.min(255, val));
|
|
405
|
+
}
|
|
406
|
+
outputData[pos + 3] = data[pos + 3]; // 保持透明度不变
|
|
358
407
|
}
|
|
359
|
-
ctx.putImageData(imgData, 0, 0);
|
|
360
|
-
return imgData;
|
|
361
408
|
}
|
|
362
|
-
|
|
409
|
+
// 处理边缘像素
|
|
410
|
+
for (let y = 0; y < height; y++) {
|
|
411
|
+
for (let x = 0; x < width; x++) {
|
|
412
|
+
if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
|
|
413
|
+
const pos = (y * width + x) * 4;
|
|
414
|
+
outputData[pos] = data[pos];
|
|
415
|
+
outputData[pos + 1] = data[pos + 1];
|
|
416
|
+
outputData[pos + 2] = data[pos + 2];
|
|
417
|
+
outputData[pos + 3] = data[pos + 3];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// 创建新的ImageData对象
|
|
422
|
+
return new ImageData(outputData, width, height);
|
|
363
423
|
}
|
|
364
424
|
/**
|
|
365
|
-
*
|
|
425
|
+
* 对图像应用阈值操作,增强对比度
|
|
366
426
|
*
|
|
367
|
-
* @param
|
|
368
|
-
* @param
|
|
369
|
-
* @
|
|
370
|
-
* @returns {ImageData} 调整大小后的图像数据
|
|
427
|
+
* @param imageData 原始图像数据
|
|
428
|
+
* @param threshold 阈值 (0-255)
|
|
429
|
+
* @returns 处理后的图像数据
|
|
371
430
|
*/
|
|
372
|
-
static
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
431
|
+
static threshold(imageData, threshold = 128) {
|
|
432
|
+
// 先转换为灰度图
|
|
433
|
+
const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
|
|
434
|
+
const data = grayscaleImage.data;
|
|
435
|
+
const length = data.length;
|
|
436
|
+
for (let i = 0; i < length; i += 4) {
|
|
437
|
+
// 二值化处理
|
|
438
|
+
const value = data[i] < threshold ? 0 : 255;
|
|
439
|
+
data[i] = data[i + 1] = data[i + 2] = value;
|
|
381
440
|
}
|
|
382
|
-
return
|
|
441
|
+
return grayscaleImage;
|
|
383
442
|
}
|
|
384
443
|
/**
|
|
385
|
-
*
|
|
444
|
+
* 将图像转换为黑白图像(二值化)
|
|
386
445
|
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* @param {ImageData} imageData - 原图像数据
|
|
390
|
-
* @param {number} [maxDimension=1000] - 目标最大尺寸(宽或高)
|
|
391
|
-
* @returns {ImageData} 处理后的图像数据
|
|
446
|
+
* @param imageData 原始图像数据
|
|
447
|
+
* @returns 二值化后的图像数据
|
|
392
448
|
*/
|
|
393
|
-
static
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
449
|
+
static toBinaryImage(imageData) {
|
|
450
|
+
// 先转换为灰度图
|
|
451
|
+
const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
|
|
452
|
+
// 使用OTSU算法自动确定阈值
|
|
453
|
+
const threshold = this.getOtsuThreshold(grayscaleImage);
|
|
454
|
+
return this.threshold(grayscaleImage, threshold);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* 使用OTSU算法计算最佳阈值
|
|
458
|
+
*
|
|
459
|
+
* @param imageData 灰度图像数据
|
|
460
|
+
* @returns 最佳阈值
|
|
461
|
+
*/
|
|
462
|
+
static getOtsuThreshold(imageData) {
|
|
463
|
+
const data = imageData.data;
|
|
464
|
+
const histogram = new Array(256).fill(0);
|
|
465
|
+
// 统计灰度直方图
|
|
466
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
467
|
+
histogram[data[i]]++;
|
|
468
|
+
}
|
|
469
|
+
const total = imageData.width * imageData.height;
|
|
470
|
+
let sum = 0;
|
|
471
|
+
// 计算总灰度值和
|
|
472
|
+
for (let i = 0; i < 256; i++) {
|
|
473
|
+
sum += i * histogram[i];
|
|
474
|
+
}
|
|
475
|
+
let sumB = 0;
|
|
476
|
+
let wB = 0;
|
|
477
|
+
let wF = 0;
|
|
478
|
+
let maxVariance = 0;
|
|
479
|
+
let threshold = 0;
|
|
480
|
+
// 遍历所有可能的阈值,找到最大类间方差
|
|
481
|
+
for (let t = 0; t < 256; t++) {
|
|
482
|
+
wB += histogram[t]; // 背景权重
|
|
483
|
+
if (wB === 0)
|
|
484
|
+
continue;
|
|
485
|
+
wF = total - wB; // 前景权重
|
|
486
|
+
if (wF === 0)
|
|
487
|
+
break;
|
|
488
|
+
sumB += t * histogram[t];
|
|
489
|
+
const mB = sumB / wB; // 背景平均灰度
|
|
490
|
+
const mF = (sum - sumB) / wF; // 前景平均灰度
|
|
491
|
+
// 计算类间方差
|
|
492
|
+
const variance = wB * wF * (mB - mF) * (mB - mF);
|
|
493
|
+
if (variance > maxVariance) {
|
|
494
|
+
maxVariance = variance;
|
|
495
|
+
threshold = t;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return threshold;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* 批量应用图像处理
|
|
502
|
+
*
|
|
503
|
+
* @param imageData 原始图像数据
|
|
504
|
+
* @param options 处理选项
|
|
505
|
+
* @returns 处理后的图像数据
|
|
506
|
+
*/
|
|
507
|
+
static batchProcess(imageData, options) {
|
|
508
|
+
let processedImage = new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height);
|
|
509
|
+
// 应用亮度和对比度调整
|
|
510
|
+
if (options.brightness !== undefined || options.contrast !== undefined) {
|
|
511
|
+
processedImage = this.adjustBrightnessContrast(processedImage, options.brightness || 0, options.contrast || 0);
|
|
512
|
+
}
|
|
513
|
+
// 应用灰度转换
|
|
514
|
+
if (options.grayscale) {
|
|
515
|
+
processedImage = this.toGrayscale(processedImage);
|
|
516
|
+
}
|
|
517
|
+
// 应用锐化
|
|
518
|
+
if (options.sharpen) {
|
|
519
|
+
processedImage = this.sharpen(processedImage);
|
|
520
|
+
}
|
|
521
|
+
// 应用颜色反转
|
|
522
|
+
if (options.invert) {
|
|
523
|
+
const data = processedImage.data;
|
|
524
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
525
|
+
// 反转RGB值
|
|
526
|
+
data[i] = 255 - data[i];
|
|
527
|
+
data[i + 1] = 255 - data[i + 1];
|
|
528
|
+
data[i + 2] = 255 - data[i + 2];
|
|
529
|
+
// Alpha通道保持不变
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return processedImage;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* 压缩图片文件
|
|
536
|
+
*
|
|
537
|
+
* @param file 图片文件
|
|
538
|
+
* @param options 压缩选项
|
|
539
|
+
* @returns Promise<File> 压缩后的文件
|
|
540
|
+
*/
|
|
541
|
+
static async compressImage(file, options) {
|
|
542
|
+
const defaultOptions = {
|
|
543
|
+
maxSizeMB: 1,
|
|
544
|
+
maxWidthOrHeight: 1920,
|
|
545
|
+
useWebWorker: true,
|
|
546
|
+
quality: 0.8,
|
|
547
|
+
fileType: file.type || "image/jpeg",
|
|
548
|
+
};
|
|
549
|
+
const compressOptions = { ...defaultOptions, ...options };
|
|
550
|
+
try {
|
|
551
|
+
return await imageCompression(file, compressOptions);
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
console.error("图片压缩失败:", error);
|
|
555
|
+
return file; // 如果压缩失败,返回原始文件
|
|
398
556
|
}
|
|
399
|
-
// 计算缩放比例,保持宽高比
|
|
400
|
-
const scale = maxDimension / Math.max(width, height);
|
|
401
|
-
const newWidth = Math.round(width * scale);
|
|
402
|
-
const newHeight = Math.round(height * scale);
|
|
403
|
-
// 调整图像大小
|
|
404
|
-
return this.resize(imageData, newWidth, newHeight);
|
|
405
557
|
}
|
|
406
558
|
/**
|
|
407
|
-
*
|
|
559
|
+
* 从图片文件创建ImageData
|
|
408
560
|
*
|
|
409
|
-
* @param
|
|
410
|
-
* @returns
|
|
561
|
+
* @param file 图片文件
|
|
562
|
+
* @returns Promise<ImageData>
|
|
411
563
|
*/
|
|
412
|
-
static
|
|
413
|
-
|
|
414
|
-
|
|
564
|
+
static async createImageDataFromFile(file) {
|
|
565
|
+
return new Promise((resolve, reject) => {
|
|
566
|
+
try {
|
|
567
|
+
const img = new Image();
|
|
568
|
+
const url = URL.createObjectURL(file);
|
|
569
|
+
img.onload = () => {
|
|
570
|
+
try {
|
|
571
|
+
// 创建canvas元素
|
|
572
|
+
const canvas = document.createElement("canvas");
|
|
573
|
+
const ctx = canvas.getContext("2d");
|
|
574
|
+
if (!ctx) {
|
|
575
|
+
reject(new Error("无法创建2D上下文"));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
canvas.width = img.width;
|
|
579
|
+
canvas.height = img.height;
|
|
580
|
+
// 绘制图片到canvas
|
|
581
|
+
ctx.drawImage(img, 0, 0);
|
|
582
|
+
// 获取图像数据
|
|
583
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
584
|
+
// 释放资源
|
|
585
|
+
URL.revokeObjectURL(url);
|
|
586
|
+
resolve(imageData);
|
|
587
|
+
}
|
|
588
|
+
catch (e) {
|
|
589
|
+
reject(e);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
img.onerror = () => {
|
|
593
|
+
URL.revokeObjectURL(url);
|
|
594
|
+
reject(new Error("图片加载失败"));
|
|
595
|
+
};
|
|
596
|
+
img.src = url;
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
reject(error);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
415
602
|
}
|
|
416
603
|
/**
|
|
417
|
-
*
|
|
604
|
+
* 将ImageData转换为File对象
|
|
418
605
|
*
|
|
419
|
-
* @param
|
|
420
|
-
* @
|
|
606
|
+
* @param imageData ImageData对象
|
|
607
|
+
* @param fileName 输出文件名
|
|
608
|
+
* @param fileType 输出文件类型
|
|
609
|
+
* @param quality 图片质量 (0-1)
|
|
610
|
+
* @returns Promise<File>
|
|
421
611
|
*/
|
|
422
|
-
static async
|
|
612
|
+
static async imageDataToFile(imageData, fileName = "image.jpg", fileType = "image/jpeg", quality = 0.8) {
|
|
423
613
|
return new Promise((resolve, reject) => {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
canvas.
|
|
428
|
-
|
|
429
|
-
const ctx = canvas.getContext('2d');
|
|
614
|
+
try {
|
|
615
|
+
const canvas = document.createElement("canvas");
|
|
616
|
+
canvas.width = imageData.width;
|
|
617
|
+
canvas.height = imageData.height;
|
|
618
|
+
const ctx = canvas.getContext("2d");
|
|
430
619
|
if (!ctx) {
|
|
431
|
-
reject(new Error(
|
|
620
|
+
reject(new Error("无法创建2D上下文"));
|
|
432
621
|
return;
|
|
433
622
|
}
|
|
434
|
-
ctx.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
623
|
+
ctx.putImageData(imageData, 0, 0);
|
|
624
|
+
canvas.toBlob((blob) => {
|
|
625
|
+
if (!blob) {
|
|
626
|
+
reject(new Error("无法创建图片Blob"));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const file = new File([blob], fileName, { type: fileType });
|
|
630
|
+
resolve(file);
|
|
631
|
+
}, fileType, quality);
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
reject(error);
|
|
635
|
+
}
|
|
442
636
|
});
|
|
443
637
|
}
|
|
444
638
|
/**
|
|
445
|
-
*
|
|
446
|
-
* 此方法将图像分割为多个部分,并行处理以提高性能
|
|
639
|
+
* 调整图像大小
|
|
447
640
|
*
|
|
448
|
-
* @param
|
|
449
|
-
* @param
|
|
450
|
-
* @param
|
|
451
|
-
* @
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const isWide = imageData.width > imageData.height;
|
|
468
|
-
const chunkSize = Math.floor((isWide ? imageData.width : imageData.height) / chunks);
|
|
469
|
-
// 创建Worker处理每个块
|
|
470
|
-
const promises = [];
|
|
471
|
-
for (let i = 0; i < chunks; i++) {
|
|
472
|
-
const chunkCanvas = document.createElement('canvas');
|
|
473
|
-
const chunkCtx = chunkCanvas.getContext('2d');
|
|
474
|
-
if (!chunkCtx)
|
|
475
|
-
continue;
|
|
476
|
-
let chunkImageData;
|
|
477
|
-
if (isWide) {
|
|
478
|
-
// 水平分割
|
|
479
|
-
const startX = i * chunkSize;
|
|
480
|
-
const width = (i === chunks - 1) ? imageData.width - startX : chunkSize;
|
|
481
|
-
chunkCanvas.width = width;
|
|
482
|
-
chunkCanvas.height = imageData.height;
|
|
483
|
-
// 复制原图像数据到分块
|
|
484
|
-
const tempCanvas = this.imageDataToCanvas(imageData);
|
|
485
|
-
chunkCtx.drawImage(tempCanvas, startX, 0, width, imageData.height, 0, 0, width, imageData.height);
|
|
486
|
-
chunkImageData = chunkCtx.getImageData(0, 0, width, imageData.height);
|
|
487
|
-
}
|
|
488
|
-
else {
|
|
489
|
-
// 垂直分割
|
|
490
|
-
const startY = i * chunkSize;
|
|
491
|
-
const height = (i === chunks - 1) ? imageData.height - startY : chunkSize;
|
|
492
|
-
chunkCanvas.width = imageData.width;
|
|
493
|
-
chunkCanvas.height = height;
|
|
494
|
-
// 复制原图像数据到分块
|
|
495
|
-
const tempCanvas = this.imageDataToCanvas(imageData);
|
|
496
|
-
chunkCtx.drawImage(tempCanvas, 0, startY, imageData.width, height, 0, 0, imageData.width, height);
|
|
497
|
-
chunkImageData = chunkCtx.getImageData(0, 0, imageData.width, height);
|
|
498
|
-
}
|
|
499
|
-
// 使用Worker处理
|
|
500
|
-
const workerCode = `
|
|
501
|
-
self.onmessage = function(e) {
|
|
502
|
-
const imageData = e.data.imageData;
|
|
503
|
-
const processingFunction = ${processingFunction.toString()};
|
|
504
|
-
const result = processingFunction(imageData);
|
|
505
|
-
self.postMessage({ result, index: e.data.index }, [result.data.buffer]);
|
|
506
|
-
}
|
|
507
|
-
`;
|
|
508
|
-
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
509
|
-
const workerUrl = URL.createObjectURL(blob);
|
|
510
|
-
const worker = new Worker(workerUrl);
|
|
511
|
-
const promise = new Promise((resolve) => {
|
|
512
|
-
worker.onmessage = function (e) {
|
|
513
|
-
resolve(e.data);
|
|
514
|
-
worker.terminate();
|
|
515
|
-
URL.revokeObjectURL(workerUrl);
|
|
516
|
-
};
|
|
517
|
-
// 传输数据
|
|
518
|
-
worker.postMessage({
|
|
519
|
-
imageData: chunkImageData,
|
|
520
|
-
index: i
|
|
521
|
-
}, [chunkImageData.data.buffer]);
|
|
522
|
-
});
|
|
523
|
-
promises.push(promise);
|
|
524
|
-
}
|
|
525
|
-
// 等待所有Worker完成并组合结果
|
|
526
|
-
const results = await Promise.all(promises);
|
|
527
|
-
// 按索引排序结果
|
|
528
|
-
results.sort((a, b) => a.index - b.index);
|
|
529
|
-
// 将处理后的块绘制到结果canvas
|
|
530
|
-
for (let i = 0; i < results.length; i++) {
|
|
531
|
-
const { result } = results[i];
|
|
532
|
-
const tempCanvas = this.imageDataToCanvas(result);
|
|
533
|
-
if (isWide) {
|
|
534
|
-
const startX = i * chunkSize;
|
|
535
|
-
resultCtx.drawImage(tempCanvas, startX, 0);
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
const startY = i * chunkSize;
|
|
539
|
-
resultCtx.drawImage(tempCanvas, 0, startY);
|
|
540
|
-
}
|
|
641
|
+
* @param imageData 原始图像数据
|
|
642
|
+
* @param maxWidth 最大宽度
|
|
643
|
+
* @param maxHeight 最大高度
|
|
644
|
+
* @param maintainAspectRatio 是否保持宽高比
|
|
645
|
+
* @returns ImageData 调整大小后的图像数据
|
|
646
|
+
*/
|
|
647
|
+
static resizeImage(imageData, maxWidth, maxHeight, maintainAspectRatio = true) {
|
|
648
|
+
const { width, height } = imageData;
|
|
649
|
+
// 如果图像已经小于指定大小,则不需要调整
|
|
650
|
+
if (width <= maxWidth && height <= maxHeight) {
|
|
651
|
+
return imageData;
|
|
652
|
+
}
|
|
653
|
+
let newWidth = maxWidth;
|
|
654
|
+
let newHeight = maxHeight;
|
|
655
|
+
// 计算新的尺寸,保持宽高比
|
|
656
|
+
if (maintainAspectRatio) {
|
|
657
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
658
|
+
newWidth = Math.floor(width * ratio);
|
|
659
|
+
newHeight = Math.floor(height * ratio);
|
|
541
660
|
}
|
|
542
|
-
|
|
661
|
+
// 创建用于调整大小的Canvas
|
|
662
|
+
const canvas = document.createElement("canvas");
|
|
663
|
+
canvas.width = newWidth;
|
|
664
|
+
canvas.height = newHeight;
|
|
665
|
+
const ctx = canvas.getContext("2d");
|
|
666
|
+
if (!ctx) {
|
|
667
|
+
throw new Error("无法创建2D上下文");
|
|
668
|
+
}
|
|
669
|
+
// 创建临时Canvas绘制原始ImageData
|
|
670
|
+
const tempCanvas = document.createElement("canvas");
|
|
671
|
+
tempCanvas.width = width;
|
|
672
|
+
tempCanvas.height = height;
|
|
673
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
674
|
+
if (!tempCtx) {
|
|
675
|
+
throw new Error("无法创建临时2D上下文");
|
|
676
|
+
}
|
|
677
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
678
|
+
// 使用缩放平滑算法
|
|
679
|
+
ctx.imageSmoothingEnabled = true;
|
|
680
|
+
ctx.imageSmoothingQuality = "high";
|
|
681
|
+
// 绘制调整大小的图像
|
|
682
|
+
ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, newWidth, newHeight);
|
|
683
|
+
// 获取新的ImageData
|
|
684
|
+
return ctx.getImageData(0, 0, newWidth, newHeight);
|
|
543
685
|
}
|
|
544
686
|
}
|
|
545
687
|
|
|
@@ -587,7 +729,7 @@
|
|
|
587
729
|
this.scanTimer = null;
|
|
588
730
|
this.options = {
|
|
589
731
|
scanInterval: 200,
|
|
590
|
-
...options
|
|
732
|
+
...options,
|
|
591
733
|
};
|
|
592
734
|
this.camera = new Camera();
|
|
593
735
|
}
|
|
@@ -635,7 +777,7 @@
|
|
|
635
777
|
this.detectBarcode(enhancedImage);
|
|
636
778
|
}
|
|
637
779
|
catch (error) {
|
|
638
|
-
console.error(
|
|
780
|
+
console.error("条形码扫描错误:", error);
|
|
639
781
|
}
|
|
640
782
|
}
|
|
641
783
|
this.scanTimer = window.setTimeout(() => this.scan(), this.options.scanInterval);
|
|
@@ -652,10 +794,10 @@
|
|
|
652
794
|
// 这里应集成条形码识别库
|
|
653
795
|
// 如 ZXing 或 QuaggaJS
|
|
654
796
|
// 简化示例,实际项目中请替换为真实实现
|
|
655
|
-
console.log(
|
|
797
|
+
console.log("正在扫描条形码...");
|
|
656
798
|
// 模拟找到条形码
|
|
657
799
|
if (Math.random() > 0.95) {
|
|
658
|
-
const mockResult =
|
|
800
|
+
const mockResult = "6901234567890"; // 模拟条形码结果
|
|
659
801
|
if (this.options.onScan) {
|
|
660
802
|
this.options.onScan(mockResult);
|
|
661
803
|
}
|
|
@@ -674,6 +816,71 @@
|
|
|
674
816
|
}
|
|
675
817
|
this.camera.release();
|
|
676
818
|
}
|
|
819
|
+
/**
|
|
820
|
+
* 处理图像数据中的条形码
|
|
821
|
+
*
|
|
822
|
+
* @param {ImageData} imageData - 要处理的图像数据
|
|
823
|
+
* @returns {string | null} 识别到的条形码内容,如未识别到则返回null
|
|
824
|
+
*/
|
|
825
|
+
processImageData(imageData) {
|
|
826
|
+
try {
|
|
827
|
+
if (!imageData ||
|
|
828
|
+
!imageData.data ||
|
|
829
|
+
imageData.width <= 0 ||
|
|
830
|
+
imageData.height <= 0) {
|
|
831
|
+
throw new Error("无效的图像数据");
|
|
832
|
+
}
|
|
833
|
+
// 图像预处理,提高识别率
|
|
834
|
+
const enhancedImage = ImageProcessor.adjustBrightnessContrast(ImageProcessor.toGrayscale(imageData), 10, // 亮度
|
|
835
|
+
20 // 对比度
|
|
836
|
+
);
|
|
837
|
+
// 注意:这里是简化实现
|
|
838
|
+
// 实际项目中,应该集成专门的条形码识别库如ZXing或Quagga.js
|
|
839
|
+
// 模拟条形码识别
|
|
840
|
+
// 在真实项目中,请替换为实际的条形码识别算法
|
|
841
|
+
const result = this.simulateBarcodeDetection(enhancedImage);
|
|
842
|
+
return result;
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
if (this.options.onError) {
|
|
846
|
+
this.options.onError(error instanceof Error ? error : new Error(String(error)));
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* 模拟条形码检测
|
|
853
|
+
* 仅用于演示,实际使用时应该替换为真实的条形码识别算法
|
|
854
|
+
*
|
|
855
|
+
* @private
|
|
856
|
+
* @param {ImageData} imageData - 要检测条形码的图像数据
|
|
857
|
+
* @returns {string | null} 模拟的条形码识别结果
|
|
858
|
+
*/
|
|
859
|
+
simulateBarcodeDetection(imageData) {
|
|
860
|
+
// 这里只是模拟,真实环境中应当使用条形码识别库进行识别
|
|
861
|
+
// 在中间区域检测到足够多垂直边缘时,认为可能存在条形码
|
|
862
|
+
const midX = Math.floor(imageData.width / 2);
|
|
863
|
+
const midY = Math.floor(imageData.height / 2);
|
|
864
|
+
const sampleWidth = Math.min(100, Math.floor(imageData.width / 3));
|
|
865
|
+
let edgeCount = 0;
|
|
866
|
+
let lastPixel = 0;
|
|
867
|
+
// 简单的边缘检测,统计中心水平线上像素变化次数
|
|
868
|
+
for (let x = midX - sampleWidth / 2; x < midX + sampleWidth / 2; x++) {
|
|
869
|
+
const pixelPos = (midY * imageData.width + x) * 4;
|
|
870
|
+
const pixelValue = imageData.data[pixelPos];
|
|
871
|
+
if (Math.abs(pixelValue - lastPixel) > 30) {
|
|
872
|
+
edgeCount++;
|
|
873
|
+
}
|
|
874
|
+
lastPixel = pixelValue;
|
|
875
|
+
}
|
|
876
|
+
// 如果边缘变化次数在合理范围内,认为是条形码
|
|
877
|
+
// 实际的条形码具有规律的宽窄条纹
|
|
878
|
+
if (edgeCount > 10 && edgeCount < 50) {
|
|
879
|
+
// 生成一个模拟的条形码结果
|
|
880
|
+
return "690" + Math.floor(Math.random() * 10000000000);
|
|
881
|
+
}
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
677
884
|
}
|
|
678
885
|
|
|
679
886
|
/**
|
|
@@ -877,7 +1084,7 @@
|
|
|
877
1084
|
this.frameCount = 0;
|
|
878
1085
|
this.lastDetectionTime = 0;
|
|
879
1086
|
this.camera = new Camera();
|
|
880
|
-
if (typeof options ===
|
|
1087
|
+
if (typeof options === "function") {
|
|
881
1088
|
// 兼容旧的构造函数方式
|
|
882
1089
|
this.onDetected = options;
|
|
883
1090
|
this.options = {
|
|
@@ -885,7 +1092,7 @@
|
|
|
885
1092
|
maxImageDimension: 800,
|
|
886
1093
|
enableCache: true,
|
|
887
1094
|
cacheSize: 20,
|
|
888
|
-
logger: console.log
|
|
1095
|
+
logger: console.log,
|
|
889
1096
|
};
|
|
890
1097
|
}
|
|
891
1098
|
else if (options) {
|
|
@@ -896,7 +1103,7 @@
|
|
|
896
1103
|
enableCache: true,
|
|
897
1104
|
cacheSize: 20,
|
|
898
1105
|
logger: console.log,
|
|
899
|
-
...options
|
|
1106
|
+
...options,
|
|
900
1107
|
};
|
|
901
1108
|
this.onDetected = options.onDetection;
|
|
902
1109
|
this.onError = options.onError;
|
|
@@ -907,7 +1114,7 @@
|
|
|
907
1114
|
maxImageDimension: 800,
|
|
908
1115
|
enableCache: true,
|
|
909
1116
|
cacheSize: 20,
|
|
910
|
-
logger: console.log
|
|
1117
|
+
logger: console.log,
|
|
911
1118
|
};
|
|
912
1119
|
}
|
|
913
1120
|
this.detectionInterval = this.options.detectionInterval;
|
|
@@ -956,7 +1163,8 @@
|
|
|
956
1163
|
const now = performance.now();
|
|
957
1164
|
// 帧率控制 - 只有满足时间间隔的帧才进行检测
|
|
958
1165
|
// 这样可以显著减少CPU使用率,同时保持良好的用户体验
|
|
959
|
-
if (this.frameCount % 3 === 0 ||
|
|
1166
|
+
if (this.frameCount % 3 === 0 ||
|
|
1167
|
+
now - this.lastDetectionTime >= this.detectionInterval) {
|
|
960
1168
|
this.throttledDetect();
|
|
961
1169
|
this.lastDetectionTime = now;
|
|
962
1170
|
}
|
|
@@ -968,7 +1176,7 @@
|
|
|
968
1176
|
this.onError(error);
|
|
969
1177
|
}
|
|
970
1178
|
else {
|
|
971
|
-
console.error(
|
|
1179
|
+
console.error("身份证检测错误:", error);
|
|
972
1180
|
}
|
|
973
1181
|
// 出错后延迟重试
|
|
974
1182
|
setTimeout(() => {
|
|
@@ -996,11 +1204,11 @@
|
|
|
996
1204
|
const fingerprint = calculateImageFingerprint(frame, 16); // 使用更大的尺寸提高特征区分度
|
|
997
1205
|
const cachedResult = this.resultCache.get(fingerprint);
|
|
998
1206
|
if (cachedResult) {
|
|
999
|
-
this.options.logger?.(
|
|
1207
|
+
this.options.logger?.("使用缓存的检测结果");
|
|
1000
1208
|
// 使用缓存结果,但更新图像数据以确保最新
|
|
1001
1209
|
const updatedResult = {
|
|
1002
1210
|
...cachedResult,
|
|
1003
|
-
imageData: frame
|
|
1211
|
+
imageData: frame,
|
|
1004
1212
|
};
|
|
1005
1213
|
if (this.onDetected) {
|
|
1006
1214
|
this.onDetected(updatedResult);
|
|
@@ -1009,7 +1217,7 @@
|
|
|
1009
1217
|
}
|
|
1010
1218
|
}
|
|
1011
1219
|
// 降低分辨率以提高性能
|
|
1012
|
-
const downsampledFrame = ImageProcessor.
|
|
1220
|
+
const downsampledFrame = ImageProcessor.resizeImage(frame, this.maxImageDimension, this.maxImageDimension);
|
|
1013
1221
|
try {
|
|
1014
1222
|
// 检测身份证
|
|
1015
1223
|
const result = await this.detectIDCard(downsampledFrame);
|
|
@@ -1032,7 +1240,7 @@
|
|
|
1032
1240
|
this.onError(error);
|
|
1033
1241
|
}
|
|
1034
1242
|
else {
|
|
1035
|
-
console.error(
|
|
1243
|
+
console.error("身份证检测错误:", error);
|
|
1036
1244
|
}
|
|
1037
1245
|
}
|
|
1038
1246
|
}
|
|
@@ -1053,7 +1261,7 @@
|
|
|
1053
1261
|
// 在实际应用中,此处应当实现实际的计算机视觉算法
|
|
1054
1262
|
const detectionResult = {
|
|
1055
1263
|
success: Math.random() > 0.3, // 70%的概率成功检测到
|
|
1056
|
-
message:
|
|
1264
|
+
message: "身份证检测完成",
|
|
1057
1265
|
};
|
|
1058
1266
|
if (detectionResult.success) {
|
|
1059
1267
|
// 模拟一个身份证矩形区域
|
|
@@ -1069,20 +1277,20 @@
|
|
|
1069
1277
|
{ x: rectX, y: rectY },
|
|
1070
1278
|
{ x: rectX + rectWidth, y: rectY },
|
|
1071
1279
|
{ x: rectX + rectWidth, y: rectY + rectHeight },
|
|
1072
|
-
{ x: rectX, y: rectY + rectHeight }
|
|
1280
|
+
{ x: rectX, y: rectY + rectHeight },
|
|
1073
1281
|
];
|
|
1074
1282
|
// 添加边界框
|
|
1075
1283
|
detectionResult.boundingBox = {
|
|
1076
1284
|
x: rectX,
|
|
1077
1285
|
y: rectY,
|
|
1078
1286
|
width: rectWidth,
|
|
1079
|
-
height: rectHeight
|
|
1287
|
+
height: rectHeight,
|
|
1080
1288
|
};
|
|
1081
1289
|
// 裁剪身份证图像
|
|
1082
|
-
const canvas = document.createElement(
|
|
1290
|
+
const canvas = document.createElement("canvas");
|
|
1083
1291
|
canvas.width = rectWidth;
|
|
1084
1292
|
canvas.height = rectHeight;
|
|
1085
|
-
const ctx = canvas.getContext(
|
|
1293
|
+
const ctx = canvas.getContext("2d");
|
|
1086
1294
|
if (ctx) {
|
|
1087
1295
|
const tempCanvas = ImageProcessor.imageDataToCanvas(imageData);
|
|
1088
1296
|
ctx.drawImage(tempCanvas, rectX, rectY, rectWidth, rectHeight, 0, 0, rectWidth, rectHeight);
|
|
@@ -1098,7 +1306,7 @@
|
|
|
1098
1306
|
*/
|
|
1099
1307
|
clearCache() {
|
|
1100
1308
|
this.resultCache.clear();
|
|
1101
|
-
this.options.logger?.(
|
|
1309
|
+
this.options.logger?.("检测结果缓存已清除");
|
|
1102
1310
|
}
|
|
1103
1311
|
/**
|
|
1104
1312
|
* 释放资源
|
|
@@ -1204,16 +1412,16 @@
|
|
|
1204
1412
|
// 加载Tesseract.js (Worker 环境下动态导入)
|
|
1205
1413
|
const { createWorker } = await import('tesseract.js');
|
|
1206
1414
|
// 创建OCR Worker
|
|
1207
|
-
const worker = await createWorker(input.tessWorkerOptions || {
|
|
1208
|
-
logger: (m) => console.log(m)
|
|
1209
|
-
}); // 添加类型断言,避免TypeScript错误
|
|
1415
|
+
const worker = (await createWorker(input.tessWorkerOptions || {
|
|
1416
|
+
logger: (m) => console.log(m),
|
|
1417
|
+
})); // 添加类型断言,避免TypeScript错误
|
|
1210
1418
|
try {
|
|
1211
1419
|
// 初始化OCR引擎
|
|
1212
1420
|
await worker.load();
|
|
1213
|
-
await worker.loadLanguage(
|
|
1214
|
-
await worker.initialize(
|
|
1421
|
+
await worker.loadLanguage("chi_sim");
|
|
1422
|
+
await worker.initialize("chi_sim");
|
|
1215
1423
|
await worker.setParameters({
|
|
1216
|
-
tessedit_char_whitelist:
|
|
1424
|
+
tessedit_char_whitelist: "0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期",
|
|
1217
1425
|
});
|
|
1218
1426
|
// 识别图像
|
|
1219
1427
|
const { data } = await worker.recognize(input.imageBase64);
|
|
@@ -1226,7 +1434,7 @@
|
|
|
1226
1434
|
// 返回处理结果
|
|
1227
1435
|
return {
|
|
1228
1436
|
idCardInfo,
|
|
1229
|
-
processingTime
|
|
1437
|
+
processingTime,
|
|
1230
1438
|
};
|
|
1231
1439
|
}
|
|
1232
1440
|
catch (error) {
|
|
@@ -1247,7 +1455,7 @@
|
|
|
1247
1455
|
function parseIDCardText(text) {
|
|
1248
1456
|
const info = {};
|
|
1249
1457
|
// 拆分为行
|
|
1250
|
-
const lines = text.split(
|
|
1458
|
+
const lines = text.split("\n").filter((line) => line.trim());
|
|
1251
1459
|
// 解析身份证号码(最容易识别的部分)
|
|
1252
1460
|
const idNumberRegex = /(\d{17}[\dX])/;
|
|
1253
1461
|
const idNumberMatch = text.match(idNumberRegex);
|
|
@@ -1256,8 +1464,9 @@
|
|
|
1256
1464
|
}
|
|
1257
1465
|
// 解析姓名
|
|
1258
1466
|
for (const line of lines) {
|
|
1259
|
-
if (line.includes(
|
|
1260
|
-
|
|
1467
|
+
if (line.includes("姓名") ||
|
|
1468
|
+
(line.length < 10 && line.length > 1 && !/\d/.test(line))) {
|
|
1469
|
+
info.name = line.replace("姓名", "").trim();
|
|
1261
1470
|
break;
|
|
1262
1471
|
}
|
|
1263
1472
|
}
|
|
@@ -1267,7 +1476,9 @@
|
|
|
1267
1476
|
if (genderMatch) {
|
|
1268
1477
|
info.gender = genderMatch[1];
|
|
1269
1478
|
const nationalityText = genderMatch[0];
|
|
1270
|
-
info.nationality = nationalityText
|
|
1479
|
+
info.nationality = nationalityText
|
|
1480
|
+
.substring(nationalityText.indexOf(genderMatch[1]) + 1)
|
|
1481
|
+
.trim();
|
|
1271
1482
|
}
|
|
1272
1483
|
// 解析出生日期
|
|
1273
1484
|
const birthDateRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/;
|
|
@@ -1279,19 +1490,19 @@
|
|
|
1279
1490
|
const addressRegex = /住址([\s\S]*?)公民身份号码/;
|
|
1280
1491
|
const addressMatch = text.match(addressRegex);
|
|
1281
1492
|
if (addressMatch) {
|
|
1282
|
-
info.address = addressMatch[1].replace(/\n/g,
|
|
1493
|
+
info.address = addressMatch[1].replace(/\n/g, "").trim();
|
|
1283
1494
|
}
|
|
1284
1495
|
// 解析签发机关
|
|
1285
1496
|
const authorityRegex = /签发机关([\s\S]*?)有效期/;
|
|
1286
1497
|
const authorityMatch = text.match(authorityRegex);
|
|
1287
1498
|
if (authorityMatch) {
|
|
1288
|
-
info.issuingAuthority = authorityMatch[1].replace(/\n/g,
|
|
1499
|
+
info.issuingAuthority = authorityMatch[1].replace(/\n/g, "").trim();
|
|
1289
1500
|
}
|
|
1290
1501
|
// 解析有效期限
|
|
1291
1502
|
const validPeriodRegex = /有效期限([\s\S]*?)(-|至)/;
|
|
1292
1503
|
const validPeriodMatch = text.match(validPeriodRegex);
|
|
1293
1504
|
if (validPeriodMatch) {
|
|
1294
|
-
info.validPeriod = validPeriodMatch[0].replace(
|
|
1505
|
+
info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim();
|
|
1295
1506
|
}
|
|
1296
1507
|
return info;
|
|
1297
1508
|
}
|
|
@@ -1338,7 +1549,7 @@
|
|
|
1338
1549
|
cacheSize: 50,
|
|
1339
1550
|
maxImageDimension: 1000,
|
|
1340
1551
|
logger: console.log,
|
|
1341
|
-
...options
|
|
1552
|
+
...options,
|
|
1342
1553
|
};
|
|
1343
1554
|
// 初始化缓存
|
|
1344
1555
|
this.resultCache = new LRUCache(this.options.cacheSize);
|
|
@@ -1357,21 +1568,21 @@
|
|
|
1357
1568
|
// 使用自定义Worker线程处理OCR
|
|
1358
1569
|
this.ocrWorker = createWorker(processOCRInWorker);
|
|
1359
1570
|
this.initialized = true;
|
|
1360
|
-
this.options.logger?.(
|
|
1571
|
+
this.options.logger?.("OCR Worker 初始化完成");
|
|
1361
1572
|
}
|
|
1362
1573
|
else {
|
|
1363
1574
|
// 使用主线程处理OCR
|
|
1364
1575
|
this.worker = tesseract_js.createWorker({
|
|
1365
|
-
logger: this.options.logger
|
|
1576
|
+
logger: this.options.logger,
|
|
1366
1577
|
});
|
|
1367
1578
|
await this.worker.load();
|
|
1368
|
-
await this.worker.loadLanguage(
|
|
1369
|
-
await this.worker.initialize(
|
|
1579
|
+
await this.worker.loadLanguage("chi_sim");
|
|
1580
|
+
await this.worker.initialize("chi_sim");
|
|
1370
1581
|
await this.worker.setParameters({
|
|
1371
|
-
tessedit_char_whitelist:
|
|
1582
|
+
tessedit_char_whitelist: "0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期",
|
|
1372
1583
|
});
|
|
1373
1584
|
this.initialized = true;
|
|
1374
|
-
this.options.logger?.(
|
|
1585
|
+
this.options.logger?.("OCR引擎初始化完成");
|
|
1375
1586
|
}
|
|
1376
1587
|
}
|
|
1377
1588
|
/**
|
|
@@ -1389,24 +1600,42 @@
|
|
|
1389
1600
|
// 检查缓存中是否有结果
|
|
1390
1601
|
const cachedResult = this.resultCache.get(fingerprint);
|
|
1391
1602
|
if (cachedResult) {
|
|
1392
|
-
this.options.logger?.(
|
|
1603
|
+
this.options.logger?.("使用缓存的OCR结果");
|
|
1393
1604
|
return cachedResult;
|
|
1394
1605
|
}
|
|
1395
1606
|
}
|
|
1396
|
-
//
|
|
1397
|
-
const downsampledImage = ImageProcessor.
|
|
1398
|
-
|
|
1607
|
+
// 调整图像大小以提高性能和准确性
|
|
1608
|
+
const downsampledImage = ImageProcessor.resizeImage(imageData, this.options.maxImageDimension || 1000, this.options.maxImageDimension || 1000, true // 保持宽高比
|
|
1609
|
+
);
|
|
1610
|
+
// 提高图像质量以获得更好的OCR结果
|
|
1611
|
+
const enhancedImage = ImageProcessor.batchProcess(downsampledImage, {
|
|
1612
|
+
brightness: this.options.brightness || 15,
|
|
1613
|
+
contrast: this.options.contrast || 25,
|
|
1614
|
+
sharpen: true,
|
|
1615
|
+
});
|
|
1616
|
+
// 转换为base64供Tesseract处理
|
|
1617
|
+
// 创建一个canvas元素
|
|
1618
|
+
const canvas = document.createElement("canvas");
|
|
1619
|
+
canvas.width = enhancedImage.width;
|
|
1620
|
+
canvas.height = enhancedImage.height;
|
|
1621
|
+
const ctx = canvas.getContext("2d");
|
|
1622
|
+
if (!ctx) {
|
|
1623
|
+
throw new Error("无法创建canvas上下文");
|
|
1624
|
+
}
|
|
1625
|
+
// 将ImageData绘制到canvas
|
|
1626
|
+
ctx.putImageData(enhancedImage, 0, 0);
|
|
1627
|
+
// 转换为Base64
|
|
1628
|
+
const base64Image = canvas.toDataURL("image/jpeg", 0.7);
|
|
1399
1629
|
// OCR识别
|
|
1400
1630
|
try {
|
|
1401
1631
|
let idCardInfo;
|
|
1402
1632
|
if (this.options.useWorker && this.ocrWorker) {
|
|
1403
1633
|
// 使用Worker线程处理
|
|
1404
|
-
const base64Image = ImageProcessor.imageDataToBase64(enhancedImage);
|
|
1405
1634
|
const result = await this.ocrWorker.postMessage({
|
|
1406
1635
|
imageBase64: base64Image,
|
|
1407
1636
|
tessWorkerOptions: {
|
|
1408
|
-
logger: this.options.logger
|
|
1409
|
-
}
|
|
1637
|
+
logger: this.options.logger,
|
|
1638
|
+
},
|
|
1410
1639
|
});
|
|
1411
1640
|
idCardInfo = result.idCardInfo;
|
|
1412
1641
|
this.options.logger?.(`OCR处理完成,用时: ${result.processingTime.toFixed(2)}ms`);
|
|
@@ -1446,7 +1675,7 @@
|
|
|
1446
1675
|
parseIDCardText(text) {
|
|
1447
1676
|
const info = {};
|
|
1448
1677
|
// 拆分为行
|
|
1449
|
-
const lines = text.split(
|
|
1678
|
+
const lines = text.split("\n").filter((line) => line.trim());
|
|
1450
1679
|
// 解析身份证号码(最容易识别的部分)
|
|
1451
1680
|
const idNumberRegex = /(\d{17}[\dX])/;
|
|
1452
1681
|
const idNumberMatch = text.match(idNumberRegex);
|
|
@@ -1455,8 +1684,9 @@
|
|
|
1455
1684
|
}
|
|
1456
1685
|
// 解析姓名
|
|
1457
1686
|
for (const line of lines) {
|
|
1458
|
-
if (line.includes(
|
|
1459
|
-
|
|
1687
|
+
if (line.includes("姓名") ||
|
|
1688
|
+
(line.length < 10 && line.length > 1 && !/\d/.test(line))) {
|
|
1689
|
+
info.name = line.replace("姓名", "").trim();
|
|
1460
1690
|
break;
|
|
1461
1691
|
}
|
|
1462
1692
|
}
|
|
@@ -1466,7 +1696,9 @@
|
|
|
1466
1696
|
if (genderMatch) {
|
|
1467
1697
|
info.gender = genderMatch[1];
|
|
1468
1698
|
const nationalityText = genderMatch[0];
|
|
1469
|
-
info.nationality = nationalityText
|
|
1699
|
+
info.nationality = nationalityText
|
|
1700
|
+
.substring(nationalityText.indexOf(genderMatch[1]) + 1)
|
|
1701
|
+
.trim();
|
|
1470
1702
|
}
|
|
1471
1703
|
// 解析出生日期
|
|
1472
1704
|
const birthDateRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/;
|
|
@@ -1478,19 +1710,19 @@
|
|
|
1478
1710
|
const addressRegex = /住址([\s\S]*?)公民身份号码/;
|
|
1479
1711
|
const addressMatch = text.match(addressRegex);
|
|
1480
1712
|
if (addressMatch) {
|
|
1481
|
-
info.address = addressMatch[1].replace(/\n/g,
|
|
1713
|
+
info.address = addressMatch[1].replace(/\n/g, "").trim();
|
|
1482
1714
|
}
|
|
1483
1715
|
// 解析签发机关
|
|
1484
1716
|
const authorityRegex = /签发机关([\s\S]*?)有效期/;
|
|
1485
1717
|
const authorityMatch = text.match(authorityRegex);
|
|
1486
1718
|
if (authorityMatch) {
|
|
1487
|
-
info.issuingAuthority = authorityMatch[1].replace(/\n/g,
|
|
1719
|
+
info.issuingAuthority = authorityMatch[1].replace(/\n/g, "").trim();
|
|
1488
1720
|
}
|
|
1489
1721
|
// 解析有效期限
|
|
1490
1722
|
const validPeriodRegex = /有效期限([\s\S]*?)(-|至)/;
|
|
1491
1723
|
const validPeriodMatch = text.match(validPeriodRegex);
|
|
1492
1724
|
if (validPeriodMatch) {
|
|
1493
|
-
info.validPeriod = validPeriodMatch[0].replace(
|
|
1725
|
+
info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim();
|
|
1494
1726
|
}
|
|
1495
1727
|
return info;
|
|
1496
1728
|
}
|
|
@@ -1499,7 +1731,7 @@
|
|
|
1499
1731
|
*/
|
|
1500
1732
|
clearCache() {
|
|
1501
1733
|
this.resultCache.clear();
|
|
1502
|
-
this.options.logger?.(
|
|
1734
|
+
this.options.logger?.("OCR结果缓存已清除");
|
|
1503
1735
|
}
|
|
1504
1736
|
/**
|
|
1505
1737
|
* 终止OCR引擎并释放资源
|
|
@@ -1516,7 +1748,7 @@
|
|
|
1516
1748
|
this.ocrWorker = null;
|
|
1517
1749
|
}
|
|
1518
1750
|
this.initialized = false;
|
|
1519
|
-
this.options.logger?.(
|
|
1751
|
+
this.options.logger?.("OCR引擎已终止");
|
|
1520
1752
|
}
|
|
1521
1753
|
/**
|
|
1522
1754
|
* 释放资源
|
|
@@ -1756,159 +1988,1521 @@
|
|
|
1756
1988
|
}
|
|
1757
1989
|
|
|
1758
1990
|
/**
|
|
1759
|
-
* @file
|
|
1760
|
-
* @description
|
|
1991
|
+
* @file 身份证防伪检测模块
|
|
1992
|
+
* @description 提供身份证防伪特征识别功能,区分真假身份证
|
|
1993
|
+
* @module AntiFakeDetector
|
|
1994
|
+
*/
|
|
1995
|
+
/**
|
|
1996
|
+
* 身份证防伪特征检测器
|
|
1997
|
+
*
|
|
1998
|
+
* 基于图像分析技术检测身份证中的多种防伪特征,包括:
|
|
1999
|
+
* 1. 荧光油墨特征
|
|
2000
|
+
* 2. 微缩文字
|
|
2001
|
+
* 3. 光变图案
|
|
2002
|
+
* 4. 雕刻凹印
|
|
2003
|
+
* 5. 隐形图案
|
|
2004
|
+
*
|
|
2005
|
+
* @example
|
|
2006
|
+
* ```typescript
|
|
2007
|
+
* // 创建防伪检测器
|
|
2008
|
+
* const antiFakeDetector = new AntiFakeDetector({
|
|
2009
|
+
* sensitivity: 0.8,
|
|
2010
|
+
* enableCache: true
|
|
2011
|
+
* });
|
|
2012
|
+
*
|
|
2013
|
+
* // 分析身份证图像
|
|
2014
|
+
* const imageData = await ImageProcessor.createImageDataFromFile(idCardFile);
|
|
2015
|
+
* const result = await antiFakeDetector.detect(imageData);
|
|
2016
|
+
*
|
|
2017
|
+
* if (result.isAuthentic) {
|
|
2018
|
+
* console.log('身份证真实,检测到防伪特征:', result.detectedFeatures);
|
|
2019
|
+
* } else {
|
|
2020
|
+
* console.log('警告!', result.message);
|
|
2021
|
+
* }
|
|
2022
|
+
* ```
|
|
2023
|
+
*/
|
|
2024
|
+
class AntiFakeDetector {
|
|
2025
|
+
/**
|
|
2026
|
+
* 创建身份证防伪检测器实例
|
|
2027
|
+
*
|
|
2028
|
+
* @param options 防伪检测器配置
|
|
2029
|
+
*/
|
|
2030
|
+
constructor(options = {}) {
|
|
2031
|
+
this.options = {
|
|
2032
|
+
sensitivity: 0.7,
|
|
2033
|
+
enableCache: true,
|
|
2034
|
+
cacheSize: 50,
|
|
2035
|
+
logger: console.log,
|
|
2036
|
+
...options,
|
|
2037
|
+
};
|
|
2038
|
+
// 初始化缓存
|
|
2039
|
+
this.resultCache = new LRUCache(this.options.cacheSize);
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* 检测身份证图像的防伪特征
|
|
2043
|
+
*
|
|
2044
|
+
* @param imageData 身份证图像数据
|
|
2045
|
+
* @returns 防伪检测结果
|
|
2046
|
+
*/
|
|
2047
|
+
async detect(imageData) {
|
|
2048
|
+
const startTime = performance.now();
|
|
2049
|
+
// 检查缓存
|
|
2050
|
+
if (this.options.enableCache) {
|
|
2051
|
+
const fingerprint = calculateImageFingerprint(imageData);
|
|
2052
|
+
const cachedResult = this.resultCache.get(fingerprint);
|
|
2053
|
+
if (cachedResult) {
|
|
2054
|
+
this.options.logger("使用缓存的防伪检测结果");
|
|
2055
|
+
return cachedResult;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
// 图像预处理增强防伪特征
|
|
2059
|
+
const enhancedImage = this.enhanceAntiFakeFeatures(imageData);
|
|
2060
|
+
// 执行多种防伪特征检测
|
|
2061
|
+
const featureResults = await Promise.all([
|
|
2062
|
+
this.detectUVInkFeatures(enhancedImage),
|
|
2063
|
+
this.detectMicroText(enhancedImage),
|
|
2064
|
+
this.detectOpticalVariable(enhancedImage),
|
|
2065
|
+
this.detectIntaglioPrinting(enhancedImage),
|
|
2066
|
+
this.detectGhostImage(enhancedImage),
|
|
2067
|
+
]);
|
|
2068
|
+
// 汇总检测结果
|
|
2069
|
+
const detectedFeatures = [];
|
|
2070
|
+
let totalConfidence = 0;
|
|
2071
|
+
for (const [feature, detected, confidence] of featureResults) {
|
|
2072
|
+
if (detected && confidence > 0.5) {
|
|
2073
|
+
detectedFeatures.push(feature);
|
|
2074
|
+
totalConfidence += confidence;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
// 计算最终结果
|
|
2078
|
+
const normalizedConfidence = featureResults.length > 0 ? totalConfidence / featureResults.length : 0;
|
|
2079
|
+
// 根据敏感度和检测到的特征决定是否通过验证
|
|
2080
|
+
const isAuthentic = normalizedConfidence >= this.options.sensitivity &&
|
|
2081
|
+
detectedFeatures.length >= 2;
|
|
2082
|
+
// 生成结果消息
|
|
2083
|
+
let message = isAuthentic
|
|
2084
|
+
? `身份证真实,检测到${detectedFeatures.length}个防伪特征`
|
|
2085
|
+
: detectedFeatures.length > 0
|
|
2086
|
+
? `可疑身份证,仅检测到${detectedFeatures.length}个防伪特征,置信度不足`
|
|
2087
|
+
: "未检测到有效防伪特征,可能为伪造证件";
|
|
2088
|
+
const result = {
|
|
2089
|
+
isAuthentic,
|
|
2090
|
+
confidence: normalizedConfidence,
|
|
2091
|
+
detectedFeatures,
|
|
2092
|
+
message,
|
|
2093
|
+
processingTime: performance.now() - startTime,
|
|
2094
|
+
};
|
|
2095
|
+
// 缓存结果
|
|
2096
|
+
if (this.options.enableCache) {
|
|
2097
|
+
const fingerprint = calculateImageFingerprint(imageData);
|
|
2098
|
+
this.resultCache.set(fingerprint, result);
|
|
2099
|
+
}
|
|
2100
|
+
return result;
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* 增强身份证图像中的防伪特征
|
|
2104
|
+
*
|
|
2105
|
+
* @param imageData 原始图像数据
|
|
2106
|
+
* @returns 增强后的图像数据
|
|
2107
|
+
* @private
|
|
2108
|
+
*/
|
|
2109
|
+
enhanceAntiFakeFeatures(imageData) {
|
|
2110
|
+
// 应用特定的图像处理增强防伪特征
|
|
2111
|
+
return ImageProcessor.batchProcess(imageData, {
|
|
2112
|
+
contrast: 30, // 增强对比度
|
|
2113
|
+
brightness: 10, // 轻微提高亮度
|
|
2114
|
+
sharpen: true, // 锐化图像突出细节
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* 检测荧光油墨特征
|
|
2119
|
+
*
|
|
2120
|
+
* @param imageData 图像数据
|
|
2121
|
+
* @returns [特征名称, 是否检测到, 置信度]
|
|
2122
|
+
* @private
|
|
2123
|
+
*/
|
|
2124
|
+
async detectUVInkFeatures(imageData) {
|
|
2125
|
+
// 提取蓝色通道增强UV油墨可见度
|
|
2126
|
+
const canvas = document.createElement("canvas");
|
|
2127
|
+
canvas.width = imageData.width;
|
|
2128
|
+
canvas.height = imageData.height;
|
|
2129
|
+
const ctx = canvas.getContext("2d");
|
|
2130
|
+
if (!ctx) {
|
|
2131
|
+
return ["荧光油墨", false, 0];
|
|
2132
|
+
}
|
|
2133
|
+
ctx.putImageData(imageData, 0, 0);
|
|
2134
|
+
// 分析蓝色通道中的特定模式
|
|
2135
|
+
// 实际实现中应使用更复杂的算法提取UV特征
|
|
2136
|
+
// 这里使用模拟实现
|
|
2137
|
+
// 模拟检测: 70%的概率检测到,置信度0.65-0.95
|
|
2138
|
+
const detected = Math.random() > 0.3;
|
|
2139
|
+
const confidence = detected ? 0.65 + Math.random() * 0.3 : 0;
|
|
2140
|
+
return ["荧光油墨", detected, confidence];
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* 检测微缩文字
|
|
2144
|
+
*
|
|
2145
|
+
* @param imageData 图像数据
|
|
2146
|
+
* @returns [特征名称, 是否检测到, 置信度]
|
|
2147
|
+
* @private
|
|
2148
|
+
*/
|
|
2149
|
+
async detectMicroText(imageData) {
|
|
2150
|
+
// 应用边缘检测突出微缩文字
|
|
2151
|
+
ImageProcessor.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
|
|
2152
|
+
// 寻找特定的微缩文字模式
|
|
2153
|
+
// 实际实现中应使用计算机视觉算法寻找微小规则文字模式
|
|
2154
|
+
// 这里使用模拟实现
|
|
2155
|
+
// 模拟检测: 80%的概率检测到,置信度0.7-0.95
|
|
2156
|
+
const detected = Math.random() > 0.2;
|
|
2157
|
+
const confidence = detected ? 0.7 + Math.random() * 0.25 : 0;
|
|
2158
|
+
return ["微缩文字", detected, confidence];
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* 检测光变图案
|
|
2162
|
+
*
|
|
2163
|
+
* @param imageData 图像数据
|
|
2164
|
+
* @returns [特征名称, 是否检测到, 置信度]
|
|
2165
|
+
* @private
|
|
2166
|
+
*/
|
|
2167
|
+
async detectOpticalVariable(imageData) {
|
|
2168
|
+
// 提取特定区域并分析颜色变化
|
|
2169
|
+
// 在实际实现中需要定位光变图案区域并分析其特征
|
|
2170
|
+
// 这里使用模拟实现
|
|
2171
|
+
// 模拟检测: 65%的概率检测到,置信度0.6-0.9
|
|
2172
|
+
const detected = Math.random() > 0.35;
|
|
2173
|
+
const confidence = detected ? 0.6 + Math.random() * 0.3 : 0;
|
|
2174
|
+
return ["光变图案", detected, confidence];
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* 检测凹印雕刻特征
|
|
2178
|
+
*
|
|
2179
|
+
* @param imageData 图像数据
|
|
2180
|
+
* @returns [特征名称, 是否检测到, 置信度]
|
|
2181
|
+
* @private
|
|
2182
|
+
*/
|
|
2183
|
+
async detectIntaglioPrinting(imageData) {
|
|
2184
|
+
// 使用特定滤镜增强凹印效果
|
|
2185
|
+
// 在实际实现中应分析阴影和纹理模式
|
|
2186
|
+
// 这里使用模拟实现
|
|
2187
|
+
// 模拟检测: 75%的概率检测到,置信度0.65-0.9
|
|
2188
|
+
const detected = Math.random() > 0.25;
|
|
2189
|
+
const confidence = detected ? 0.65 + Math.random() * 0.25 : 0;
|
|
2190
|
+
return ["雕刻凹印", detected, confidence];
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* 检测隐形图案(幽灵图像)
|
|
2194
|
+
*
|
|
2195
|
+
* @param imageData 图像数据
|
|
2196
|
+
* @returns [特征名称, 是否检测到, 置信度]
|
|
2197
|
+
* @private
|
|
2198
|
+
*/
|
|
2199
|
+
async detectGhostImage(imageData) {
|
|
2200
|
+
// 调整对比度和亮度显现隐形图案
|
|
2201
|
+
// 在实际实现中应使用特定滤镜和图像处理算法
|
|
2202
|
+
// 这里使用模拟实现
|
|
2203
|
+
// 模拟检测: 60%的概率检测到,置信度0.55-0.85
|
|
2204
|
+
const detected = Math.random() > 0.4;
|
|
2205
|
+
const confidence = detected ? 0.55 + Math.random() * 0.3 : 0;
|
|
2206
|
+
return ["隐形图案", detected, confidence];
|
|
2207
|
+
}
|
|
2208
|
+
/**
|
|
2209
|
+
* 清除结果缓存
|
|
2210
|
+
*/
|
|
2211
|
+
clearCache() {
|
|
2212
|
+
this.resultCache.clear();
|
|
2213
|
+
this.options.logger("防伪检测结果缓存已清除");
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* 释放资源
|
|
2217
|
+
*/
|
|
2218
|
+
dispose() {
|
|
2219
|
+
this.resultCache.clear();
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
/**
|
|
2224
|
+
* @file ID扫描识别库主入口文件
|
|
2225
|
+
* @description 提供身份证识别与二维码、条形码扫描功能的纯前端TypeScript库
|
|
1761
2226
|
* @module IDScannerLib
|
|
1762
|
-
* @version 1.
|
|
2227
|
+
* @version 1.3.0
|
|
1763
2228
|
* @license MIT
|
|
1764
2229
|
*/
|
|
1765
2230
|
/**
|
|
1766
2231
|
* IDScanner 主类
|
|
1767
|
-
*
|
|
2232
|
+
*
|
|
2233
|
+
* 整合二维码、条形码扫描和身份证识别功能,提供统一的接口
|
|
2234
|
+
* 使用动态导入实现按需加载
|
|
1768
2235
|
*/
|
|
1769
|
-
class IDScanner {
|
|
2236
|
+
let IDScanner$1 = class IDScanner {
|
|
1770
2237
|
/**
|
|
1771
2238
|
* 构造函数
|
|
1772
2239
|
* @param options 配置选项
|
|
1773
2240
|
*/
|
|
1774
2241
|
constructor(options = {}) {
|
|
1775
2242
|
this.options = options;
|
|
1776
|
-
this.
|
|
1777
|
-
this.barcodeScanner = null;
|
|
1778
|
-
this.idDetector = null;
|
|
1779
|
-
this.ocrProcessor = null;
|
|
1780
|
-
this.dataExtractor = null;
|
|
1781
|
-
this.scanMode = 'qr';
|
|
2243
|
+
this.scanMode = "qr";
|
|
1782
2244
|
this.videoElement = null;
|
|
2245
|
+
this.scanning = false;
|
|
2246
|
+
this.qrModule = null;
|
|
2247
|
+
this.ocrModule = null;
|
|
2248
|
+
this.scanTimer = null;
|
|
2249
|
+
this.isQRModuleLoaded = false;
|
|
2250
|
+
this.isOCRModuleLoaded = false;
|
|
2251
|
+
// 新增防伪检测器
|
|
2252
|
+
this.antiFakeDetector = null;
|
|
2253
|
+
this.isAntiFakeModuleLoaded = false;
|
|
1783
2254
|
this.camera = new Camera(options.cameraOptions);
|
|
1784
2255
|
}
|
|
1785
2256
|
/**
|
|
1786
2257
|
* 初始化模块
|
|
1787
|
-
* 根据需要初始化OCR
|
|
2258
|
+
* 根据需要初始化OCR引擎和防伪检测模块
|
|
1788
2259
|
*/
|
|
1789
2260
|
async initialize() {
|
|
1790
2261
|
try {
|
|
1791
|
-
//
|
|
1792
|
-
this.
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2262
|
+
// 预加载OCR模块但不初始化
|
|
2263
|
+
if (!this.isOCRModuleLoaded) {
|
|
2264
|
+
// 动态导入OCR模块
|
|
2265
|
+
const OCRModule = await Promise.resolve().then(function () { return ocrModule; }).then((m) => m.OCRModule);
|
|
2266
|
+
this.ocrModule = new OCRModule({
|
|
2267
|
+
cameraOptions: this.options.cameraOptions,
|
|
2268
|
+
onIDCardScanned: this.options.onIDCardScanned,
|
|
2269
|
+
onError: this.options.onError,
|
|
2270
|
+
});
|
|
2271
|
+
this.isOCRModuleLoaded = true;
|
|
2272
|
+
// 初始化OCR模块
|
|
2273
|
+
await this.ocrModule.initialize();
|
|
2274
|
+
}
|
|
2275
|
+
// 初始化防伪检测模块
|
|
2276
|
+
if (!this.isAntiFakeModuleLoaded) {
|
|
2277
|
+
this.antiFakeDetector = new AntiFakeDetector();
|
|
2278
|
+
this.isAntiFakeModuleLoaded = true;
|
|
2279
|
+
}
|
|
2280
|
+
console.log("IDScanner初始化完成");
|
|
1796
2281
|
}
|
|
1797
2282
|
catch (error) {
|
|
2283
|
+
console.error("初始化失败:", error);
|
|
1798
2284
|
this.handleError(error);
|
|
1799
2285
|
throw error;
|
|
1800
2286
|
}
|
|
1801
2287
|
}
|
|
1802
2288
|
/**
|
|
1803
|
-
*
|
|
1804
|
-
* @param videoElement HTML视频元素
|
|
2289
|
+
* 初始化OCR模块
|
|
1805
2290
|
*/
|
|
1806
|
-
async
|
|
1807
|
-
this.
|
|
2291
|
+
async initOCRModule() {
|
|
2292
|
+
if (this.isOCRModuleLoaded)
|
|
2293
|
+
return;
|
|
2294
|
+
try {
|
|
2295
|
+
// 动态导入OCR模块
|
|
2296
|
+
const OCRModule = await Promise.resolve().then(function () { return ocrModule; }).then((m) => m.OCRModule);
|
|
2297
|
+
this.ocrModule = new OCRModule({
|
|
2298
|
+
cameraOptions: this.options.cameraOptions,
|
|
2299
|
+
onIDCardScanned: this.options.onIDCardScanned,
|
|
2300
|
+
onError: this.options.onError,
|
|
2301
|
+
});
|
|
2302
|
+
this.isOCRModuleLoaded = true;
|
|
2303
|
+
// 初始化OCR模块
|
|
2304
|
+
await this.ocrModule.initialize();
|
|
2305
|
+
}
|
|
2306
|
+
catch (error) {
|
|
2307
|
+
console.error("OCR模块初始化失败:", error);
|
|
2308
|
+
throw error;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* 启动二维码扫描
|
|
2313
|
+
* @param videoElement HTML视频元素
|
|
2314
|
+
*/
|
|
2315
|
+
async startQRScanner(videoElement) {
|
|
2316
|
+
this.stop();
|
|
2317
|
+
this.videoElement = videoElement;
|
|
2318
|
+
this.scanMode = "qr";
|
|
2319
|
+
try {
|
|
2320
|
+
// 动态加载二维码模块
|
|
2321
|
+
if (!this.isQRModuleLoaded) {
|
|
2322
|
+
const ScannerModule = await Promise.resolve().then(function () { return qrModule; }).then((m) => m.ScannerModule);
|
|
2323
|
+
this.qrModule = new ScannerModule({
|
|
2324
|
+
cameraOptions: this.options.cameraOptions,
|
|
2325
|
+
qrScannerOptions: this.options.qrScannerOptions,
|
|
2326
|
+
barcodeScannerOptions: this.options.barcodeScannerOptions,
|
|
2327
|
+
onQRCodeScanned: this.options.onQRCodeScanned,
|
|
2328
|
+
onBarcodeScanned: this.options.onBarcodeScanned,
|
|
2329
|
+
onError: this.options.onError,
|
|
2330
|
+
});
|
|
2331
|
+
this.isQRModuleLoaded = true;
|
|
2332
|
+
}
|
|
2333
|
+
await this.qrModule.startQRScanner(videoElement);
|
|
2334
|
+
}
|
|
2335
|
+
catch (error) {
|
|
2336
|
+
this.handleError(error);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* 启动条形码扫描
|
|
2341
|
+
* @param videoElement HTML视频元素
|
|
2342
|
+
*/
|
|
2343
|
+
async startBarcodeScanner(videoElement) {
|
|
2344
|
+
this.stop();
|
|
2345
|
+
this.videoElement = videoElement;
|
|
2346
|
+
this.scanMode = "barcode";
|
|
2347
|
+
try {
|
|
2348
|
+
// 动态加载二维码模块
|
|
2349
|
+
if (!this.isQRModuleLoaded) {
|
|
2350
|
+
const ScannerModule = await Promise.resolve().then(function () { return qrModule; }).then((m) => m.ScannerModule);
|
|
2351
|
+
this.qrModule = new ScannerModule({
|
|
2352
|
+
cameraOptions: this.options.cameraOptions,
|
|
2353
|
+
qrScannerOptions: this.options.qrScannerOptions,
|
|
2354
|
+
barcodeScannerOptions: this.options.barcodeScannerOptions,
|
|
2355
|
+
onQRCodeScanned: this.options.onQRCodeScanned,
|
|
2356
|
+
onBarcodeScanned: this.options.onBarcodeScanned,
|
|
2357
|
+
onError: this.options.onError,
|
|
2358
|
+
});
|
|
2359
|
+
this.isQRModuleLoaded = true;
|
|
2360
|
+
}
|
|
2361
|
+
await this.qrModule.startBarcodeScanner(videoElement);
|
|
2362
|
+
}
|
|
2363
|
+
catch (error) {
|
|
2364
|
+
this.handleError(error);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* 启动身份证扫描
|
|
2369
|
+
* @param videoElement HTML视频元素
|
|
2370
|
+
*/
|
|
2371
|
+
async startIDCardScanner(videoElement) {
|
|
2372
|
+
this.stop();
|
|
2373
|
+
this.videoElement = videoElement;
|
|
2374
|
+
this.scanMode = "idcard";
|
|
2375
|
+
try {
|
|
2376
|
+
// 检查OCR模块是否已加载,若未加载则自动初始化
|
|
2377
|
+
if (!this.isOCRModuleLoaded) {
|
|
2378
|
+
await this.initialize();
|
|
2379
|
+
}
|
|
2380
|
+
await this.ocrModule.startIDCardScanner(videoElement);
|
|
2381
|
+
}
|
|
2382
|
+
catch (error) {
|
|
2383
|
+
this.handleError(error);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* 停止扫描
|
|
2388
|
+
*/
|
|
2389
|
+
stop() {
|
|
2390
|
+
if (this.scanMode === "qr" || this.scanMode === "barcode") {
|
|
2391
|
+
if (this.qrModule) {
|
|
2392
|
+
this.qrModule.stop();
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
else if (this.scanMode === "idcard") {
|
|
2396
|
+
if (this.ocrModule) {
|
|
2397
|
+
this.ocrModule.stop();
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* 处理错误
|
|
2403
|
+
*/
|
|
2404
|
+
handleError(error) {
|
|
2405
|
+
if (this.options.onError) {
|
|
2406
|
+
this.options.onError(error);
|
|
2407
|
+
}
|
|
2408
|
+
else {
|
|
2409
|
+
console.error("IDScanner错误:", error);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* 释放资源
|
|
2414
|
+
*/
|
|
2415
|
+
async terminate() {
|
|
2416
|
+
this.stop();
|
|
2417
|
+
// 释放OCR资源
|
|
2418
|
+
if (this.isOCRModuleLoaded && this.ocrModule) {
|
|
2419
|
+
await this.ocrModule.terminate();
|
|
2420
|
+
this.ocrModule = null;
|
|
2421
|
+
this.isOCRModuleLoaded = false;
|
|
2422
|
+
}
|
|
2423
|
+
// 释放QR扫描资源
|
|
2424
|
+
if (this.isQRModuleLoaded && this.qrModule) {
|
|
2425
|
+
this.qrModule = null;
|
|
2426
|
+
this.isQRModuleLoaded = false;
|
|
2427
|
+
}
|
|
2428
|
+
// 释放防伪检测资源
|
|
2429
|
+
if (this.antiFakeDetector) {
|
|
2430
|
+
this.antiFakeDetector.dispose();
|
|
2431
|
+
this.antiFakeDetector = null;
|
|
2432
|
+
this.isAntiFakeModuleLoaded = false;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* 处理图片中的二维码
|
|
2437
|
+
* @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
|
|
2438
|
+
* @returns 返回Promise,解析为扫描结果
|
|
2439
|
+
*/
|
|
2440
|
+
async processQRCodeImage(imageSource) {
|
|
2441
|
+
try {
|
|
2442
|
+
// 动态加载二维码模块
|
|
2443
|
+
if (!this.isQRModuleLoaded) {
|
|
2444
|
+
const ScannerModule = await Promise.resolve().then(function () { return qrModule; }).then((m) => m.ScannerModule);
|
|
2445
|
+
this.qrModule = new ScannerModule({
|
|
2446
|
+
qrScannerOptions: this.options.qrScannerOptions,
|
|
2447
|
+
onQRCodeScanned: this.options.onQRCodeScanned,
|
|
2448
|
+
onError: this.options.onError,
|
|
2449
|
+
});
|
|
2450
|
+
this.isQRModuleLoaded = true;
|
|
2451
|
+
}
|
|
2452
|
+
// 处理不同类型的图片源
|
|
2453
|
+
let imageElement;
|
|
2454
|
+
if (imageSource instanceof File) {
|
|
2455
|
+
// 如果是File对象,创建新的Image元素并加载图片
|
|
2456
|
+
imageElement = new Image();
|
|
2457
|
+
imageElement.crossOrigin = "anonymous"; // 处理跨域图片
|
|
2458
|
+
const url = URL.createObjectURL(imageSource);
|
|
2459
|
+
await new Promise((resolve, reject) => {
|
|
2460
|
+
imageElement.onload = resolve;
|
|
2461
|
+
imageElement.onerror = reject;
|
|
2462
|
+
imageElement.src = url;
|
|
2463
|
+
});
|
|
2464
|
+
// 使用后释放URL对象
|
|
2465
|
+
URL.revokeObjectURL(url);
|
|
2466
|
+
}
|
|
2467
|
+
else if (typeof imageSource === "string") {
|
|
2468
|
+
// 如果是URL字符串,创建新的Image元素并加载图片
|
|
2469
|
+
imageElement = new Image();
|
|
2470
|
+
imageElement.crossOrigin = "anonymous"; // 处理跨域图片
|
|
2471
|
+
await new Promise((resolve, reject) => {
|
|
2472
|
+
imageElement.onload = resolve;
|
|
2473
|
+
imageElement.onerror = reject;
|
|
2474
|
+
imageElement.src = imageSource;
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
else if (imageSource instanceof HTMLImageElement) {
|
|
2478
|
+
// 如果已经是Image元素,直接使用
|
|
2479
|
+
imageElement = imageSource;
|
|
2480
|
+
}
|
|
2481
|
+
else if (imageSource instanceof HTMLCanvasElement) {
|
|
2482
|
+
// 如果是Canvas元素,创建Image并从Canvas获取数据
|
|
2483
|
+
imageElement = new Image();
|
|
2484
|
+
imageElement.src = imageSource.toDataURL();
|
|
2485
|
+
await new Promise((resolve) => {
|
|
2486
|
+
imageElement.onload = resolve;
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
else {
|
|
2490
|
+
throw new Error("不支持的图片源类型");
|
|
2491
|
+
}
|
|
2492
|
+
// 获取图像数据
|
|
2493
|
+
const canvas = document.createElement("canvas");
|
|
2494
|
+
canvas.width = imageElement.naturalWidth;
|
|
2495
|
+
canvas.height = imageElement.naturalHeight;
|
|
2496
|
+
const ctx = canvas.getContext("2d");
|
|
2497
|
+
if (!ctx) {
|
|
2498
|
+
throw new Error("无法创建Canvas上下文");
|
|
2499
|
+
}
|
|
2500
|
+
ctx.drawImage(imageElement, 0, 0);
|
|
2501
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
2502
|
+
// 使用QR模块处理图像
|
|
2503
|
+
return this.qrModule.processQRCodeImage(imageData);
|
|
2504
|
+
}
|
|
2505
|
+
catch (error) {
|
|
2506
|
+
this.handleError(error);
|
|
2507
|
+
throw error;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* 处理图片中的条形码
|
|
2512
|
+
* @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
|
|
2513
|
+
* @returns 返回Promise,解析为扫描结果
|
|
2514
|
+
*/
|
|
2515
|
+
async processBarcodeImage(imageSource) {
|
|
2516
|
+
try {
|
|
2517
|
+
// 动态加载二维码模块
|
|
2518
|
+
if (!this.isQRModuleLoaded) {
|
|
2519
|
+
const ScannerModule = await Promise.resolve().then(function () { return qrModule; }).then((m) => m.ScannerModule);
|
|
2520
|
+
this.qrModule = new ScannerModule({
|
|
2521
|
+
barcodeScannerOptions: this.options.barcodeScannerOptions,
|
|
2522
|
+
onBarcodeScanned: this.options.onBarcodeScanned,
|
|
2523
|
+
onError: this.options.onError,
|
|
2524
|
+
});
|
|
2525
|
+
this.isQRModuleLoaded = true;
|
|
2526
|
+
}
|
|
2527
|
+
// 处理不同类型的图片源
|
|
2528
|
+
let imageElement;
|
|
2529
|
+
if (imageSource instanceof File) {
|
|
2530
|
+
// 如果是File对象,创建新的Image元素并加载图片
|
|
2531
|
+
imageElement = new Image();
|
|
2532
|
+
imageElement.crossOrigin = "anonymous"; // 处理跨域图片
|
|
2533
|
+
const url = URL.createObjectURL(imageSource);
|
|
2534
|
+
await new Promise((resolve, reject) => {
|
|
2535
|
+
imageElement.onload = resolve;
|
|
2536
|
+
imageElement.onerror = reject;
|
|
2537
|
+
imageElement.src = url;
|
|
2538
|
+
});
|
|
2539
|
+
// 使用后释放URL对象
|
|
2540
|
+
URL.revokeObjectURL(url);
|
|
2541
|
+
}
|
|
2542
|
+
else if (typeof imageSource === "string") {
|
|
2543
|
+
// 如果是URL字符串,创建新的Image元素并加载图片
|
|
2544
|
+
imageElement = new Image();
|
|
2545
|
+
imageElement.crossOrigin = "anonymous"; // 处理跨域图片
|
|
2546
|
+
await new Promise((resolve, reject) => {
|
|
2547
|
+
imageElement.onload = resolve;
|
|
2548
|
+
imageElement.onerror = reject;
|
|
2549
|
+
imageElement.src = imageSource;
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
else if (imageSource instanceof HTMLImageElement) {
|
|
2553
|
+
// 如果已经是Image元素,直接使用
|
|
2554
|
+
imageElement = imageSource;
|
|
2555
|
+
}
|
|
2556
|
+
else if (imageSource instanceof HTMLCanvasElement) {
|
|
2557
|
+
// 如果是Canvas元素,创建Image并从Canvas获取数据
|
|
2558
|
+
imageElement = new Image();
|
|
2559
|
+
imageElement.src = imageSource.toDataURL();
|
|
2560
|
+
await new Promise((resolve) => {
|
|
2561
|
+
imageElement.onload = resolve;
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
else {
|
|
2565
|
+
throw new Error("不支持的图片源类型");
|
|
2566
|
+
}
|
|
2567
|
+
// 获取图像数据
|
|
2568
|
+
const canvas = document.createElement("canvas");
|
|
2569
|
+
canvas.width = imageElement.naturalWidth;
|
|
2570
|
+
canvas.height = imageElement.naturalHeight;
|
|
2571
|
+
const ctx = canvas.getContext("2d");
|
|
2572
|
+
if (!ctx) {
|
|
2573
|
+
throw new Error("无法创建Canvas上下文");
|
|
2574
|
+
}
|
|
2575
|
+
ctx.drawImage(imageElement, 0, 0);
|
|
2576
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
2577
|
+
// 使用Barcode模块处理图像
|
|
2578
|
+
return this.qrModule.processBarcodeImage(imageData);
|
|
2579
|
+
}
|
|
2580
|
+
catch (error) {
|
|
2581
|
+
this.handleError(error);
|
|
2582
|
+
throw error;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
/**
|
|
2586
|
+
* 处理图片中的身份证
|
|
2587
|
+
* @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
|
|
2588
|
+
* @returns 返回Promise,解析为身份证信息
|
|
2589
|
+
*/
|
|
2590
|
+
async processIDCardImage(imageSource) {
|
|
2591
|
+
if (!this.isOCRModuleLoaded) {
|
|
2592
|
+
await this.initOCRModule();
|
|
2593
|
+
}
|
|
2594
|
+
try {
|
|
2595
|
+
// 处理不同类型的图片源
|
|
2596
|
+
let imageElement;
|
|
2597
|
+
if (imageSource instanceof File) {
|
|
2598
|
+
// 如果是File对象,先进行压缩
|
|
2599
|
+
const compressedFile = await ImageProcessor.compressImage(imageSource, {
|
|
2600
|
+
maxSizeMB: 2, // 最大2MB
|
|
2601
|
+
maxWidthOrHeight: 1800, // 最大尺寸
|
|
2602
|
+
useWebWorker: true,
|
|
2603
|
+
});
|
|
2604
|
+
// 创建新的Image元素并加载图片
|
|
2605
|
+
imageElement = new Image();
|
|
2606
|
+
imageElement.crossOrigin = "anonymous"; // 处理跨域图片
|
|
2607
|
+
const url = URL.createObjectURL(compressedFile);
|
|
2608
|
+
await new Promise((resolve, reject) => {
|
|
2609
|
+
imageElement.onload = resolve;
|
|
2610
|
+
imageElement.onerror = reject;
|
|
2611
|
+
imageElement.src = url;
|
|
2612
|
+
});
|
|
2613
|
+
// 使用后释放URL对象
|
|
2614
|
+
URL.revokeObjectURL(url);
|
|
2615
|
+
}
|
|
2616
|
+
else if (typeof imageSource === "string") {
|
|
2617
|
+
// 如果是URL字符串,创建新的Image元素并加载图片
|
|
2618
|
+
imageElement = new Image();
|
|
2619
|
+
imageElement.crossOrigin = "anonymous"; // 处理跨域图片
|
|
2620
|
+
await new Promise((resolve, reject) => {
|
|
2621
|
+
imageElement.onload = resolve;
|
|
2622
|
+
imageElement.onerror = reject;
|
|
2623
|
+
imageElement.src = imageSource;
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
else if (imageSource instanceof HTMLImageElement) {
|
|
2627
|
+
// 如果已经是Image元素,直接使用
|
|
2628
|
+
imageElement = imageSource;
|
|
2629
|
+
}
|
|
2630
|
+
else if (imageSource instanceof HTMLCanvasElement) {
|
|
2631
|
+
// 如果是Canvas元素,创建Image并从Canvas获取数据
|
|
2632
|
+
imageElement = new Image();
|
|
2633
|
+
imageElement.src = imageSource.toDataURL();
|
|
2634
|
+
await new Promise((resolve) => {
|
|
2635
|
+
imageElement.onload = resolve;
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
else {
|
|
2639
|
+
throw new Error("不支持的图片源类型");
|
|
2640
|
+
}
|
|
2641
|
+
// 获取图像数据
|
|
2642
|
+
const canvas = document.createElement("canvas");
|
|
2643
|
+
canvas.width = imageElement.naturalWidth;
|
|
2644
|
+
canvas.height = imageElement.naturalHeight;
|
|
2645
|
+
const ctx = canvas.getContext("2d");
|
|
2646
|
+
if (!ctx) {
|
|
2647
|
+
throw new Error("无法创建Canvas上下文");
|
|
2648
|
+
}
|
|
2649
|
+
ctx.drawImage(imageElement, 0, 0);
|
|
2650
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
2651
|
+
// 对图像进行预处理,提高识别率
|
|
2652
|
+
const enhancedImageData = ImageProcessor.batchProcess(imageData, {
|
|
2653
|
+
brightness: 10,
|
|
2654
|
+
contrast: 15,
|
|
2655
|
+
sharpen: true,
|
|
2656
|
+
});
|
|
2657
|
+
// 使用OCR模块处理图像
|
|
2658
|
+
const idInfo = await this.ocrModule.processIDCard(enhancedImageData);
|
|
2659
|
+
// 进行防伪检测并将结果添加到身份证信息中
|
|
2660
|
+
if (this.isAntiFakeModuleLoaded && this.antiFakeDetector) {
|
|
2661
|
+
try {
|
|
2662
|
+
const result = await this.antiFakeDetector.detect(enhancedImageData);
|
|
2663
|
+
// 将防伪检测结果添加到身份证信息对象中
|
|
2664
|
+
const extendedInfo = idInfo;
|
|
2665
|
+
extendedInfo.antiFakeResult = result;
|
|
2666
|
+
// 触发防伪检测回调
|
|
2667
|
+
if (this.options.onAntiFakeDetected) {
|
|
2668
|
+
this.options.onAntiFakeDetected(result);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
catch (error) {
|
|
2672
|
+
console.warn("身份证防伪检测失败:", error);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
return idInfo;
|
|
2676
|
+
}
|
|
2677
|
+
catch (error) {
|
|
2678
|
+
this.handleError(error);
|
|
2679
|
+
throw error;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* 批量处理图像
|
|
2684
|
+
* @param imageSource 图片源,可以是Image元素、Canvas元素、URL字符串或File对象
|
|
2685
|
+
* @param options 图像处理选项
|
|
2686
|
+
* @param outputFormat 输出格式,'imagedata'或'file'
|
|
2687
|
+
* @returns 返回Promise,解析为处理后的ImageData或File
|
|
2688
|
+
*/
|
|
2689
|
+
async processImage(imageSource, options = {}, outputFormat = "imagedata") {
|
|
2690
|
+
try {
|
|
2691
|
+
// 处理不同类型的图片源
|
|
2692
|
+
let imageData;
|
|
2693
|
+
if (imageSource instanceof File) {
|
|
2694
|
+
// 如果是File对象,先进行压缩
|
|
2695
|
+
const compressedFile = await ImageProcessor.compressImage(imageSource, {
|
|
2696
|
+
maxSizeMB: 2, // 最大2MB
|
|
2697
|
+
maxWidthOrHeight: 1920, // 最大尺寸
|
|
2698
|
+
useWebWorker: true,
|
|
2699
|
+
});
|
|
2700
|
+
// 从File创建ImageData
|
|
2701
|
+
imageData = await ImageProcessor.createImageDataFromFile(compressedFile);
|
|
2702
|
+
}
|
|
2703
|
+
else if (typeof imageSource === "string") {
|
|
2704
|
+
// 如果是URL字符串,创建新的Image元素并加载图片
|
|
2705
|
+
const imageElement = new Image();
|
|
2706
|
+
imageElement.crossOrigin = "anonymous"; // 处理跨域图片
|
|
2707
|
+
await new Promise((resolve, reject) => {
|
|
2708
|
+
imageElement.onload = resolve;
|
|
2709
|
+
imageElement.onerror = reject;
|
|
2710
|
+
imageElement.src = imageSource;
|
|
2711
|
+
});
|
|
2712
|
+
// 获取图像数据
|
|
2713
|
+
const canvas = document.createElement("canvas");
|
|
2714
|
+
canvas.width = imageElement.naturalWidth;
|
|
2715
|
+
canvas.height = imageElement.naturalHeight;
|
|
2716
|
+
const ctx = canvas.getContext("2d");
|
|
2717
|
+
if (!ctx) {
|
|
2718
|
+
throw new Error("无法创建Canvas上下文");
|
|
2719
|
+
}
|
|
2720
|
+
ctx.drawImage(imageElement, 0, 0);
|
|
2721
|
+
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
2722
|
+
}
|
|
2723
|
+
else if (imageSource instanceof HTMLImageElement) {
|
|
2724
|
+
// 如果是Image元素,从它创建ImageData
|
|
2725
|
+
const canvas = document.createElement("canvas");
|
|
2726
|
+
canvas.width = imageSource.naturalWidth;
|
|
2727
|
+
canvas.height = imageSource.naturalHeight;
|
|
2728
|
+
const ctx = canvas.getContext("2d");
|
|
2729
|
+
if (!ctx) {
|
|
2730
|
+
throw new Error("无法创建Canvas上下文");
|
|
2731
|
+
}
|
|
2732
|
+
ctx.drawImage(imageSource, 0, 0);
|
|
2733
|
+
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
2734
|
+
}
|
|
2735
|
+
else if (imageSource instanceof HTMLCanvasElement) {
|
|
2736
|
+
// 如果是Canvas元素,直接获取其ImageData
|
|
2737
|
+
const ctx = imageSource.getContext("2d");
|
|
2738
|
+
if (!ctx) {
|
|
2739
|
+
throw new Error("无法获取Canvas上下文");
|
|
2740
|
+
}
|
|
2741
|
+
imageData = ctx.getImageData(0, 0, imageSource.width, imageSource.height);
|
|
2742
|
+
}
|
|
2743
|
+
else {
|
|
2744
|
+
throw new Error("不支持的图片源类型");
|
|
2745
|
+
}
|
|
2746
|
+
// 进行图像处理
|
|
2747
|
+
const processedImageData = ImageProcessor.batchProcess(imageData, options);
|
|
2748
|
+
// 根据需要的输出格式返回结果
|
|
2749
|
+
if (outputFormat === "file") {
|
|
2750
|
+
// 将ImageData转换为File
|
|
2751
|
+
const file = await ImageProcessor.imageDataToFile(processedImageData, "processed_image.jpg", "image/jpeg", 0.85);
|
|
2752
|
+
// 触发回调
|
|
2753
|
+
if (this.options.onImageProcessed) {
|
|
2754
|
+
this.options.onImageProcessed(file);
|
|
2755
|
+
}
|
|
2756
|
+
return file;
|
|
2757
|
+
}
|
|
2758
|
+
else {
|
|
2759
|
+
// 触发回调
|
|
2760
|
+
if (this.options.onImageProcessed) {
|
|
2761
|
+
this.options.onImageProcessed(processedImageData);
|
|
2762
|
+
}
|
|
2763
|
+
return processedImageData;
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
catch (error) {
|
|
2767
|
+
this.handleError(error);
|
|
2768
|
+
throw error;
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* 压缩图片
|
|
2773
|
+
* @param file 要压缩的图片文件
|
|
2774
|
+
* @param options 压缩选项
|
|
2775
|
+
* @returns 返回Promise,解析为压缩后的文件
|
|
2776
|
+
*/
|
|
2777
|
+
async compressImage(file, options) {
|
|
2778
|
+
try {
|
|
2779
|
+
return await ImageProcessor.compressImage(file, options);
|
|
2780
|
+
}
|
|
2781
|
+
catch (error) {
|
|
2782
|
+
this.handleError(error);
|
|
2783
|
+
throw error;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* 身份证防伪检测
|
|
2788
|
+
* @param imageSource 图片源
|
|
2789
|
+
* @returns 防伪检测结果
|
|
2790
|
+
*/
|
|
2791
|
+
async detectIDCardAntiFake(imageSource) {
|
|
2792
|
+
if (!this.isAntiFakeModuleLoaded || !this.antiFakeDetector) {
|
|
2793
|
+
await this.initialize();
|
|
2794
|
+
if (!this.antiFakeDetector) {
|
|
2795
|
+
throw new Error("防伪检测模块初始化失败");
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
try {
|
|
2799
|
+
// 转换输入为ImageData
|
|
2800
|
+
let imageData;
|
|
2801
|
+
if (typeof imageSource === "string") {
|
|
2802
|
+
// 处理URL或Base64
|
|
2803
|
+
const img = new Image();
|
|
2804
|
+
await new Promise((resolve, reject) => {
|
|
2805
|
+
img.onload = () => resolve();
|
|
2806
|
+
img.onerror = () => reject(new Error("图像加载失败"));
|
|
2807
|
+
img.src = imageSource;
|
|
2808
|
+
});
|
|
2809
|
+
const canvas = document.createElement("canvas");
|
|
2810
|
+
canvas.width = img.width;
|
|
2811
|
+
canvas.height = img.height;
|
|
2812
|
+
const ctx = canvas.getContext("2d");
|
|
2813
|
+
if (!ctx) {
|
|
2814
|
+
throw new Error("无法创建Canvas上下文");
|
|
2815
|
+
}
|
|
2816
|
+
ctx.drawImage(img, 0, 0);
|
|
2817
|
+
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
2818
|
+
}
|
|
2819
|
+
else if (imageSource instanceof File) {
|
|
2820
|
+
// 处理文件
|
|
2821
|
+
imageData = await ImageProcessor.createImageDataFromFile(imageSource);
|
|
2822
|
+
}
|
|
2823
|
+
else if (imageSource instanceof HTMLImageElement) {
|
|
2824
|
+
// 处理Image元素
|
|
2825
|
+
const canvas = document.createElement("canvas");
|
|
2826
|
+
canvas.width = imageSource.width;
|
|
2827
|
+
canvas.height = imageSource.height;
|
|
2828
|
+
const ctx = canvas.getContext("2d");
|
|
2829
|
+
if (!ctx) {
|
|
2830
|
+
throw new Error("无法创建Canvas上下文");
|
|
2831
|
+
}
|
|
2832
|
+
ctx.drawImage(imageSource, 0, 0);
|
|
2833
|
+
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
2834
|
+
}
|
|
2835
|
+
else {
|
|
2836
|
+
// 处理Canvas元素
|
|
2837
|
+
const ctx = imageSource.getContext("2d");
|
|
2838
|
+
if (!ctx) {
|
|
2839
|
+
throw new Error("无法获取Canvas上下文");
|
|
2840
|
+
}
|
|
2841
|
+
imageData = ctx.getImageData(0, 0, imageSource.width, imageSource.height);
|
|
2842
|
+
}
|
|
2843
|
+
// 执行防伪检测
|
|
2844
|
+
const result = await this.antiFakeDetector.detect(imageData);
|
|
2845
|
+
// 触发回调
|
|
2846
|
+
if (this.options.onAntiFakeDetected) {
|
|
2847
|
+
this.options.onAntiFakeDetected(result);
|
|
2848
|
+
}
|
|
2849
|
+
return result;
|
|
2850
|
+
}
|
|
2851
|
+
catch (error) {
|
|
2852
|
+
this.handleError(error);
|
|
2853
|
+
throw error;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
};
|
|
2857
|
+
|
|
2858
|
+
class IDScannerDemo {
|
|
2859
|
+
constructor(videoElementId, resultContainerId, switchButtonId, imageInputId) {
|
|
2860
|
+
this.imageInput = null;
|
|
2861
|
+
this.currentMode = "qr";
|
|
2862
|
+
// 获取DOM元素
|
|
2863
|
+
this.videoElement = document.getElementById(videoElementId);
|
|
2864
|
+
this.resultContainer = document.getElementById(resultContainerId);
|
|
2865
|
+
this.switchButton = document.getElementById(switchButtonId);
|
|
2866
|
+
// 如果提供了图片输入元素ID,初始化图片输入功能
|
|
2867
|
+
if (imageInputId) {
|
|
2868
|
+
this.imageInput = document.getElementById(imageInputId);
|
|
2869
|
+
if (this.imageInput) {
|
|
2870
|
+
this.imageInput.addEventListener("change", this.handleImageInput.bind(this));
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
try {
|
|
2874
|
+
// 创建IDScanner实例
|
|
2875
|
+
this.scanner = new IDScanner$1({
|
|
2876
|
+
onQRCodeScanned: this.handleQRCodeResult.bind(this),
|
|
2877
|
+
onBarcodeScanned: this.handleQRCodeResult.bind(this), // 复用QR码结果处理
|
|
2878
|
+
onIDCardScanned: this.handleIDCardResult.bind(this),
|
|
2879
|
+
onError: this.handleError.bind(this),
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
catch (error) {
|
|
2883
|
+
console.error("创建IDScanner实例失败:", error);
|
|
2884
|
+
this.handleError(error instanceof Error ? error : new Error("初始化失败"));
|
|
2885
|
+
// 创建一个空对象以避免空引用错误
|
|
2886
|
+
this.scanner = {};
|
|
2887
|
+
}
|
|
2888
|
+
// 添加模式切换按钮事件监听
|
|
2889
|
+
this.switchButton.addEventListener("click", this.toggleScanMode.bind(this));
|
|
2890
|
+
}
|
|
2891
|
+
async initialize() {
|
|
2892
|
+
try {
|
|
2893
|
+
await this.scanner.initialize();
|
|
2894
|
+
await this.scanner.startQRScanner(this.videoElement);
|
|
2895
|
+
this.switchButton.textContent = "切换到身份证模式";
|
|
2896
|
+
this.currentMode = "qr";
|
|
2897
|
+
}
|
|
2898
|
+
catch (error) {
|
|
2899
|
+
this.handleError(error instanceof Error ? error : new Error("初始化失败"));
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
async toggleScanMode() {
|
|
2903
|
+
try {
|
|
2904
|
+
this.scanner.stop();
|
|
2905
|
+
if (this.currentMode === "qr") {
|
|
2906
|
+
this.currentMode = "barcode";
|
|
2907
|
+
await this.scanner.startBarcodeScanner(this.videoElement);
|
|
2908
|
+
this.switchButton.textContent = "切换到身份证模式";
|
|
2909
|
+
}
|
|
2910
|
+
else if (this.currentMode === "barcode") {
|
|
2911
|
+
this.currentMode = "idcard";
|
|
2912
|
+
await this.scanner.startIDCardScanner(this.videoElement);
|
|
2913
|
+
this.switchButton.textContent = "切换到二维码模式";
|
|
2914
|
+
}
|
|
2915
|
+
else {
|
|
2916
|
+
this.currentMode = "qr";
|
|
2917
|
+
await this.scanner.startQRScanner(this.videoElement);
|
|
2918
|
+
this.switchButton.textContent = "切换到条形码模式";
|
|
2919
|
+
}
|
|
2920
|
+
this.resultContainer.innerHTML = "";
|
|
2921
|
+
}
|
|
2922
|
+
catch (error) {
|
|
2923
|
+
this.handleError(error instanceof Error ? error : new Error("切换模式失败"));
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* 处理图片输入
|
|
2928
|
+
* 支持从文件选择器获取图片并进行识别
|
|
2929
|
+
*/
|
|
2930
|
+
async handleImageInput(event) {
|
|
2931
|
+
const input = event.target;
|
|
2932
|
+
if (!input.files || input.files.length === 0)
|
|
2933
|
+
return;
|
|
2934
|
+
const file = input.files[0];
|
|
2935
|
+
try {
|
|
2936
|
+
// 检查文件类型
|
|
2937
|
+
if (!file.type.startsWith("image/")) {
|
|
2938
|
+
throw new Error("请选择图片文件");
|
|
2939
|
+
}
|
|
2940
|
+
// 创建一个本地URL以显示图片
|
|
2941
|
+
const imageUrl = URL.createObjectURL(file);
|
|
2942
|
+
// 显示处理中的提示
|
|
2943
|
+
this.resultContainer.innerHTML = `
|
|
2944
|
+
<h3>正在处理图片...</h3>
|
|
2945
|
+
<img src="${imageUrl}" style="max-width: 100%; max-height: 300px; margin-bottom: 10px;">
|
|
2946
|
+
`;
|
|
2947
|
+
// 根据当前模式处理图片
|
|
2948
|
+
try {
|
|
2949
|
+
if (this.currentMode === "qr") {
|
|
2950
|
+
const result = await this.scanner.processQRCodeImage(imageUrl);
|
|
2951
|
+
if (result) {
|
|
2952
|
+
this.handleQRCodeResult(result);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
else if (this.currentMode === "barcode") {
|
|
2956
|
+
const result = await this.scanner.processBarcodeImage(imageUrl);
|
|
2957
|
+
if (result) {
|
|
2958
|
+
this.handleQRCodeResult(result);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
else if (this.currentMode === "idcard") {
|
|
2962
|
+
const result = await this.scanner.processIDCardImage(imageUrl);
|
|
2963
|
+
if (result) {
|
|
2964
|
+
this.handleIDCardResult(result);
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
catch (error) {
|
|
2969
|
+
// 如果处理失败,显示错误
|
|
2970
|
+
this.resultContainer.innerHTML = `
|
|
2971
|
+
<h3>识别结果:</h3>
|
|
2972
|
+
<img src="${imageUrl}" style="max-width: 100%; max-height: 300px; margin-bottom: 10px;">
|
|
2973
|
+
<p class="error">未能识别内容,请尝试其他图片或调整图片质量</p>
|
|
2974
|
+
<p class="error">${error instanceof Error ? error.message : "未知错误"}</p>
|
|
2975
|
+
`;
|
|
2976
|
+
}
|
|
2977
|
+
// 清除文件选择,允许再次选择相同的文件
|
|
2978
|
+
input.value = "";
|
|
2979
|
+
}
|
|
2980
|
+
catch (error) {
|
|
2981
|
+
this.handleError(error instanceof Error ? error : new Error(String(error)));
|
|
2982
|
+
// 清除文件选择
|
|
2983
|
+
input.value = "";
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
handleQRCodeResult(result) {
|
|
2987
|
+
this.resultContainer.innerHTML = `
|
|
2988
|
+
<h3>扫描结果:</h3>
|
|
2989
|
+
<p>${result}</p>
|
|
2990
|
+
`;
|
|
2991
|
+
}
|
|
2992
|
+
handleIDCardResult(info) {
|
|
2993
|
+
this.resultContainer.innerHTML = `
|
|
2994
|
+
<h3>身份证信息:</h3>
|
|
2995
|
+
<p>姓名: ${info.name || "未识别"}</p>
|
|
2996
|
+
<p>性别: ${info.gender || "未识别"}</p>
|
|
2997
|
+
<p>民族: ${info.nationality || "未识别"}</p>
|
|
2998
|
+
<p>出生日期: ${info.birthDate || "未识别"}</p>
|
|
2999
|
+
<p>地址: ${info.address || "未识别"}</p>
|
|
3000
|
+
<p>身份证号: ${info.idNumber || "未识别"}</p>
|
|
3001
|
+
<p>签发机关: ${info.issuingAuthority || "未识别"}</p>
|
|
3002
|
+
<p>有效期限: ${info.validPeriod || "未识别"}</p>
|
|
3003
|
+
`;
|
|
3004
|
+
}
|
|
3005
|
+
handleError(error) {
|
|
3006
|
+
console.error("扫描错误:", error);
|
|
3007
|
+
this.resultContainer.innerHTML = `
|
|
3008
|
+
<div class="error">
|
|
3009
|
+
<h3>错误:</h3>
|
|
3010
|
+
<p>${error.message}</p>
|
|
3011
|
+
</div>
|
|
3012
|
+
`;
|
|
3013
|
+
}
|
|
3014
|
+
stop() {
|
|
3015
|
+
if (this.scanner && typeof this.scanner.stop === "function") {
|
|
3016
|
+
this.scanner.stop();
|
|
3017
|
+
}
|
|
3018
|
+
if (this.scanner && typeof this.scanner.terminate === "function") {
|
|
3019
|
+
this.scanner.terminate();
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
/**
|
|
3025
|
+
* @file ID扫描识别库UMD格式入口文件
|
|
3026
|
+
* @description 专门为UMD格式构建的入口,使用静态导入而非动态导入
|
|
3027
|
+
* @module IDScannerLib
|
|
3028
|
+
* @version 1.1.0
|
|
3029
|
+
* @license MIT
|
|
3030
|
+
*/
|
|
3031
|
+
/**
|
|
3032
|
+
* IDScanner 主类
|
|
3033
|
+
* UMD版本使用静态导入实现
|
|
3034
|
+
*/
|
|
3035
|
+
class IDScanner {
|
|
3036
|
+
/**
|
|
3037
|
+
* 构造函数
|
|
3038
|
+
* @param options 配置选项
|
|
3039
|
+
*/
|
|
3040
|
+
constructor(options = {}) {
|
|
3041
|
+
this.options = options;
|
|
3042
|
+
this.qrScanner = null;
|
|
3043
|
+
this.barcodeScanner = null;
|
|
3044
|
+
this.idDetector = null;
|
|
3045
|
+
this.ocrProcessor = null;
|
|
3046
|
+
this.dataExtractor = null;
|
|
3047
|
+
this.scanMode = "qr";
|
|
3048
|
+
this.videoElement = null;
|
|
3049
|
+
this.camera = new Camera(options.cameraOptions);
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* 初始化模块
|
|
3053
|
+
* 根据需要初始化OCR引擎
|
|
3054
|
+
*/
|
|
3055
|
+
async initialize() {
|
|
3056
|
+
try {
|
|
3057
|
+
// 初始化OCR模块
|
|
3058
|
+
this.ocrProcessor = new OCRProcessor();
|
|
3059
|
+
this.dataExtractor = new DataExtractor();
|
|
3060
|
+
await this.ocrProcessor.initialize();
|
|
3061
|
+
console.log("IDScanner initialized");
|
|
3062
|
+
}
|
|
3063
|
+
catch (error) {
|
|
3064
|
+
this.handleError(error);
|
|
3065
|
+
throw error;
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
/**
|
|
3069
|
+
* 启动二维码扫描
|
|
3070
|
+
* @param videoElement HTML视频元素
|
|
3071
|
+
*/
|
|
3072
|
+
async startQRScanner(videoElement) {
|
|
3073
|
+
this.stop();
|
|
1808
3074
|
this.videoElement = videoElement;
|
|
1809
|
-
this.scanMode =
|
|
3075
|
+
this.scanMode = "qr";
|
|
3076
|
+
try {
|
|
3077
|
+
if (!this.qrScanner) {
|
|
3078
|
+
this.qrScanner = new QRScanner({
|
|
3079
|
+
...this.options.qrScannerOptions,
|
|
3080
|
+
onScan: this.handleQRScan.bind(this),
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
await this.camera.start(videoElement);
|
|
3084
|
+
this.qrScanner.start(videoElement);
|
|
3085
|
+
}
|
|
3086
|
+
catch (error) {
|
|
3087
|
+
this.handleError(error);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
/**
|
|
3091
|
+
* 启动条形码扫描
|
|
3092
|
+
* @param videoElement HTML视频元素
|
|
3093
|
+
*/
|
|
3094
|
+
async startBarcodeScanner(videoElement) {
|
|
3095
|
+
this.stop();
|
|
3096
|
+
this.videoElement = videoElement;
|
|
3097
|
+
this.scanMode = "barcode";
|
|
3098
|
+
try {
|
|
3099
|
+
if (!this.barcodeScanner) {
|
|
3100
|
+
this.barcodeScanner = new BarcodeScanner({
|
|
3101
|
+
...this.options.barcodeScannerOptions,
|
|
3102
|
+
onScan: this.handleBarcodeScan.bind(this),
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
await this.camera.start(videoElement);
|
|
3106
|
+
this.barcodeScanner.start(videoElement);
|
|
3107
|
+
}
|
|
3108
|
+
catch (error) {
|
|
3109
|
+
this.handleError(error);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* 启动身份证扫描
|
|
3114
|
+
* @param videoElement HTML视频元素
|
|
3115
|
+
*/
|
|
3116
|
+
async startIDCardScanner(videoElement) {
|
|
3117
|
+
this.stop();
|
|
3118
|
+
this.videoElement = videoElement;
|
|
3119
|
+
this.scanMode = "idcard";
|
|
3120
|
+
try {
|
|
3121
|
+
if (!this.ocrProcessor) {
|
|
3122
|
+
await this.initialize();
|
|
3123
|
+
}
|
|
3124
|
+
if (!this.idDetector) {
|
|
3125
|
+
this.idDetector = new IDCardDetector({
|
|
3126
|
+
onDetection: this.handleIDDetection.bind(this),
|
|
3127
|
+
onError: this.handleError.bind(this),
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
await this.camera.start(videoElement);
|
|
3131
|
+
this.idDetector.start(videoElement);
|
|
3132
|
+
}
|
|
3133
|
+
catch (error) {
|
|
3134
|
+
this.handleError(error);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
/**
|
|
3138
|
+
* 停止扫描
|
|
3139
|
+
*/
|
|
3140
|
+
stop() {
|
|
3141
|
+
if (this.scanMode === "qr" && this.qrScanner) {
|
|
3142
|
+
this.qrScanner.stop();
|
|
3143
|
+
}
|
|
3144
|
+
else if (this.scanMode === "barcode" && this.barcodeScanner) {
|
|
3145
|
+
this.barcodeScanner.stop();
|
|
3146
|
+
}
|
|
3147
|
+
else if (this.scanMode === "idcard" && this.idDetector) {
|
|
3148
|
+
this.idDetector.stop();
|
|
3149
|
+
}
|
|
3150
|
+
this.camera.stop();
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* 处理二维码扫描结果
|
|
3154
|
+
*/
|
|
3155
|
+
handleQRScan(result) {
|
|
3156
|
+
if (this.options.onQRCodeScanned) {
|
|
3157
|
+
this.options.onQRCodeScanned(result);
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
/**
|
|
3161
|
+
* 处理条形码扫描结果
|
|
3162
|
+
*/
|
|
3163
|
+
handleBarcodeScan(result) {
|
|
3164
|
+
if (this.options.onBarcodeScanned) {
|
|
3165
|
+
this.options.onBarcodeScanned(result);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
/**
|
|
3169
|
+
* 处理身份证检测结果
|
|
3170
|
+
*/
|
|
3171
|
+
async handleIDDetection(result) {
|
|
3172
|
+
if (!this.ocrProcessor || !this.dataExtractor)
|
|
3173
|
+
return;
|
|
3174
|
+
try {
|
|
3175
|
+
// 检查 imageData 是否存在
|
|
3176
|
+
if (!result.imageData) {
|
|
3177
|
+
this.handleError(new Error("无效的图像数据"));
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
const idCardInfo = await this.ocrProcessor.processIDCard(result.imageData);
|
|
3181
|
+
const extractedInfo = this.dataExtractor.extractAndValidate(idCardInfo);
|
|
3182
|
+
if (this.options.onIDCardScanned) {
|
|
3183
|
+
this.options.onIDCardScanned(extractedInfo);
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
catch (error) {
|
|
3187
|
+
this.handleError(error);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
/**
|
|
3191
|
+
* 处理错误
|
|
3192
|
+
*/
|
|
3193
|
+
handleError(error) {
|
|
3194
|
+
if (this.options.onError) {
|
|
3195
|
+
this.options.onError(error);
|
|
3196
|
+
}
|
|
3197
|
+
else {
|
|
3198
|
+
console.error("IDScanner error:", error);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
/**
|
|
3202
|
+
* 释放资源
|
|
3203
|
+
*/
|
|
3204
|
+
async terminate() {
|
|
3205
|
+
this.stop();
|
|
3206
|
+
if (this.ocrProcessor) {
|
|
3207
|
+
await this.ocrProcessor.terminate();
|
|
3208
|
+
this.ocrProcessor = null;
|
|
3209
|
+
}
|
|
3210
|
+
this.qrScanner = null;
|
|
3211
|
+
this.barcodeScanner = null;
|
|
3212
|
+
this.idDetector = null;
|
|
3213
|
+
this.dataExtractor = null;
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* 处理图片中的二维码
|
|
3217
|
+
* @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
|
|
3218
|
+
* @returns 返回Promise,解析为扫描结果
|
|
3219
|
+
*/
|
|
3220
|
+
async processQRCodeImage(imageSource) {
|
|
1810
3221
|
try {
|
|
1811
3222
|
if (!this.qrScanner) {
|
|
1812
3223
|
this.qrScanner = new QRScanner({
|
|
1813
3224
|
...this.options.qrScannerOptions,
|
|
1814
|
-
onScan: this.handleQRScan.bind(this)
|
|
3225
|
+
onScan: this.handleQRScan.bind(this),
|
|
1815
3226
|
});
|
|
1816
3227
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
3228
|
+
// 处理不同类型的图片源
|
|
3229
|
+
let imageElement;
|
|
3230
|
+
if (typeof imageSource === "string") {
|
|
3231
|
+
// 如果是URL字符串,创建新的Image元素并加载图片
|
|
3232
|
+
imageElement = new Image();
|
|
3233
|
+
imageElement.crossOrigin = "anonymous"; // 处理跨域图片
|
|
3234
|
+
await new Promise((resolve, reject) => {
|
|
3235
|
+
imageElement.onload = resolve;
|
|
3236
|
+
imageElement.onerror = reject;
|
|
3237
|
+
imageElement.src = imageSource;
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
else if (imageSource instanceof HTMLImageElement) {
|
|
3241
|
+
// 如果已经是Image元素,直接使用
|
|
3242
|
+
imageElement = imageSource;
|
|
3243
|
+
}
|
|
3244
|
+
else if (imageSource instanceof HTMLCanvasElement) {
|
|
3245
|
+
// 如果是Canvas元素,创建新的Image元素并从Canvas获取数据
|
|
3246
|
+
const dataURL = imageSource.toDataURL();
|
|
3247
|
+
imageElement = new Image();
|
|
3248
|
+
await new Promise((resolve, reject) => {
|
|
3249
|
+
imageElement.onload = resolve;
|
|
3250
|
+
imageElement.onerror = reject;
|
|
3251
|
+
imageElement.src = dataURL;
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
else {
|
|
3255
|
+
throw new Error("不支持的图片源类型");
|
|
3256
|
+
}
|
|
3257
|
+
// 创建Canvas处理图片
|
|
3258
|
+
const canvas = document.createElement("canvas");
|
|
3259
|
+
const ctx = canvas.getContext("2d");
|
|
3260
|
+
if (!ctx) {
|
|
3261
|
+
throw new Error("无法创建Canvas上下文");
|
|
3262
|
+
}
|
|
3263
|
+
// 设置Canvas尺寸与图片相同
|
|
3264
|
+
canvas.width = imageElement.naturalWidth;
|
|
3265
|
+
canvas.height = imageElement.naturalHeight;
|
|
3266
|
+
ctx.drawImage(imageElement, 0, 0);
|
|
3267
|
+
// 获取图像数据并处理
|
|
3268
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
3269
|
+
return new Promise((resolve, reject) => {
|
|
3270
|
+
try {
|
|
3271
|
+
const result = this.qrScanner?.processImageData(imageData);
|
|
3272
|
+
if (result) {
|
|
3273
|
+
resolve(result);
|
|
3274
|
+
}
|
|
3275
|
+
else {
|
|
3276
|
+
reject(new Error("未检测到二维码"));
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
catch (error) {
|
|
3280
|
+
reject(error);
|
|
3281
|
+
}
|
|
3282
|
+
});
|
|
1819
3283
|
}
|
|
1820
3284
|
catch (error) {
|
|
1821
3285
|
this.handleError(error);
|
|
3286
|
+
throw error;
|
|
1822
3287
|
}
|
|
1823
3288
|
}
|
|
1824
3289
|
/**
|
|
1825
|
-
*
|
|
1826
|
-
* @param
|
|
3290
|
+
* 处理图片中的条形码
|
|
3291
|
+
* @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
|
|
3292
|
+
* @returns 返回Promise,解析为扫描结果
|
|
1827
3293
|
*/
|
|
1828
|
-
async
|
|
1829
|
-
this.stop();
|
|
1830
|
-
this.videoElement = videoElement;
|
|
1831
|
-
this.scanMode = 'barcode';
|
|
3294
|
+
async processBarcodeImage(imageSource) {
|
|
1832
3295
|
try {
|
|
1833
3296
|
if (!this.barcodeScanner) {
|
|
1834
3297
|
this.barcodeScanner = new BarcodeScanner({
|
|
1835
3298
|
...this.options.barcodeScannerOptions,
|
|
1836
|
-
onScan: this.handleBarcodeScan.bind(this)
|
|
3299
|
+
onScan: this.handleBarcodeScan.bind(this),
|
|
1837
3300
|
});
|
|
1838
3301
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
3302
|
+
// 处理不同类型的图片源
|
|
3303
|
+
let imageElement;
|
|
3304
|
+
if (typeof imageSource === "string") {
|
|
3305
|
+
// 如果是URL字符串,创建新的Image元素并加载图片
|
|
3306
|
+
imageElement = new Image();
|
|
3307
|
+
imageElement.crossOrigin = "anonymous"; // 处理跨域图片
|
|
3308
|
+
await new Promise((resolve, reject) => {
|
|
3309
|
+
imageElement.onload = resolve;
|
|
3310
|
+
imageElement.onerror = reject;
|
|
3311
|
+
imageElement.src = imageSource;
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
else if (imageSource instanceof HTMLImageElement) {
|
|
3315
|
+
// 如果已经是Image元素,直接使用
|
|
3316
|
+
imageElement = imageSource;
|
|
3317
|
+
}
|
|
3318
|
+
else if (imageSource instanceof HTMLCanvasElement) {
|
|
3319
|
+
// 如果是Canvas元素,创建新的Image元素并从Canvas获取数据
|
|
3320
|
+
const dataURL = imageSource.toDataURL();
|
|
3321
|
+
imageElement = new Image();
|
|
3322
|
+
await new Promise((resolve, reject) => {
|
|
3323
|
+
imageElement.onload = resolve;
|
|
3324
|
+
imageElement.onerror = reject;
|
|
3325
|
+
imageElement.src = dataURL;
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
else {
|
|
3329
|
+
throw new Error("不支持的图片源类型");
|
|
3330
|
+
}
|
|
3331
|
+
// 创建Canvas处理图片
|
|
3332
|
+
const canvas = document.createElement("canvas");
|
|
3333
|
+
const ctx = canvas.getContext("2d");
|
|
3334
|
+
if (!ctx) {
|
|
3335
|
+
throw new Error("无法创建Canvas上下文");
|
|
3336
|
+
}
|
|
3337
|
+
// 设置Canvas尺寸与图片相同
|
|
3338
|
+
canvas.width = imageElement.naturalWidth;
|
|
3339
|
+
canvas.height = imageElement.naturalHeight;
|
|
3340
|
+
ctx.drawImage(imageElement, 0, 0);
|
|
3341
|
+
// 获取图像数据并处理
|
|
3342
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
3343
|
+
return new Promise((resolve, reject) => {
|
|
3344
|
+
try {
|
|
3345
|
+
const result = this.barcodeScanner?.processImageData(imageData);
|
|
3346
|
+
if (result) {
|
|
3347
|
+
resolve(result);
|
|
3348
|
+
}
|
|
3349
|
+
else {
|
|
3350
|
+
reject(new Error("未检测到条形码"));
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
catch (error) {
|
|
3354
|
+
reject(error);
|
|
3355
|
+
}
|
|
3356
|
+
});
|
|
1841
3357
|
}
|
|
1842
3358
|
catch (error) {
|
|
1843
3359
|
this.handleError(error);
|
|
3360
|
+
throw error;
|
|
1844
3361
|
}
|
|
1845
3362
|
}
|
|
1846
3363
|
/**
|
|
1847
|
-
*
|
|
1848
|
-
* @param
|
|
3364
|
+
* 处理图片中的身份证
|
|
3365
|
+
* @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
|
|
3366
|
+
* @returns 返回Promise,解析为身份证信息
|
|
1849
3367
|
*/
|
|
1850
|
-
async
|
|
1851
|
-
this.stop();
|
|
1852
|
-
this.videoElement = videoElement;
|
|
1853
|
-
this.scanMode = 'idcard';
|
|
3368
|
+
async processIDCardImage(imageSource) {
|
|
1854
3369
|
try {
|
|
1855
|
-
if (!this.ocrProcessor) {
|
|
3370
|
+
if (!this.ocrProcessor || !this.dataExtractor) {
|
|
1856
3371
|
await this.initialize();
|
|
1857
3372
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
3373
|
+
// 处理不同类型的图片源
|
|
3374
|
+
let imageElement;
|
|
3375
|
+
if (typeof imageSource === "string") {
|
|
3376
|
+
// 如果是URL字符串,创建新的Image元素并加载图片
|
|
3377
|
+
imageElement = new Image();
|
|
3378
|
+
imageElement.crossOrigin = "anonymous"; // 处理跨域图片
|
|
3379
|
+
await new Promise((resolve, reject) => {
|
|
3380
|
+
imageElement.onload = resolve;
|
|
3381
|
+
imageElement.onerror = reject;
|
|
3382
|
+
imageElement.src = imageSource;
|
|
1862
3383
|
});
|
|
1863
3384
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
3385
|
+
else if (imageSource instanceof HTMLImageElement) {
|
|
3386
|
+
// 如果已经是Image元素,直接使用
|
|
3387
|
+
imageElement = imageSource;
|
|
3388
|
+
}
|
|
3389
|
+
else if (imageSource instanceof HTMLCanvasElement) {
|
|
3390
|
+
// 如果是Canvas元素,创建新的Image元素并从Canvas获取数据
|
|
3391
|
+
const dataURL = imageSource.toDataURL();
|
|
3392
|
+
imageElement = new Image();
|
|
3393
|
+
await new Promise((resolve, reject) => {
|
|
3394
|
+
imageElement.onload = resolve;
|
|
3395
|
+
imageElement.onerror = reject;
|
|
3396
|
+
imageElement.src = dataURL;
|
|
3397
|
+
});
|
|
3398
|
+
}
|
|
3399
|
+
else {
|
|
3400
|
+
throw new Error("不支持的图片源类型");
|
|
3401
|
+
}
|
|
3402
|
+
// 创建Canvas处理图片
|
|
3403
|
+
const canvas = document.createElement("canvas");
|
|
3404
|
+
const ctx = canvas.getContext("2d");
|
|
3405
|
+
if (!ctx) {
|
|
3406
|
+
throw new Error("无法创建Canvas上下文");
|
|
3407
|
+
}
|
|
3408
|
+
// 设置Canvas尺寸与图片相同
|
|
3409
|
+
canvas.width = imageElement.naturalWidth;
|
|
3410
|
+
canvas.height = imageElement.naturalHeight;
|
|
3411
|
+
ctx.drawImage(imageElement, 0, 0);
|
|
3412
|
+
// 获取图像数据并处理
|
|
3413
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
3414
|
+
// 调用OCR处理器处理身份证图像
|
|
3415
|
+
const ocrResult = await this.ocrProcessor.processIDCard(imageData);
|
|
3416
|
+
const extractedInfo = this.dataExtractor.extractAndValidate(ocrResult);
|
|
3417
|
+
if (this.options.onIDCardScanned) {
|
|
3418
|
+
this.options.onIDCardScanned(extractedInfo);
|
|
3419
|
+
}
|
|
3420
|
+
return extractedInfo;
|
|
1866
3421
|
}
|
|
1867
3422
|
catch (error) {
|
|
1868
3423
|
this.handleError(error);
|
|
3424
|
+
throw error;
|
|
1869
3425
|
}
|
|
1870
3426
|
}
|
|
3427
|
+
}
|
|
3428
|
+
// 添加静态属性IDScannerDemo,使其能被通过IDScanner.IDScannerDemo访问
|
|
3429
|
+
IDScanner.IDScannerDemo = IDScannerDemo;
|
|
3430
|
+
|
|
3431
|
+
/**
|
|
3432
|
+
* @file OCR模块入口文件
|
|
3433
|
+
* @description 包含身份证OCR识别相关功能
|
|
3434
|
+
* @module IDScannerOCR
|
|
3435
|
+
* @version 1.0.0
|
|
3436
|
+
* @license MIT
|
|
3437
|
+
*/
|
|
3438
|
+
/**
|
|
3439
|
+
* OCR模块类
|
|
3440
|
+
*
|
|
3441
|
+
* 提供身份证检测和OCR文字识别功能
|
|
3442
|
+
*/
|
|
3443
|
+
class OCRModule {
|
|
3444
|
+
/**
|
|
3445
|
+
* 构造函数
|
|
3446
|
+
* @param options 配置选项
|
|
3447
|
+
*/
|
|
3448
|
+
constructor(options = {}) {
|
|
3449
|
+
this.options = options;
|
|
3450
|
+
this.isRunning = false;
|
|
3451
|
+
this.videoElement = null;
|
|
3452
|
+
this.camera = new Camera(options.cameraOptions);
|
|
3453
|
+
this.idDetector = new IDCardDetector({
|
|
3454
|
+
onDetection: this.handleIDDetection.bind(this),
|
|
3455
|
+
onError: this.handleError.bind(this),
|
|
3456
|
+
});
|
|
3457
|
+
this.ocrProcessor = new OCRProcessor();
|
|
3458
|
+
this.dataExtractor = new DataExtractor();
|
|
3459
|
+
}
|
|
1871
3460
|
/**
|
|
1872
|
-
*
|
|
3461
|
+
* 初始化OCR引擎
|
|
3462
|
+
*
|
|
3463
|
+
* @returns Promise<void>
|
|
1873
3464
|
*/
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
this.
|
|
1877
|
-
|
|
1878
|
-
else if (this.scanMode === 'barcode' && this.barcodeScanner) {
|
|
1879
|
-
this.barcodeScanner.stop();
|
|
3465
|
+
async initialize() {
|
|
3466
|
+
try {
|
|
3467
|
+
await this.ocrProcessor.initialize();
|
|
3468
|
+
console.log("OCR engine initialized");
|
|
1880
3469
|
}
|
|
1881
|
-
|
|
1882
|
-
this.
|
|
3470
|
+
catch (error) {
|
|
3471
|
+
this.handleError(error);
|
|
3472
|
+
throw error;
|
|
1883
3473
|
}
|
|
1884
|
-
this.camera.stop();
|
|
1885
3474
|
}
|
|
1886
3475
|
/**
|
|
1887
|
-
*
|
|
3476
|
+
* 启动身份证扫描
|
|
3477
|
+
* @param videoElement HTML视频元素
|
|
1888
3478
|
*/
|
|
1889
|
-
|
|
1890
|
-
if (this.
|
|
1891
|
-
|
|
3479
|
+
async startIDCardScanner(videoElement) {
|
|
3480
|
+
if (!this.ocrProcessor) {
|
|
3481
|
+
throw new Error("OCR engine not initialized. Call initialize() first.");
|
|
1892
3482
|
}
|
|
3483
|
+
this.videoElement = videoElement;
|
|
3484
|
+
this.isRunning = true;
|
|
3485
|
+
await this.camera.start(videoElement);
|
|
3486
|
+
this.idDetector.start(videoElement);
|
|
1893
3487
|
}
|
|
1894
3488
|
/**
|
|
1895
|
-
*
|
|
3489
|
+
* 停止扫描
|
|
1896
3490
|
*/
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
3491
|
+
stop() {
|
|
3492
|
+
this.isRunning = false;
|
|
3493
|
+
this.idDetector.stop();
|
|
3494
|
+
this.camera.stop();
|
|
1901
3495
|
}
|
|
1902
3496
|
/**
|
|
1903
3497
|
* 处理身份证检测结果
|
|
1904
3498
|
*/
|
|
1905
3499
|
async handleIDDetection(result) {
|
|
1906
|
-
if (!this.
|
|
3500
|
+
if (!this.isRunning)
|
|
1907
3501
|
return;
|
|
1908
3502
|
try {
|
|
1909
3503
|
// 检查 imageData 是否存在
|
|
1910
3504
|
if (!result.imageData) {
|
|
1911
|
-
this.handleError(new Error(
|
|
3505
|
+
this.handleError(new Error("无效的图像数据"));
|
|
1912
3506
|
return;
|
|
1913
3507
|
}
|
|
1914
3508
|
const idCardInfo = await this.ocrProcessor.processIDCard(result.imageData);
|
|
@@ -1929,7 +3523,7 @@
|
|
|
1929
3523
|
this.options.onError(error);
|
|
1930
3524
|
}
|
|
1931
3525
|
else {
|
|
1932
|
-
console.error(
|
|
3526
|
+
console.error("OCRModule error:", error);
|
|
1933
3527
|
}
|
|
1934
3528
|
}
|
|
1935
3529
|
/**
|
|
@@ -1937,24 +3531,210 @@
|
|
|
1937
3531
|
*/
|
|
1938
3532
|
async terminate() {
|
|
1939
3533
|
this.stop();
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
3534
|
+
await this.ocrProcessor.terminate();
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* 直接处理图像数据中的身份证
|
|
3538
|
+
* @param imageData 要处理的图像数据
|
|
3539
|
+
* @returns 返回Promise,解析为身份证信息
|
|
3540
|
+
*/
|
|
3541
|
+
async processIDCard(imageData) {
|
|
3542
|
+
try {
|
|
3543
|
+
if (!this.ocrProcessor) {
|
|
3544
|
+
throw new Error("OCR engine not initialized. Call initialize() first.");
|
|
3545
|
+
}
|
|
3546
|
+
// 检查图像数据有效性
|
|
3547
|
+
if (!imageData ||
|
|
3548
|
+
!imageData.data ||
|
|
3549
|
+
imageData.width <= 0 ||
|
|
3550
|
+
imageData.height <= 0) {
|
|
3551
|
+
throw new Error("无效的图像数据");
|
|
3552
|
+
}
|
|
3553
|
+
// 进行图像预处理,提高识别率
|
|
3554
|
+
const processedImage = ImageProcessor.adjustBrightnessContrast(imageData, 5, // 轻微提高亮度
|
|
3555
|
+
10 // 适度提高对比度
|
|
3556
|
+
);
|
|
3557
|
+
// 调用OCR处理器进行文字识别
|
|
3558
|
+
const idCardInfo = await this.ocrProcessor.processIDCard(processedImage);
|
|
3559
|
+
// 提取和验证身份证信息
|
|
3560
|
+
const extractedInfo = this.dataExtractor.extractAndValidate(idCardInfo);
|
|
3561
|
+
// 如果有回调,触发回调
|
|
3562
|
+
if (this.options.onIDCardScanned) {
|
|
3563
|
+
this.options.onIDCardScanned(extractedInfo);
|
|
3564
|
+
}
|
|
3565
|
+
return extractedInfo;
|
|
3566
|
+
}
|
|
3567
|
+
catch (error) {
|
|
3568
|
+
this.handleError(error);
|
|
3569
|
+
throw error;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
var ocrModule = /*#__PURE__*/Object.freeze({
|
|
3575
|
+
__proto__: null,
|
|
3576
|
+
DataExtractor: DataExtractor,
|
|
3577
|
+
IDCardDetector: IDCardDetector,
|
|
3578
|
+
OCRModule: OCRModule,
|
|
3579
|
+
OCRProcessor: OCRProcessor
|
|
3580
|
+
});
|
|
3581
|
+
|
|
3582
|
+
/**
|
|
3583
|
+
* @file 二维码和条形码扫描模块
|
|
3584
|
+
* @description 包含二维码和条形码扫描功能
|
|
3585
|
+
* @module IDScannerQR
|
|
3586
|
+
* @version 1.0.0
|
|
3587
|
+
* @license MIT
|
|
3588
|
+
*/
|
|
3589
|
+
/**
|
|
3590
|
+
* 扫描模块类
|
|
3591
|
+
*
|
|
3592
|
+
* 提供独立的二维码和条形码扫描功能
|
|
3593
|
+
*/
|
|
3594
|
+
class ScannerModule {
|
|
3595
|
+
/**
|
|
3596
|
+
* 构造函数
|
|
3597
|
+
* @param options 配置选项
|
|
3598
|
+
*/
|
|
3599
|
+
constructor(options = {}) {
|
|
3600
|
+
this.options = options;
|
|
3601
|
+
this.scanMode = null;
|
|
3602
|
+
this.videoElement = null;
|
|
3603
|
+
this.camera = new Camera(options.cameraOptions);
|
|
3604
|
+
this.qrScanner = new QRScanner({
|
|
3605
|
+
...options.qrScannerOptions,
|
|
3606
|
+
onScan: this.handleQRScan.bind(this),
|
|
3607
|
+
});
|
|
3608
|
+
this.barcodeScanner = new BarcodeScanner({
|
|
3609
|
+
...options.barcodeScannerOptions,
|
|
3610
|
+
onScan: this.handleBarcodeScan.bind(this),
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
/**
|
|
3614
|
+
* 启动二维码扫描
|
|
3615
|
+
* @param videoElement HTML视频元素
|
|
3616
|
+
*/
|
|
3617
|
+
async startQRScanner(videoElement) {
|
|
3618
|
+
this.stop(); // 确保先停止可能正在运行的扫描
|
|
3619
|
+
this.videoElement = videoElement;
|
|
3620
|
+
this.scanMode = "qr";
|
|
3621
|
+
await this.camera.start(videoElement);
|
|
3622
|
+
this.qrScanner.start(videoElement);
|
|
3623
|
+
}
|
|
3624
|
+
/**
|
|
3625
|
+
* 启动条形码扫描
|
|
3626
|
+
* @param videoElement HTML视频元素
|
|
3627
|
+
*/
|
|
3628
|
+
async startBarcodeScanner(videoElement) {
|
|
3629
|
+
this.stop(); // 确保先停止可能正在运行的扫描
|
|
3630
|
+
this.videoElement = videoElement;
|
|
3631
|
+
this.scanMode = "barcode";
|
|
3632
|
+
await this.camera.start(videoElement);
|
|
3633
|
+
this.barcodeScanner.start(videoElement);
|
|
3634
|
+
}
|
|
3635
|
+
/**
|
|
3636
|
+
* 停止扫描
|
|
3637
|
+
*/
|
|
3638
|
+
stop() {
|
|
3639
|
+
if (this.scanMode === "qr") {
|
|
3640
|
+
this.qrScanner.stop();
|
|
3641
|
+
}
|
|
3642
|
+
else if (this.scanMode === "barcode") {
|
|
3643
|
+
this.barcodeScanner.stop();
|
|
3644
|
+
}
|
|
3645
|
+
if (this.videoElement) {
|
|
3646
|
+
this.camera.stop();
|
|
3647
|
+
}
|
|
3648
|
+
this.scanMode = null;
|
|
3649
|
+
}
|
|
3650
|
+
/**
|
|
3651
|
+
* 处理二维码扫描结果
|
|
3652
|
+
*/
|
|
3653
|
+
handleQRScan(result) {
|
|
3654
|
+
if (this.options.onQRCodeScanned) {
|
|
3655
|
+
this.options.onQRCodeScanned(result);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
* 处理条形码扫描结果
|
|
3660
|
+
*/
|
|
3661
|
+
handleBarcodeScan(result) {
|
|
3662
|
+
if (this.options.onBarcodeScanned) {
|
|
3663
|
+
this.options.onBarcodeScanned(result);
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* 处理错误
|
|
3668
|
+
*/
|
|
3669
|
+
handleError(error) {
|
|
3670
|
+
if (this.options.onError) {
|
|
3671
|
+
this.options.onError(error);
|
|
3672
|
+
}
|
|
3673
|
+
else {
|
|
3674
|
+
console.error("ScannerModule error:", error);
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
/**
|
|
3678
|
+
* 处理图像数据中的二维码
|
|
3679
|
+
* @param imageData 要处理的图像数据
|
|
3680
|
+
* @returns 返回Promise,解析为扫描结果
|
|
3681
|
+
*/
|
|
3682
|
+
async processQRCodeImage(imageData) {
|
|
3683
|
+
try {
|
|
3684
|
+
const result = this.qrScanner.processImageData(imageData);
|
|
3685
|
+
if (result) {
|
|
3686
|
+
// 如果需要,触发回调
|
|
3687
|
+
if (this.options.onQRCodeScanned) {
|
|
3688
|
+
this.options.onQRCodeScanned(result);
|
|
3689
|
+
}
|
|
3690
|
+
return result;
|
|
3691
|
+
}
|
|
3692
|
+
throw new Error("未检测到二维码");
|
|
3693
|
+
}
|
|
3694
|
+
catch (error) {
|
|
3695
|
+
this.handleError(error);
|
|
3696
|
+
throw error;
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
/**
|
|
3700
|
+
* 处理图像数据中的条形码
|
|
3701
|
+
* @param imageData 要处理的图像数据
|
|
3702
|
+
* @returns 返回Promise,解析为扫描结果
|
|
3703
|
+
*/
|
|
3704
|
+
async processBarcodeImage(imageData) {
|
|
3705
|
+
try {
|
|
3706
|
+
const result = this.barcodeScanner.processImageData(imageData);
|
|
3707
|
+
if (result) {
|
|
3708
|
+
// 如果需要,触发回调
|
|
3709
|
+
if (this.options.onBarcodeScanned) {
|
|
3710
|
+
this.options.onBarcodeScanned(result);
|
|
3711
|
+
}
|
|
3712
|
+
return result;
|
|
3713
|
+
}
|
|
3714
|
+
throw new Error("未检测到条形码");
|
|
3715
|
+
}
|
|
3716
|
+
catch (error) {
|
|
3717
|
+
this.handleError(error);
|
|
3718
|
+
throw error;
|
|
1943
3719
|
}
|
|
1944
|
-
this.qrScanner = null;
|
|
1945
|
-
this.barcodeScanner = null;
|
|
1946
|
-
this.idDetector = null;
|
|
1947
|
-
this.dataExtractor = null;
|
|
1948
3720
|
}
|
|
1949
3721
|
}
|
|
1950
3722
|
|
|
3723
|
+
var qrModule = /*#__PURE__*/Object.freeze({
|
|
3724
|
+
__proto__: null,
|
|
3725
|
+
BarcodeScanner: BarcodeScanner,
|
|
3726
|
+
Camera: Camera,
|
|
3727
|
+
QRScanner: QRScanner,
|
|
3728
|
+
ScannerModule: ScannerModule
|
|
3729
|
+
});
|
|
3730
|
+
|
|
1951
3731
|
exports.BarcodeScanner = BarcodeScanner;
|
|
1952
3732
|
exports.DataExtractor = DataExtractor;
|
|
1953
3733
|
exports.IDCardDetector = IDCardDetector;
|
|
1954
3734
|
exports.IDScanner = IDScanner;
|
|
3735
|
+
exports.IDScannerDemo = IDScannerDemo;
|
|
1955
3736
|
exports.ImageProcessor = ImageProcessor;
|
|
1956
3737
|
exports.OCRProcessor = OCRProcessor;
|
|
1957
3738
|
exports.QRScanner = QRScanner;
|
|
1958
3739
|
|
|
1959
3740
|
}));
|
|
1960
|
-
//# sourceMappingURL=id-scanner.js.map
|