UNPKG

id-scanner-lib

Version:

Browser-based ID card, QR code, and face recognition scanner with liveness detection

1,635 lines (1,625 loc) 176 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('tesseract.js'), require('browser-image-compression'), require('jsqr')) : typeof define === 'function' && define.amd ? define(['exports', 'tesseract.js', 'browser-image-compression', 'jsqr'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.IDScannerLib = {}, global.tesseract_js, global.imageCompression, global.jsQR)); })(this, (function (exports, tesseract_js, imageCompression, jsQR) { 'use strict'; /** * @file 配置管理器 + Scanner 配置 * @description 提供全局配置管理功能 + Scanner v2.0 配置接口 * @module core/config */ // ============================================================ // 原有 ConfigManager(保持向后兼容) // ============================================================ class ConfigManager { constructor() { this.config = {}; this.changeCallbacks = new Map(); this.initialized = false; this._resetDefaults(); } static getInstance() { if (!ConfigManager.instance) { ConfigManager.instance = new ConfigManager(); } return ConfigManager.instance; } static resetInstance() { ConfigManager.instance = undefined; } _resetDefaults() { this.config = { debug: false, logLevel: 'info', modelPath: '/models', maxRetries: 3, retryDelay: 1000, detectionInterval: 100, face: { enabled: true, confidenceThreshold: 0.5, maxFaces: 10, }, idCard: { enabled: false, }, qr: { enabled: false, }, }; this.initialized = true; } get(key, defaultValue) { const keys = key.split('.'); let value = this.config; for (const k of keys) { value = value?.[k]; } return (value !== undefined ? value : defaultValue); } set(key, value) { const keys = key.split('.'); let target = this.config; for (let i = 0; i < keys.length - 1; i++) { if (!target[keys[i]]) target[keys[i]] = {}; target = target[keys[i]]; } const oldValue = target[keys[keys.length - 1]]; target[keys[keys.length - 1]] = value; const callbacks = this.changeCallbacks.get(key); if (callbacks) { callbacks.forEach(cb => cb(value, oldValue)); } } onChange(key, callback) { if (!this.changeCallbacks.has(key)) { this.changeCallbacks.set(key, []); } this.changeCallbacks.get(key).push(callback); } /** @deprecated Use onChange instead */ onConfigChange(key, callback) { this.onChange(key, callback); } removeChangeCallback(key, callback) { const callbacks = this.changeCallbacks.get(key); if (callbacks) { const index = callbacks.indexOf(callback); if (index > -1) callbacks.splice(index, 1); } } reset() { this.config = {}; this._resetDefaults(); } } /** * @file 日志系统 * @description 提供统一的日志记录与管理功能 * @module core/logger */ /** * 日志级别枚举 */ exports.LoggerLevel = void 0; (function (LoggerLevel) { LoggerLevel["DEBUG"] = "debug"; LoggerLevel["INFO"] = "info"; LoggerLevel["WARN"] = "warn"; LoggerLevel["ERROR"] = "error"; })(exports.LoggerLevel || (exports.LoggerLevel = {})); /** * @deprecated 使用 LoggerLevel 代替 */ const LogLevel = exports.LoggerLevel; /** * 控制台日志处理器 * 将日志输出到浏览器控制台 */ class ConsoleLogHandler { /** * 处理日志条目 * @param entry 日志条目 */ handle(entry) { const timestamp = new Date(entry.timestamp).toISOString(); const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`; // 优先使用 error,其次使用 data const extra = entry.error || entry.data || ''; switch (entry.level) { case exports.LoggerLevel.DEBUG: console.debug(prefix, entry.message, extra); break; case exports.LoggerLevel.INFO: console.info(prefix, entry.message, extra); break; case exports.LoggerLevel.WARN: console.warn(prefix, entry.message, extra); break; case exports.LoggerLevel.ERROR: console.error(prefix, entry.message, extra); break; // 输出什么也不做 } } } /** * 内存日志处理器 * 将日志保存在内存中,用于后续分析或显示 */ class MemoryLogHandler { /** * 构造函数 * @param maxEntries 最大日志条目数,默认为1000 */ constructor(maxEntries = 1000) { /** 日志条目数组 */ this.entries = []; this.maxEntries = maxEntries; } /** * 处理日志条目 * @param entry 日志条目 */ handle(entry) { this.entries.push(entry); // 如果超过最大条目数,移除最老的 if (this.entries.length > this.maxEntries) { this.entries.shift(); } } /** * 获取所有日志条目 */ getEntries() { return [...this.entries]; } /** * 根据级别过滤日志条目 * @param level 日志级别 */ getEntriesByLevel(level) { return this.entries.filter(entry => entry.level === level); } /** * 根据标签过滤日志条目 * @param tag 日志标签 */ getEntriesByTag(tag) { return this.entries.filter(entry => entry.tag === tag); } /** * 清空日志 */ clear() { this.entries = []; } } /** * 远程日志处理器 * 将日志发送到远程服务器 */ class RemoteLogHandler { /** * 构造函数 * @param endpoint 远程服务器URL * @param maxQueueSize 最大队列长度,默认为100 * @param flushInterval 发送间隔(毫秒),默认为5000 */ constructor(endpoint, maxQueueSize = 100, flushInterval = 5000) { /** 批量发送的队列 */ this.queue = []; /** 定时发送的计时器ID */ this.timerId = null; /** 当前连续失败计数 */ this.consecutiveFailures = 0; this.endpoint = endpoint; this.maxQueueSize = maxQueueSize; this.flushInterval = flushInterval; this.isBrowser = typeof window !== 'undefined' && typeof window.addEventListener === 'function'; this.maxConsecutiveFailures = 10; // 设置定时发送 this.startTimer(); // 页面卸载前尝试发送剩余日志 if (this.isBrowser) { window.addEventListener('beforeunload', () => { this.flush(); }); } } /** * 处理日志条目 * @param entry 日志条目 */ handle(entry) { // 只处理INFO以上级别的日志 if (entry.level >= exports.LoggerLevel.INFO) { this.queue.push(entry); // 如果队列满了,立即发送 if (this.queue.length >= this.maxQueueSize) { this.flush(); } } } /** * 发送队列中的日志 */ flush() { if (this.queue.length === 0) return; // 如果连续失败次数过多,停止发送以防止无限循环 if (this.consecutiveFailures >= this.maxConsecutiveFailures) { console.warn('RemoteLogHandler: Too many consecutive failures, stopping. Clear queue.'); this.queue = []; this.consecutiveFailures = 0; return; } const entriesToSend = [...this.queue]; this.queue = []; this.sendLogEntries(entriesToSend); } /** * 发送日志条目到远程服务器 * @param entries 日志条目数组 */ sendLogEntries(entries) { if (entries.length === 0) return; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s 超时 fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entries), keepalive: true, signal: controller.signal }).then(() => { clearTimeout(timeoutId); this.consecutiveFailures = 0; // 发送成功,重置失败计数 }).catch((err) => { clearTimeout(timeoutId); console.error('Failed to send logs to remote server:', err); this.consecutiveFailures++; // 如果失败次数过多,丢弃日志防止内存泄漏 if (this.consecutiveFailures >= this.maxConsecutiveFailures) { console.warn('RemoteLogHandler: Max consecutive failures exceeded, discarding logs'); this.queue = []; this.consecutiveFailures = 0; return; } // 失败时把日志放回队列,但防止无限增长 const maxReturn = Math.min(entries.length, this.maxQueueSize - this.queue.length); if (maxReturn > 0) { const returnedEntries = entries.slice(0, maxReturn); this.queue = [...returnedEntries, ...this.queue]; } }); } /** * 开始定时发送 */ startTimer() { if (!this.isBrowser) return; if (this.timerId !== null) return; this.timerId = setInterval(() => { this.flush(); }, this.flushInterval); } /** * 停止定时发送 */ stopTimer() { if (this.timerId !== null) { clearInterval(this.timerId); this.timerId = null; } } } /** * 日志管理类 * 中央日志管理器,提供统一的日志记录接口 */ class Logger { /** * 私有构造函数,防止直接实例化 */ constructor() { /** 日志处理器 */ this.handlers = []; /** 默认标签 */ this.defaultTag = 'IDScanner'; /** 日志级别 */ this.logLevel = exports.LoggerLevel.INFO; this.config = ConfigManager.getInstance(); // 默认添加控制台处理器 this.addHandler(new ConsoleLogHandler()); // 监听配置变化 this.config.onConfigChange('logLevel', (level) => { this.debug('Logger', `Log level changed to ${level}`); }); } /** * 获取单例实例 */ static getInstance() { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; } /** * 重置单例实例(主要用于测试) */ static resetInstance() { Logger.instance = undefined; } /** * 添加日志处理器 * @param handler 日志处理器 */ addHandler(handler) { this.handlers.push(handler); } /** * 移除日志处理器 * @param handler 要移除的处理器 */ removeHandler(handler) { const index = this.handlers.indexOf(handler); if (index !== -1) { this.handlers.splice(index, 1); } } /** * 移除所有处理器 */ clearHandlers() { this.handlers = []; } /** * 设置默认标签 * @param tag 默认标签 */ setDefaultTag(tag) { this.defaultTag = tag; } /** * 记录调试级别日志 * @param tag 标签 * @param message 消息 * @param errorOrData 错误对象或结构化数据 */ debug(tag, message, errorOrData) { if (errorOrData instanceof Error) { this.log(exports.LoggerLevel.DEBUG, tag, message, errorOrData); } else { this.logWithData(exports.LoggerLevel.DEBUG, tag, message, errorOrData); } } /** * 记录信息级别日志 * @param tag 标签 * @param message 消息 * @param errorOrData 错误对象或结构化数据 */ info(tag, message, errorOrData) { if (errorOrData instanceof Error) { this.log(exports.LoggerLevel.INFO, tag, message, errorOrData); } else { this.logWithData(exports.LoggerLevel.INFO, tag, message, errorOrData); } } /** * 记录警告级别日志 * @param tag 标签 * @param message 消息 * @param errorOrData 错误对象或结构化数据 */ warn(tag, message, errorOrData) { if (errorOrData instanceof Error) { this.log(exports.LoggerLevel.WARN, tag, message, errorOrData); } else { this.logWithData(exports.LoggerLevel.WARN, tag, message, errorOrData); } } /** * 记录错误级别日志 * @param tag 标签 * @param message 消息 * @param errorOrData 错误对象或结构化数据 */ error(tag, message, errorOrData) { if (errorOrData instanceof Error) { this.log(exports.LoggerLevel.ERROR, tag, message, errorOrData); } else { this.logWithData(exports.LoggerLevel.ERROR, tag, message, errorOrData); } } /** * 创建标记了特定标签的日志记录器 * @param tag 标签 */ getTaggedLogger(tag) { return new TaggedLogger(this, tag); } /** * 记录日志 * @param level 日志级别 * @param tag 标签 * @param message 消息 * @param error 错误 */ log(level, tag, message, error) { // 检查日志级别 const levelValue = this.getLevelValue(level); const currentLevelValue = this.getLevelValue(this.logLevel); if (levelValue < currentLevelValue) { return; } // 创建日志条目 const entry = { timestamp: Date.now(), level: level, tag: tag || this.defaultTag, message, error }; // 分发到所有处理程序 for (const handler of this.handlers) { try { handler.handle(entry); } catch (handlerError) { console.error(`[Logger] 处理程序错误:`, handlerError); } } // 如果没有处理程序,使用控制台 if (this.handlers.length === 0) { this.consoleOutput(entry); } } /** * 记录日志(支持结构化数据) * @param level 日志级别 * @param tag 标签 * @param message 消息 * @param data 结构化数据 */ logWithData(level, tag, message, data) { // 检查日志级别 const levelValue = this.getLevelValue(level); const currentLevelValue = this.getLevelValue(this.logLevel); if (levelValue < currentLevelValue) { return; } // 创建日志条目 const entry = { timestamp: Date.now(), level: level, tag: tag || this.defaultTag, message, data }; // 分发到所有处理程序 for (const handler of this.handlers) { try { handler.handle(entry); } catch (handlerError) { console.error(`[Logger] 处理程序错误:`, handlerError); } } // 如果没有处理程序,使用控制台 if (this.handlers.length === 0) { this.consoleOutput(entry); } } /** * 控制台输出 * @param entry 日志条目 */ consoleOutput(entry) { const timestamp = new Date(entry.timestamp).toISOString(); const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`; // 构造日志内容:错误对象或数据对象 const extra = entry.error || entry.data || ''; switch (entry.level) { case exports.LoggerLevel.DEBUG: console.debug(`${prefix} ${entry.message}`, extra); break; case exports.LoggerLevel.INFO: console.info(`${prefix} ${entry.message}`, extra); break; case exports.LoggerLevel.WARN: console.warn(`${prefix} ${entry.message}`, extra); break; case exports.LoggerLevel.ERROR: console.error(`${prefix} ${entry.message}`, extra); break; } } /** * 获取日志级别值 * @param level 日志级别 */ getLevelValue(level) { switch (level) { case exports.LoggerLevel.DEBUG: return 0; case exports.LoggerLevel.INFO: return 1; case exports.LoggerLevel.WARN: return 2; case exports.LoggerLevel.ERROR: return 3; default: return 1; // 默认INFO级别 } } /** * 设置日志级别 * @param level 日志级别 */ setLevel(level) { if (typeof level === 'string') { switch (level) { case 'debug': this.logLevel = exports.LoggerLevel.DEBUG; break; case 'info': this.logLevel = exports.LoggerLevel.INFO; break; case 'warn': this.logLevel = exports.LoggerLevel.WARN; break; case 'error': this.logLevel = exports.LoggerLevel.ERROR; break; default: this.logLevel = exports.LoggerLevel.INFO; } } else { this.logLevel = level; } this.debug('Logger', `日志级别已设置为 ${this.logLevel}`); } /** * 获取当前日志级别 * @returns 当前日志级别 */ getLevel() { return this.logLevel; } } /** * 带标签的日志记录器 * 提供特定标签的简易日志接口 */ class TaggedLogger { /** * 构造函数 * @param logger 所属的主日志记录器 * @param tag 标签 */ constructor(logger, tag) { this.logger = logger; this.tag = tag; } /** * 记录调试级别日志 * @param message 消息 * @param error 错误 */ debug(message, error) { this.logger.debug(this.tag, message, error); } /** * 记录信息级别日志 * @param message 消息 * @param error 错误 */ info(message, error) { this.logger.info(this.tag, message, error); } /** * 记录警告级别日志 * @param message 消息 * @param error 错误 */ warn(message, error) { this.logger.warn(this.tag, message, error); } /** * 记录错误级别日志 * @param message 消息 * @param error 错误 */ error(message, error) { this.logger.error(this.tag, message, error); } } /** * 日志级别枚举 */ /** * @file 事件发射器 * @description 提供基础的事件发射和订阅功能 * @module core/event-emitter */ /** * 事件发射器基类 * 提供基础的事件发射和订阅功能 */ class EventEmitter { constructor() { /** 事件处理器映射 */ this.eventHandlers = new Map(); } /** * 订阅事件 * @param eventName 事件名称 * @param handler 事件处理器 */ on(eventName, handler) { if (!this.eventHandlers.has(eventName)) { this.eventHandlers.set(eventName, new Set()); } this.eventHandlers.get(eventName).add(handler); } /** * 取消订阅事件 * @param eventName 事件名称 * @param handler 事件处理器,如果不提供则移除该事件的所有处理器 */ off(eventName, handler) { if (!this.eventHandlers.has(eventName)) { return; } if (handler) { this.eventHandlers.get(eventName).delete(handler); // 如果没有处理器了,删除这个事件 if (this.eventHandlers.get(eventName).size === 0) { this.eventHandlers.delete(eventName); } } else { // 移除该事件的所有处理器 this.eventHandlers.delete(eventName); } } /** * 取消订阅事件 (off的别名) * @param eventName 事件名称 * @param handler 事件处理器 */ removeListener(eventName, handler) { this.off(eventName, handler); } /** * 订阅事件,但只触发一次 * @param eventName 事件名称 * @param handler 事件处理器 */ once(eventName, handler) { const onceHandler = (data) => { handler(data); this.off(eventName, onceHandler); }; this.on(eventName, onceHandler); } /** * 发射事件 * @param eventName 事件名称 * @param data 事件数据 */ emit(eventName, data) { if (!this.eventHandlers.has(eventName)) { return; } for (const handler of this.eventHandlers.get(eventName)) { try { handler(data); } catch (error) { console.error(`Error in event handler for "${eventName}":`, error); } } } /** * 获取某个事件的处理器数量 * @param eventName 事件名称 */ listenerCount(eventName) { return this.eventHandlers.has(eventName) ? this.eventHandlers.get(eventName).size : 0; } /** * 移除所有事件处理器 */ removeAllListeners() { this.eventHandlers.clear(); } /** * 获取所有事件名称 */ eventNames() { return Array.from(this.eventHandlers.keys()); } } /** * @file 版本号文件 * @description 定义库的版本号 * @module Version */ // 当前版本号 const VERSION = '1.3.3'; // 构建日期 const BUILD_DATE = new Date().toISOString(); /* eslint-disable */ /** * @file 模块管理器 * @description 统一管理库的各功能模块,提供模块的注册、初始化和卸载功能 * @module core/module-manager */ /** * 模块管理器类 * 负责管理所有功能模块的生命周期 */ class ModuleManager extends EventEmitter { /** * 获取模块管理器单例 */ static getInstance() { if (!ModuleManager.instance) { ModuleManager.instance = new ModuleManager(); } return ModuleManager.instance; } /** * 重置单例实例(主要用于测试) */ static resetInstance() { ModuleManager.instance = undefined; } /** * 私有构造函数,确保单例模式 */ constructor() { super(); this.modules = new Map(); this.initialized = false; this.initPromise = null; this.logger = Logger.getInstance(); this.logger.debug('ModuleManager', `初始化模块管理器 v${VERSION}`); } /** * 注册模块 * @param module 要注册的模块 * @returns 模块管理器实例,支持链式调用 */ register(module) { if (this.modules.has(module.name)) { this.logger.warn('ModuleManager', `模块 "${module.name}" 已经注册,将被覆盖`); } this.modules.set(module.name, module); this.logger.debug('ModuleManager', `注册模块: ${module.name} v${module.version}`); this.emit('module:registered', { name: module.name }); return this; } /** * 获取模块 * @param name 模块名称 * @returns 模块实例 */ getModule(name) { return this.modules.get(name); } /** * 初始化所有注册的模块 */ async initialize() { if (this.initialized) { return; } this.logger.debug('ModuleManager', '开始初始化所有模块...'); for (const [name, module] of this.modules.entries()) { try { this.logger.debug('ModuleManager', `初始化模块: ${name}`); await module.initialize(); this.emit('module:initialized', { name }); this.logger.debug('ModuleManager', `模块 ${name} 初始化完成`); } catch (error) { this.logger.error('ModuleManager', `模块 ${name} 初始化失败`, error instanceof Error ? error : undefined); this.emit('module:error', { name, error }); throw new Error(`模块 ${name} 初始化失败: ${error instanceof Error ? error.message : String(error)}`); } } this.initialized = true; this.logger.debug('ModuleManager', '所有模块初始化完成'); this.emit('modules:initialized'); } /** * 卸载所有模块并释放资源 */ async dispose() { this.logger.debug('ModuleManager', '开始释放所有模块资源...'); for (const [name, module] of this.modules.entries()) { try { this.logger.debug('ModuleManager', `释放模块资源: ${name}`); await module.dispose(); this.emit('module:disposed', { name }); } catch (error) { this.logger.error('ModuleManager', `模块 ${name} 资源释放失败`, error instanceof Error ? error : undefined); this.emit('module:error', { name, error }); } } this.modules.clear(); this.initialized = false; this.logger.debug('ModuleManager', '所有模块资源已释放'); this.emit('modules:disposed'); } /** * 获取所有已注册的模块名称 */ getRegisteredModules() { return Array.from(this.modules.keys()); } /** * 检查模块是否已注册 * @param name 模块名称 */ hasModule(name) { return this.modules.has(name); } } /** * @file 基础模块 * @description 提供基础模块实现,作为所有功能模块的基类 * @module core/base-module */ /** * 基础模块类 * 提供模块的基本功能和生命周期管理 */ class BaseModule extends EventEmitter { /** * 构造函数 */ constructor() { super(); /** 模块版本 */ this.version = VERSION; /** 模块是否已初始化 */ this._isInitialized = false; this.logger = Logger.getInstance(); } /** * 获取模块是否已初始化 */ get isInitialized() { return this._isInitialized; } /** * 释放模块资源 * 子类可以覆盖此方法以添加额外的资源释放逻辑 */ async dispose() { if (!this._isInitialized) { return; } this.logger.debug(this.name, '释放模块资源'); // 重置初始化状态 this._isInitialized = false; // 删除所有事件监听器 this.removeAllListeners(); this.logger.debug(this.name, '模块资源已释放'); } /** * 检查模块是否已初始化,如果未初始化则抛出错误 */ ensureInitialized() { if (!this._isInitialized) { throw new Error(`模块 ${this.name} 尚未初始化`); } } } /** * @file 结果包装类 * @description 提供统一的操作结果封装 * @module core/result */ /** * 结果类型 * 用于封装操作的成功或失败结果 */ class Result { /** * 构造函数 * @param success 是否成功 * @param data 结果数据 * @param error 错误对象 * @param meta 元数据 */ constructor(success, data, error, meta) { this._success = success; this._data = data; this._error = error; this._meta = meta; } /** * 创建成功结果 * @param data 结果数据 * @param meta 元数据 */ static success(data, meta) { return new Result(true, data, undefined, meta); } /** * 创建失败结果 * @param error 错误对象 * @param meta 元数据 */ static failure(error, meta) { return new Result(false, undefined, error, meta); } /** * 检查结果是否成功 */ isSuccess() { return this._success; } /** * 检查结果是否失败 */ isFailure() { return !this._success; } /** * 获取结果数据 */ getData() { return this._data; } /** * 获取错误对象 */ getError() { return this._error; } /** * 获取元数据 */ getMeta() { return this._meta; } /** * 映射结果(如果成功) * @param fn 映射函数 */ map(fn) { if (this.isSuccess() && this._data !== undefined) { try { const newData = fn(this._data); return Result.success(newData, this._meta); } catch (error) { return Result.failure(error instanceof Error ? error : new Error(String(error)), this._meta); } } return Result.failure(this._error, this._meta); } /** * 如果成功,则执行函数 * @param fn 要执行的函数 */ onSuccess(fn) { if (this.isSuccess()) { try { fn(this._data); } catch (error) { console.error('Error in onSuccess handler:', error); } } return this; } /** * 如果失败,则执行函数 * @param fn 要执行的函数 */ onFailure(fn) { if (this.isFailure() && this._error) { try { fn(this._error); } catch (error) { console.error('Error in onFailure handler:', error); } } return this; } /** * 无论成功失败,都执行函数 * @param fn 要执行的函数 */ onFinally(fn) { try { fn(); } catch (error) { console.error('Error in onFinally handler:', error); } return this; } /** * 转换为字符串 */ toString() { if (this.isSuccess()) { return `Success: ${JSON.stringify(this._data)}`; } else { return `Failure: ${this._error?.message || 'Unknown error'}`; } } } /** * @file 身份证模块类型定义 * @description 身份证模块相关的类型和接口定义 * @module modules/id-card/types */ /** * 身份证类型枚举 */ exports.IDCardType = void 0; (function (IDCardType) { /** 第二代居民身份证正面 */ IDCardType["FRONT"] = "front"; /** 第二代居民身份证背面 */ IDCardType["BACK"] = "back"; /** 第一代居民身份证 */ IDCardType["FIRST_GENERATION"] = "first_generation"; /** 临时身份证 */ IDCardType["TEMPORARY"] = "temporary"; /** 外国人永久居留证 */ IDCardType["FOREIGN_PERMANENT"] = "foreign_permanent"; /** 港澳台居民居住证 */ IDCardType["HMT_RESIDENT"] = "hmt_resident"; /** 未知类型 */ IDCardType["UNKNOWN"] = "unknown"; })(exports.IDCardType || (exports.IDCardType = {})); /** * @file 身份证检测器 * @description 提供身份证检测和解析功能 * @module modules/id-card/id-card-detector */ /** * 身份证检测器类 */ class IDCardDetector extends EventEmitter { /** * 构造函数 * @param options 配置选项 */ constructor(options = {}) { super(); this.initialized = false; this.models = {}; /** 重用的 Canvas 元素,用于减少内存分配 */ this.reusableCanvas = null; this.reusableContext = null; this.options = { enabled: true, minConfidence: 0.7, detectType: true, detectEdge: true, enableEdgeDetection: false, enableOCR: true, cropAndAlign: true, enableAntiFake: false, returnImage: false, modelPath: '/models/id-card', ...options }; this.logger = Logger.getInstance(); } /** * 初始化检测器 */ async initialize() { if (this.initialized || !this.options.enabled) { return; } this.logger.debug('IDCardDetector', '初始化身份证检测器'); try { // 加载检测模型 await this.loadDetectionModel(); // 如果启用OCR,加载OCR模型 if (this.options.enableOCR) { await this.loadOCRModel(); } // 如果启用防伪检测,加载防伪模型 if (this.options.enableAntiFake) { await this.loadAntiFakeModel(); } this.initialized = true; this.emit('detector:initialized', {}); this.logger.debug('IDCardDetector', '身份证检测器初始化完成'); } catch (error) { this.logger.error('IDCardDetector', '身份证检测器初始化失败', error); throw error; } } /** * 加载检测模型 * @private */ async loadDetectionModel() { // 实际项目中,这里应该加载检测模型 this.logger.debug('IDCardDetector', '加载身份证检测模型'); // 模拟加载模型的延迟 await new Promise(resolve => setTimeout(resolve, 100)); // 设置模型 this.models.detection = { loaded: true, name: 'id-card-detection' }; } /** * 加载OCR模型 * @private */ async loadOCRModel() { // 实际项目中,这里应该加载OCR模型 this.logger.debug('IDCardDetector', '加载身份证OCR模型'); // 模拟加载模型的延迟 await new Promise(resolve => setTimeout(resolve, 100)); // 设置模型 this.models.ocr = { loaded: true, name: 'id-card-ocr' }; } /** * 加载防伪模型 * @private */ async loadAntiFakeModel() { // 实际项目中,这里应该加载防伪模型 this.logger.debug('IDCardDetector', '加载身份证防伪模型'); // 模拟加载模型的延迟 await new Promise(resolve => setTimeout(resolve, 100)); // 设置模型 this.models.antiFake = { loaded: true, name: 'id-card-anti-fake' }; } /** * 处理图像 * @param image 图像源(可以是ImageData、HTMLImageElement、HTMLCanvasElement等) * @param processOptions 图像处理选项 * @returns 处理结果 */ async processImage(image, processOptions = {}) { if (!this.initialized) { return Result.failure(new Error('身份证检测器未初始化')); } // 输入验证 if (!image) { return Result.failure(new Error('图像源不能为空')); } // 验证 HTMLImageElement 是否已加载 if (image instanceof HTMLImageElement && !image.complete) { return Result.failure(new Error('图像尚未加载完成')); } // 验证 ImageData 尺寸 if (image instanceof ImageData) { if (image.width === 0 || image.height === 0) { return Result.failure(new Error('图像尺寸无效')); } } // 验证 Canvas 尺寸 if (image instanceof HTMLCanvasElement) { if (image.width === 0 || image.height === 0) { return Result.failure(new Error('Canvas尺寸无效')); } } try { this.logger.debug('IDCardDetector', '开始处理图像'); // 预处理图像 const processedImage = await this.preprocessImage(image, processOptions); // 检测身份证 const detectionResult = await this.detectIDCard(processedImage); if (!detectionResult || detectionResult.confidence < (this.options.minConfidence || 0.7)) { return Result.failure(new Error('未检测到身份证或置信度过低')); } let idCardInfo = { type: detectionResult.type, edge: detectionResult.edge, confidence: detectionResult.confidence }; // 如果启用OCR识别,提取文字信息 if (this.options.enableOCR && this.models.ocr) { // 裁剪并校正图像 const alignedImage = this.options.cropAndAlign ? await this.cropAndAlign(processedImage, detectionResult.edge) : processedImage; // 识别文字 const ocrResult = await this.recognizeText(alignedImage, detectionResult.type); // 合并结果 idCardInfo = { ...idCardInfo, ...ocrResult }; } // 如果启用防伪检测,进行防伪检测 if (this.options.enableAntiFake && this.models.antiFake) { const antiFakeResult = await this.detectAntiFake(processedImage, detectionResult); idCardInfo.antiFake = antiFakeResult; } // 如果需要返回原始图像 if (this.options.returnImage) { // 根据图像类型获取ImageData if (image instanceof ImageData) { idCardInfo.image = image; } else if (image instanceof HTMLCanvasElement) { const context = image.getContext('2d'); if (context) { idCardInfo.image = context.getImageData(0, 0, image.width, image.height); } } else if (image instanceof HTMLImageElement && image.complete) { const canvas = this.getReusableCanvas(image.naturalWidth, image.naturalHeight); const context = canvas.getContext('2d'); if (context) { context.drawImage(image, 0, 0); idCardInfo.image = context.getImageData(0, 0, canvas.width, canvas.height); } } } this.logger.debug('IDCardDetector', '图像处理完成'); this.emit('detector:result', { result: idCardInfo }); return Result.success(idCardInfo); } catch (error) { this.logger.error('IDCardDetector', '图像处理失败', error); return Result.failure(error); } } /** * 获取可重用的 Canvas 元素 * @param width 宽度 * @param height 高度 * @returns CanvasRenderingContext2D */ getReusableCanvas(width, height) { // 如果存在可重用的 canvas 且尺寸匹配,直接返回 if (this.reusableCanvas && this.reusableCanvas.width === width && this.reusableCanvas.height === height) { return this.reusableCanvas; } // 创建新的 canvas this.reusableCanvas = document.createElement('canvas'); this.reusableCanvas.width = width; this.reusableCanvas.height = height; this.reusableContext = this.reusableCanvas.getContext('2d'); return this.reusableCanvas; } /** * 预处理图像 * @param image 图像源 * @param options 处理选项 * @returns 处理后的图像 * @private */ async preprocessImage(image, options) { this.logger.debug('IDCardDetector', '预处理图像'); // 创建ImageData对象 let imageData; if (image instanceof ImageData) { imageData = image; } else { const width = image instanceof HTMLImageElement ? image.naturalWidth : image.width; const height = image instanceof HTMLImageElement ? image.naturalHeight : image.height; const canvas = this.getReusableCanvas(width, height); const context = canvas.getContext('2d'); if (!context) { throw new Error('无法获取Canvas上下文'); } if (image instanceof HTMLImageElement) { context.drawImage(image, 0, 0); } else { context.drawImage(image, 0, 0); } imageData = context.getImageData(0, 0, width, height); } // 应用图像处理选项 // 实际项目中,这里应该根据options进行相应的图像处理 return imageData; } /** * 检测身份证 * @param image 图像数据 * @returns 检测结果 * @private */ async detectIDCard(image) { // 实际项目中,这里应该调用模型进行身份证检测 this.logger.debug('IDCardDetector', '检测身份证'); // 模拟检测结果 // 在实际应用中,这里应该使用机器学习模型进行推理 return { type: exports.IDCardType.FRONT, edge: { topLeft: { x: 10, y: 10 }, topRight: { x: image.width - 10, y: 10 }, bottomRight: { x: image.width - 10, y: image.height - 10 }, bottomLeft: { x: 10, y: image.height - 10 } }, confidence: 0.95 }; } /** * 裁剪并校正图像 * @param image 图像数据 * @param edge 边缘信息 * @returns 校正后的图像 * @private */ async cropAndAlign(image, edge) { this.logger.debug('IDCardDetector', '裁剪并校正图像'); // 设置标准身份证尺寸比例 const standardWidth = 428; const standardHeight = 270; const canvas = this.getReusableCanvas(standardWidth, standardHeight); const context = canvas.getContext('2d'); if (!context) { throw new Error('无法获取Canvas上下文'); } // 创建临时Canvas用于源图像 const tempCanvas = document.createElement('canvas'); tempCanvas.width = image.width; tempCanvas.height = image.height; const tempContext = tempCanvas.getContext('2d'); if (!tempContext) { throw new Error('无法获取临时Canvas上下文'); } // 将ImageData绘制到临时Canvas tempContext.putImageData(image, 0, 0); // 在实际应用中,这里应该使用透视变换算法 // 例如使用Canvas的transform或WebGL进行变换 // 简化处理:直接裁剪 context.drawImage(tempCanvas, edge.topLeft.x, edge.topLeft.y, edge.topRight.x - edge.topLeft.x, edge.bottomLeft.y - edge.topLeft.y, 0, 0, standardWidth, standardHeight); return context.getImageData(0, 0, standardWidth, standardHeight); } /** * 识别文字 * * @note 此方法返回模拟数据,用于框架开发和测试 * 实际使用时需要替换为真实的 OCR 模型集成 * * @param image 图像数据 * @param type 身份证类型 * @returns 识别结果 * @private */ async recognizeText(image, type) { this.logger.debug('IDCardDetector', '识别文字'); // 模拟OCR结果 // 注意:这是框架的占位实现,真实场景需要接入实际的 OCR 服务 // 可选的方案包括: // - TensorFlow.js + 自定义 OCR 模型 // - 第三方 OCR API (如百度OCR、腾讯OCR) // - Tesseract.js WASM 版本 // if (type === exports.IDCardType.FRONT) { return { name: '张三', // TODO: 替换为真实OCR结果 gender: '男', // TODO: 替换为真实OCR结果 ethnicity: '汉', // TODO: 替换为真实OCR结果 birthDate: '1990-01-01', // TODO: 替换为真实OCR结果 address: '北京市朝阳区某某街道某某社区1号楼1单元101', // TODO: 替换为真实OCR结果 idNumber: '110101199001010001', // TODO: 替换为真实OCR结果 photoRegion: { x: 300, y: 40, width: 100, height: 130 } }; } else if (type === exports.IDCardType.BACK) { return { issueAuthority: '北京市公安局朝阳分局', // TODO: 替换为真实OCR结果 validFrom: '2015-01-01', // TODO: 替换为真实OCR结果 validTo: '2035-01-01' // TODO: 替换为真实OCR结果 }; } return {}; } /** * 检测防伪特征 * * @note 此方法返回模拟数据,用于框架开发和测试 * 实际使用时需要替换为真实的防伪检测模型 * * @param image 图像数据 * @param detectionResult 检测结果 * @returns 防伪检测结果 * @private */ async detectAntiFake(image, detectionResult) { this.logger.debug('IDCardDetector', '检测防伪特征'); // 模拟防伪检测结果 // 注意:这是框架的占位实现,真实场景需要接入实际的防伪检测模型 // 可选的方案包括: // - 紫外光特征检测 // - 红外光特征检测 // - 微缩文字检测 // - 光学变色特征检测 return { passed: true, score: 0.92, features: { fluorescent: true, // TODO: 替换为真实检测结果 microtext: true, // TODO: 替换为真实检测结果 opticalVariable: true, // TODO: 替换为真实检测结果 texture: true, // TODO: 替换为真实检测结果 watermark: true // TODO: 替换为真实检测结果 } }; } /** * 释放资源 */ dispose() { this.logger.debug('IDCardDetector', '释放资源'); // 清理模型 this.models = {}; this.initialized = false; // 清理可重用的 Canvas this.reusableCanvas = null; this.reusableContext = null; // 清理事件监听 this.removeAllListeners(); } } /** * 格式化日期字符串为标准格式 (YYYY-MM-DD) */ function formatDateString(dateStr) { const dateMatch = dateStr.match(/(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/); if (dateMatch) { const year = dateMatch[1]; const month = dateMatch[2].padStart(2, "0"); const day = dateMatch[3].padStart(2, "0"); return `${year}-${month}-${day}`; } if (/^\d{8}$/.test(dateStr)) { const year = dateStr.substring(0, 4); const month = dateStr.substring(4, 6); const day = dateStr.substring(6, 8); return `${year}-${month}-${day}`; } return dateStr; } /** * IDCardTextParser - 统一解析身份证OCR文本 * 提取 ocr-processor.ts 和 ocr-worker.ts 中的解析逻辑 */ class IDCardTextParser { /** * 解析身份证文本 * @param text OCR识别的原始文本 * @returns 解析后的身份证信息 */ static parse(text) { const info = {}; const processedText = text.replace(/\s+/g, " ").trim(); const lines = processedText.split("\n").filter((line) => line.trim()); // 1. 解析身份证号码 const idNumberRegex = /(\d{17}[\dX])/; const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/; const basicMatch = processedText.match(idNumberRegex); const prefixMatch = processedText.match(idNumberWithPrefixRegex); if (prefixMatch && prefixMatch[1]) { info.idNumber = prefixMatch[1]; } else if (basicMatch && basicMatch[1]) { info.idNumber = basicMatch[1]; } // 2. 解析姓名 const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/; const nameMatch = processedText.match(nameWithLabelRegex); if (nameMatch && nameMatch[1]) { info.name = nameMatch[1].trim(); } else { for (const line of lines) { if (line.length >= 2 && line.length <= 5 && /^[一-龥]+$/.test(line) && !/性别|民族|住址|公民|签发|有效/.test(line)) { info.name = line.trim(); break; } } } // 3. 解析性别和民族 const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/; const genderOnlyRegex = /性别[\s\:]*([男女])/; const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/; const genderNationalityMatch = processedText.match(genderAndNationalityRegex); const genderOnlyMatch = processedText.match(genderOnlyRegex); const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex); if (genderNationalityMatch) { info.gender = genderNationalityMatch[1]; info.ethnicity = genderNationalityMatch[2]; } else { if (genderOnlyMatch) info.gender = genderOnlyMatch[1]; if (nationalityOnlyMatch) info.ethnicity = nationalityOnlyMatch[1]; } // 4. 判断身份证类型 if (processedText.includes('出生') || processedText.includes('公民身份号码')) { info.type = exports.IDCardType.FRONT; } else if (processedText.includes('签发机关') || processedText.includes('有效期')) { info.type = exports.IDCardType.BACK; } // 5. 解析出生日期 const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/; const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/; const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/; const birthDateMatch = processedText.match(birthDateRegex1) || processedText.match(birthDateRegex2) || processedText.match(birthDateRegex3); if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) { const year = info.idNumber.substring(6, 10); const month = info.idNumber.substring(10, 12); const day = info.idNumber.substring(12, 14); info.birthDate = `${year}-${month}-${day}`; } else if (birthDateMatch) { const year = birthDateMatch[1]; const month = birthDateMatch[2].padStart(2, "0"); const day = birthDateMatch[3].padStart(2, "0"); info.birthDate = `${year}-${month}-${day}`; } // 6. 解析地址 const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/; const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/; const addressMatch = processedText.match(addressRegex1) || processedText.match(addressRegex2); if (addressMatch && addressMatch[1]) { info.address = addressMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim(); if (info.address.length > 70) info.address = info.address.substring(0, 70); if (!/[一-龥]/.test(info.address)) info.address = ''; } // 7. 解析签发机关 const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/; const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/; const authorityMatch = processedText.match(authorityRegex1) || processedText.match(authorityRegex2); if (authorityMatch && authorityMatch[1]) { info.issueAuthority = authorityMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim(); } // 8. 解析有效期限 const validPeriodRegex1 = /有效期限[\s\:]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日\s]*)[-\s]*(至|-)[-\s]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日]*|[永久长期]*)/; const vali