UNPKG

idcard-reader

Version:

通过二代身份证机具读取二代身份证信息

376 lines (369 loc) 12.9 kB
/** * idcard-reader * 通过二代身份证机具读取二代身份证信息 * * @version 5.0.0 * @author waiting * @license MIT * @link https://github.com/waitingsong/node-idcard-reader#readme */ import { join, tmpdir, dirname, isFileExists, fileExists, createFileAsync, normalize } from '@waiting/shared-core'; import { parseDeviceOpts, parseCompositeOpts, validateDllFile, testWrite, composite, formatBaseData, initialIDData, initialDataBase } from '@waiting/idcard-reader-base'; export { initialCompositeOpts, initialOpts, nationMap } from '@waiting/idcard-reader-base'; import { info, error } from '@waiting/log'; import { Library } from 'ffi-napi'; import { of, range, timer, iif, combineLatest } from 'rxjs'; import { mergeMap, timeout, tap, map, concatMap, filter, take, defaultIfEmpty, shareReplay, catchError } from 'rxjs/operators'; const config = { appDir: '', tmpDir: join(tmpdir(), 'idcard-reader'), }; const dllFuncs = { /** 查找设备端口 */ SDT_OpenPort: ['int', ['int']], /** 关闭端口 */ SDT_ClosePort: ['int', ['int']], /** 找卡 port,0,0 */ SDT_StartFindIDCard: ['int', ['int', 'pointer', 'int']], /** 选卡 */ SDT_SelectIDCard: ['int', ['int', 'pointer', 'int']], /** 读取基础信息 */ SDT_ReadBaseMsg: ['int', ['int', 'pointer', 'pointer', 'pointer', 'pointer', 'int']], /** 对 SAM 进行状态检测 */ SDT_GetSAMStatus: ['int', ['int', 'int']], /** 读取SAM_V的编号 返回值0x90-成功,其他-失败 */ SDT_GetSAMIDToStr: ['int', ['int', 'pointer', 'int']], /** 读取追加信息 */ SDT_ReadNewAppMsg: ['int', ['int', 'pointer', 'pointer', 'int']], // SDT_ReadAllAppMsg: ['int', ['int', 'pointer', 'pointer', 'int'] ], /** 重置SAM */ SDT_ResetSAM: ['int', ['int', 'int']], }; const dllImgFuncs = { /** 解码头像 */ GetBmp: ['int', ['string', 'int']], }; function connectDevice(device, port) { if (device && device.inUse) { device.deviceOpts.debug && info('Cautiton: connectDevice() device in use'); return 0; } const openRet = device.apib.SDT_OpenPort(port); device.deviceOpts.debug && info(`open device port ret: ${openRet}`); return openRet === 144 ? port : 0; } function disconnectDevice(device) { const ret = device.apib.SDT_ClosePort(device.openPort); device.deviceOpts.debug && info(`disconnectDevice at port: ${device.openPort}, ret: ${ret} `); device.inUse = false; return ret === 144; } function resetDevice(device, port) { if (port && port > 0) { const ret = device.apib.SDT_ResetSAM(port, 0); info(`reset ${port} ret: ${ret}`); } else { for (let i = 1; i <= 16; i += 1) { const ret = device.apib.SDT_ResetSAM(i, 0); info(`reset ${i} ret: ${ret}`); } for (let i = 1001; i <= 1016; i += 1) { const ret = device.apib.SDT_ResetSAM(i, 0); info(`reset ${i} ret: ${ret}`); } } device.deviceOpts.debug && info(`reset device at port: ${device.openPort}`); device.inUse = false; } function findDeviceList(deviceOpts, compositeOpts, apib) { const arr = []; if (deviceOpts.port > 0) { // 仅USB接口 const device = findDevice(deviceOpts.port, deviceOpts, compositeOpts, apib, true); if (device.openPort > 0) { arr.push(device); } } else { // 必须先检测usb端口 for (let i = 1001; i <= 1016; i += 1) { const device = findDevice(i, deviceOpts, compositeOpts, apib, true); if (device.openPort > 0) { // device.simid = getSamid(device) arr.push(device); if (!deviceOpts.searchAll) { break; } } } if (!deviceOpts.searchAll && arr.length) { return arr; } // 检测串口 for (let i = 1; i <= 16; i += 1) { const device = findDevice(i, deviceOpts, compositeOpts, apib, false); if (device.openPort > 0) { arr.push(device); if (!deviceOpts.searchAll) { break; } } } } return arr; } function findDevice(openPort, deviceOpts, compositeOpts, apib, useUsb) { const device = { apib, apii: null, deviceOpts, compositeOpts, inUse: false, openPort: 0, useUsb, }; const port = connectDevice(device, openPort); if (port > 0) { device.inUse = true; device.openPort = port; deviceOpts.debug && info(`Found device at serial/usb port: ${port}`); disconnectDevice(device); } return device; } /** 读取二代证基础信息 */ function readDataBase(device) { const path = dirname(device.deviceOpts.dllTxt); process.env.PATH = `${process.env.PATH};${path}`; // const srcDir = path.replace(/\\/g, '/') + '/' // const targetPath = normalize(device.deviceOpts.imgSaveDir + '/').replace(/\\/g, '/') if (device.deviceOpts.debug) { info('starting reading readCard '); // info('IDCard_GetInformation() src path:' + srcDir) // info('IDCard_GetInformation() target path:' + targetPath) } const open = connectDevice(device, device.openPort); if (!open) { throw new Error(`打开端口失败 readDataBase() port: ${device.openPort}`); } const cardReady$ = findCard(device).pipe(mergeMap((found) => { if (found) { return of(selectCard(device)).pipe(timeout(1500)); } else { throw new Error('findCard() 未能找到指定设备'); } }), tap((ready) => { if (!ready) { throw new Error('二代证无效,请确保证件处于机具读卡区域内'); } })); const ret$ = cardReady$.pipe(map(() => readCard(device)), tap((raw) => { if (device.deviceOpts.debug) { // info(`readDataBase bufLen: ${buf.byteLength}`) info('readDataBase ret'); info(raw); } })); return ret$; } /** 检测卡是否可读取状态 */ function findCard(device) { const { findCardRetryTimes } = device.deviceOpts; const findRet$ = range(0, findCardRetryTimes > 0 ? findCardRetryTimes + 1 : 1).pipe(concatMap((_, index) => { if (index > 0 && index >= findCardRetryTimes) { throw new Error(`findCard fail over ${findCardRetryTimes} times`); } // 移动中读取到卡 延迟执行选卡 const delay$ = timer(index === 0 ? 0 : 1000); return delay$.pipe(mergeMap(() => of(_findCard(device)))); })); const ret$ = findRet$.pipe(tap((ret) => { device.deviceOpts.debug && info(`findStatus: ${ret}`); }), filter(ret => ret === 159), take(1), defaultIfEmpty(0), map(ret => ret > 0)); return ret$; } function _findCard(device) { try { const buf = Buffer.alloc(4); return device.apib.SDT_StartFindIDCard(device.openPort, buf, 1); } catch (ex) { device.deviceOpts.debug && error(ex); return 0; } } /** 选卡 */ function selectCard(device) { const buf = Buffer.alloc(4); const res = device.apib.SDT_SelectIDCard(device.openPort, buf, 1); return res === 144; } function readCard(device) { const opts = { pucCHMsg: Buffer.alloc(1024), puiCHMsgLen: Buffer.from([1024]), pucPHMsg: Buffer.alloc(1024), puiPHMsgLen: Buffer.from([1024]), }; // console.log(opts) const data = { err: 1, code: 0, text: opts.pucCHMsg, image: opts.pucPHMsg, imagePath: '', }; try { data.code = device.apib.SDT_ReadBaseMsg(device.openPort, opts.pucCHMsg, opts.puiCHMsgLen, opts.pucPHMsg, opts.puiPHMsgLen, 1); } catch (ex) { console.error(ex); } if (data.code === 144) { data.err = 0; } else { resetDevice(device, device.openPort); } return data; } async function init(options) { const deviceOpts = parseDeviceOpts(options); const compositeOpts = parseCompositeOpts(options); const { debug } = deviceOpts; if (debug) { info(compositeOpts); info(deviceOpts); } await validateDllFile(deviceOpts.dllTxt); // 不允许 未指定照片解码dll if (compositeOpts.useComposite) { if (!deviceOpts.dllImage) { throw new Error('Value of dellImage empty'); } else if (!await isFileExists(deviceOpts.dllImage)) { throw new Error('File not exists: ' + deviceOpts.dllImage); } } await testWrite(deviceOpts.imgSaveDir); const apib = Library(deviceOpts.dllTxt, dllFuncs); const devices = findDeviceList(deviceOpts, compositeOpts, apib); if (devices && devices.length) { debug && console.info('Found devices:', devices); return devices; } else { throw new Error('未找到读卡设备'); } } /** Read card data */ function read(device) { if (device.openPort) { disconnectDevice(device); // 读卡获取原始buffer数据 const raw$ = readDataBase(device).pipe(shareReplay(1)); // 生成 base 数据 const base$ = raw$.pipe(tap((raw) => { if (raw.err) { throw new Error('读卡失败:code:' + raw.code.toString()); } }), map((raw) => { const base = pickFields(raw && raw.text.byteLength ? raw.text.toString('ucs2') : ''); return base; }), shareReplay(1)); // 解码头像 const imagePath$ = raw$.pipe(mergeMap(raw => retriveAvatar(raw.image, device)), mergeMap((imagePath) => { return fileExists(imagePath).pipe(tap((path) => { if (!path) { error(`解码头像文件失败 path: "${imagePath}"`); } })); })); // 合成图片 const imgsPath$ = iif(() => !device.compositeOpts.useComposite, of({ compositePath: '', imagePath: '', }), combineLatest(base$, imagePath$).pipe(mergeMap(([base, imagePath]) => { return composite(imagePath, base, device.compositeOpts).pipe(map((compositePath) => { return { compositePath, imagePath, }; })); }))); const ret$ = combineLatest(base$, imgsPath$).pipe(tap(() => { disconnectDevice(device); }), map(([base, imgsPath]) => { const ret = { ...initialIDData, ...imgsPath, base, }; return ret; }), timeout(20000), catchError((err) => { disconnectDevice(device); throw err; })); return ret$.toPromise(); } else { throw new Error('设备端口未指定'); } } /** pick fields from origin text */ function pickFields(text) { const ret = { ...initialDataBase }; if (!text || !text.length) { return ret; } ret.name = text.slice(0, 15).trim(); ret.gender = +text.slice(15, 16); ret.nation = text.slice(16, 18); // 民族 ret.birth = text.slice(18, 26); // 16 ret.address = text.slice(26, 61).trim(); // 70 ret.idc = text.slice(61, 79); // 身份证号 ret.regorg = text.slice(79, 94).trim(); // 签发机关 ret.startdate = text.slice(94, 102); ret.enddate = text.slice(102, 110); return formatBaseData(ret); } /** * 解码读取到的头像 Buffer,返回生成的图片路径 */ function retriveAvatar(image, device) { const opts = device.deviceOpts; device.apii = Library(opts.dllImage, dllImgFuncs); return decodeImage(device, image); } async function decodeImage(device, buf) { // console.log(buf.slice(0, 10)) const name = join(device.deviceOpts.imgSaveDir, _genImageName('idcrimage_')); const tmpname = name + '.wlt'; if (!device.apii) { return ''; } await createFileAsync(tmpname, buf); // covert wlt file to bmp const res = device.apii.GetBmp(tmpname, device.useUsb ? 2 : 1); device.deviceOpts.debug && info(['resolve image res:', res]); if (res === 1) { const ipath = normalize(name + '.bmp'); return ipath; } else { return ''; } } function _genImageName(prefix) { const dd = new Date(); const mon = dd.getMonth(); const day = dd.getDate(); const rstr = Math.random().toString().slice(-8); return `${prefix}${dd.getFullYear()}${mon > 9 ? mon : '0' + mon.toString()}${day > 9 ? day : '0' + day.toString()}_${rstr}`; } // base directory of this module config.appDir = join(__dirname, '/..'); export { init, pickFields, read, retriveAvatar }; //# sourceMappingURL=index.esm.js.map