diff --git a/pages/index/index.js b/pages/index/index.js index 57ab3b8..7f7d02a 100644 --- a/pages/index/index.js +++ b/pages/index/index.js @@ -1,6 +1,7 @@ const LAST_CONNECTED_DEVICE = 'last_connected_device' const PrinterJobs = require('../../printer/printerjobs') const printerUtil = require('../../printer/printerutil') +const imageUtil = require('../../utils/imageutil') function inArray(arr, key, val) { for (let i = 0; i < arr.length; i++) { @@ -36,7 +37,12 @@ Page({ data: { devices: [], connected: false, - chs: [] + chs: [], + isPrinting: false, + printProgress: 0, + printStatusText: '', + selectedImage: null, + imageData: null }, onUnload() { this.closeBluetoothAdapter() @@ -226,67 +232,314 @@ Page({ }) }, writeBLECharacteristicValue() { - let printerJobs = new PrinterJobs(); - printerJobs - .print('2018年12月5日17:34') - .print(printerUtil.fillLine()) - .setAlign('ct') - .setSize(2, 2) - .print('#20饿了么外卖') - .setSize(1, 1) - .print('切尔西Chelsea') - .setSize(2, 2) - .print('在线支付(已支付)') - .setSize(1, 1) - .print('订单号:5415221202244734') - .print('下单时间:2017-07-07 18:08:08') - .setAlign('lt') - .print(printerUtil.fillAround('一号口袋')) - .print(printerUtil.inline('意大利茄汁一面 * 1', '15')) - .print(printerUtil.fillAround('其他')) - .print('餐盒费:1') - .print('[赠送康师傅冰红茶] * 1') - .print(printerUtil.fillLine()) - .setAlign('rt') - .print('原价:¥16') - .print('总价:¥16') - .setAlign('lt') - .print(printerUtil.fillLine()) - .print('备注') - .print("无") - .print(printerUtil.fillLine()) - .println(); + this.startPrintJob(() => { + let printerJobs = new PrinterJobs(); + printerJobs + .print('2018年12月5日17:34') + .print(printerUtil.fillLine()) + .setAlign('ct') + .setSize(2, 2) + .print('#20饿了么外卖') + .setSize(1, 1) + .print('切尔西Chelsea') + .setSize(2, 2) + .print('在线支付(已支付)') + .setSize(1, 1) + .print('订单号:5415221202244734') + .print('下单时间:2017-07-07 18:08:08') + .setAlign('lt') + .print(printerUtil.fillAround('一号口袋')) + .print(printerUtil.inline('意大利茄汁一面 * 1', '15')) + .print(printerUtil.fillAround('其他')) + .print('餐盒费:1') + .print('[赠送康师傅冰红茶] * 1') + .print(printerUtil.fillLine()) + .setAlign('rt') + .print('原价:¥16') + .print('总价:¥16') + .setAlign('lt') + .print(printerUtil.fillLine()) + .print('备注') + .print("无") + .print(printerUtil.fillLine()) + .println(); - let buffer = printerJobs.buffer(); - console.log('ArrayBuffer', 'length: ' + buffer.byteLength, ' hex: ' + ab2hex(buffer)); - // 1.并行调用多次会存在写失败的可能性 - // 2.建议每次写入不超过20字节 - // 分包处理,延时调用 - const maxChunk = 20; - const delay = 20; - for (let i = 0, j = 0, length = buffer.byteLength; i < length; i += maxChunk, j++) { - let subPackage = buffer.slice(i, i + maxChunk <= length ? (i + maxChunk) : length); - setTimeout(this._writeBLECharacteristicValue, j * delay, subPackage); - } + return printerJobs.buffer(); + }); }, - _writeBLECharacteristicValue(buffer) { + /** + * 写入BLE特征值(单包数据发送) + * + * 详细记录每个数据包的发送情况,包括: + * - 包序号和总包数 + * - 数据包内容(十六进制) + * - 发送结果(成功/失败) + * + * @param {ArrayBuffer} buffer - 要发送的数据包 + * @param {number} packageNumber - 当前包序号(从1开始) + * @param {number} totalPackages - 总包数 + */ + _writeBLECharacteristicValue(buffer, packageNumber = 0, totalPackages = 0) { + // 详细的发送前日志 + if (packageNumber > 0) { + console.log(`\n--- 发送第${packageNumber}/${totalPackages}包数据 ---`); + console.log(`数据长度: ${buffer.byteLength} 字节`); + console.log(`数据十六进制: ${ab2hex(buffer)}`); + console.log(`时间戳: ${new Date().toLocaleTimeString()}`); + } + wx.writeBLECharacteristicValue({ deviceId: this._deviceId, serviceId: this._serviceId, characteristicId: this._characteristicId, value: buffer, - success(res) { - console.log('writeBLECharacteristicValue success', res) + success: (res) => { + console.log(`✅ 第${packageNumber}/${totalPackages}包发送成功`, res); + + // 更新打印进度 + if (packageNumber > 0) { + this.updatePrintProgress(); + } }, - fail(res) { - console.log('writeBLECharacteristicValue fail', res) + fail: (res) => { + console.error(`❌ 第${packageNumber}/${totalPackages}包发送失败`, res); + console.error(`错误详情: errCode=${res.errCode}, errMsg=${res.errMsg}`); + + // 可以在这里添加重试逻辑 + if (packageNumber > 0 && res.errCode === 10008) { + console.warn('特征值不支持写入操作,请检查特征值属性'); + } } }) }, + + /** + * 开始打印任务 + * + * 打印数据分包发送管理: + * 1. 获取完整的打印数据Buffer + * 2. 计算分包数量和进度 + * 3. 按顺序发送每个数据包 + * 4. 实时更新发送进度和状态 + * + * @param {Function} getBufferFunc - 获取打印数据的函数,返回ArrayBuffer + * + * 分包策略: + * - 每包最大20字节(微信小程序BLE限制) + * - 包间延时20ms,避免发送过快 + * - 实时显示发送进度 [当前包/总包数] + * + * @example + * this.startPrintJob(() => { + * let printerJobs = new PrinterJobs(); + * printerJobs.print('测试内容'); + * return printerJobs.buffer(); + * }); + */ + startPrintJob(getBufferFunc) { + try { + const buffer = getBufferFunc(); + + // 详细的调试信息输出 + console.log('=== 打印任务开始 ==='); + console.log(`总数据长度: ${buffer.byteLength} 字节`); + console.log(`完整数据十六进制: ${ab2hex(buffer)}`); + + // 设置打印状态 + this.setData({ + isPrinting: true, + printProgress: 0, + printStatusText: '准备打印...' + }); + + // 计算分包策略 + const maxChunk = 20; // 微信小程序BLE单次写入最大字节数 + const totalPackages = Math.ceil(buffer.byteLength / maxChunk); + this._totalPackages = totalPackages; + this._currentPackage = 0; + + console.log(`分包策略: 每包${maxChunk}字节,共${totalPackages}包`); + + // 分包处理,延时调用 + const delay = 20; // 包间延时,避免发送过快 + + for (let i = 0, j = 0, length = buffer.byteLength; i < length; i += maxChunk, j++) { + // 计算当前包的起始和结束位置 + const startPos = i; + const endPos = Math.min(i + maxChunk, length); + const subPackage = buffer.slice(startPos, endPos); + + // 调试输出每个分包的信息 + console.log(`第${j + 1}包: 位置[${startPos}-${endPos}], 长度${subPackage.byteLength}字节`); + console.log(`第${j + 1}包十六进制: ${ab2hex(subPackage)}`); + + // 设置定时器发送数据包 + setTimeout(this._writeBLECharacteristicValue, j * delay, subPackage, j + 1, totalPackages); + } + + // 设置完成定时器(预留1秒缓冲时间) + const totalTime = totalPackages * delay + 1000; + console.log(`预计发送完成时间: ${totalTime}ms`); + + setTimeout(() => { + this.completePrintJob(); + }, totalTime); + + } catch (error) { + console.error('打印任务失败:', error); + this.setData({ + isPrinting: false, + printStatusText: '打印失败: ' + error.message + }); + } + }, + + /** + * 更新打印进度 + * + * 实时计算并显示当前打印进度: + * - 计算百分比进度 + * - 更新UI状态 + * - 记录进度日志 + */ + updatePrintProgress() { + this._currentPackage++; + const progress = Math.round((this._currentPackage / this._totalPackages) * 100); + + console.log(`📊 打印进度更新: ${this._currentPackage}/${this._totalPackages} = ${progress}%`); + + this.setData({ + printProgress: progress, + printStatusText: `正在传输 [${this._currentPackage}/${this._totalPackages}]` + }); + }, + + /** + * 完成打印任务 + * + * 处理打印完成状态: + * 1. 设置100%进度 + * 2. 显示成功消息 + * 3. 3秒后自动隐藏状态 + * + * 如果所有包都成功发送,显示成功消息; + * 否则显示警告信息。 + */ + completePrintJob() { + const allPackagesSent = this._currentPackage >= this._totalPackages; + + if (allPackagesSent) { + console.log('✅ 打印任务完成:所有数据包发送成功'); + this.setData({ + printProgress: 100, + printStatusText: '打印指令发送成功' + }); + } else { + console.warn(`⚠️ 打印任务完成:仅发送了 ${this._currentPackage}/${this._totalPackages} 包`); + this.setData({ + printProgress: Math.round((this._currentPackage / this._totalPackages) * 100), + printStatusText: '打印数据发送不完整' + }); + } + + // 3秒后隐藏状态 + setTimeout(() => { + console.log('打印状态UI已隐藏'); + this.setData({ + isPrinting: false, + printProgress: 0, + printStatusText: '' + }); + + // 重置计数器 + this._currentPackage = 0; + this._totalPackages = 0; + }, 3000); + }, closeBluetoothAdapter() { wx.closeBluetoothAdapter() this._discoveryStarted = false }, + /** + * 打印二维码 + */ + printQRCode() { + this.startPrintJob(() => { + let printerJobs = new PrinterJobs(); + printerJobs + .setAlign('ct') + .print('扫码关注') + .qrcode('https://github.com/your-repo', 8, 'M') + .lineFeed(2); + + return printerJobs.buffer(); + }); + }, + + /** + * 选择并预览图片 + */ + printImage() { + imageUtil.chooseAndProcessImage() + .then(result => { + this.setData({ + selectedImage: result.imagePath, + imageData: { + bitmapData: result.bitmapData, + width: result.width, + height: result.height + } + }); + }) + .catch(error => { + wx.showToast({ + title: '图片处理失败', + icon: 'none' + }); + console.error('图片处理失败:', error); + }); + }, + + /** + * 确认打印图片 + */ + confirmPrintImage() { + if (!this.data.imageData) { + wx.showToast({ + title: '没有图片数据', + icon: 'none' + }); + return; + } + + this.startPrintJob(() => { + let printerJobs = new PrinterJobs(); + printerJobs + .setAlign('ct') + .print('图片打印') + .bitmap(this.data.imageData.bitmapData, this.data.imageData.width, this.data.imageData.height) + .lineFeed(2); + + return printerJobs.buffer(); + }); + + // 清除预览 + this.setData({ + selectedImage: null, + imageData: null + }); + }, + + /** + * 取消打印图片 + */ + cancelPrintImage() { + this.setData({ + selectedImage: null, + imageData: null + }); + }, + onLoad(options) { const lastDevice = wx.getStorageSync(LAST_CONNECTED_DEVICE); this.setData({ diff --git a/pages/index/index.wxml b/pages/index/index.wxml index 4ebed7a..b20232e 100644 --- a/pages/index/index.wxml +++ b/pages/index/index.wxml @@ -38,11 +38,42 @@ 特性值: {{item.value}} + + + + 打印状态 + + + {{printStatusText}} + + + + + + + + + + 图片预览 + + + + + + + + + + \ No newline at end of file diff --git a/pages/index/index.wxss b/pages/index/index.wxss index 7369601..58202e2 100644 --- a/pages/index/index.wxss +++ b/pages/index/index.wxss @@ -52,4 +52,62 @@ min-height: 2.58823529em; line-height: 2.58823529em; padding: 10rpx; +} + +/* 打印状态管理样式 */ +.print-status { + width: 100%; + padding: 20rpx; + margin: 20rpx 0; + background-color: #f5f5f5; + border-radius: 10rpx; +} + +.status-title { + font-size: 32rpx; + font-weight: bold; + margin-bottom: 20rpx; + text-align: center; +} + +.progress-container { + display: flex; + flex-direction: column; + align-items: center; +} + +.progress-text { + margin-top: 10rpx; + font-size: 28rpx; + color: #666; +} + +/* 图片预览样式 */ +.image-preview { + display: flex; + flex-direction: column; + align-items: center; + padding: 20rpx; + background-color: #f9f9f9; + border-radius: 10rpx; + margin: 20rpx 0; +} + +.preview-title { + font-size: 32rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.preview-image { + width: 300rpx; + height: 300rpx; + border: 2rpx solid #ddd; + border-radius: 10rpx; + margin-bottom: 20rpx; +} + +.preview-buttons { + display: flex; + gap: 20rpx; } \ No newline at end of file diff --git a/printer/commands.js b/printer/commands.js index 3401e76..a134408 100644 --- a/printer/commands.js +++ b/printer/commands.js @@ -186,6 +186,115 @@ _.COLOR = { 1: [0x1b, 0x72, 0x01] // red }; +/** + * [QR_CODE QR Code commands] + * ESC/POS二维码打印指令集 + * 基于ESC/POS标准规范实现 + * + * 指令格式说明: + * GS ( k <功能代码> <数据长度> <数据> + * 其中:GS = 0x1D, ( = 0x28, k = 0x6B + * + * @type {Object} + */ +_.QR_CODE = { + /** + * 设置二维码模块大小 + * 设置QR码中单个模块的像素大小 + * + * @param {number} size - 模块大小,范围1-16,默认推荐6 + * @returns {Array} ESC/POS指令数组 + * + * 指令格式:GS ( k 3 0 49 67 n + * 十六进制:1D 28 6B 03 00 31 43 n + * + * 示例: + * QR_MODULE_SIZE(6) -> [0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x43, 0x06] + */ + QR_MODULE_SIZE: function(size) { + // 参数验证 + if (size < 1 || size > 16) { + console.warn(`二维码模块大小 ${size} 超出范围[1-16],使用默认值6`); + size = 6; + } + return [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, size]; + }, + + /** + * 设置二维码错误纠正等级 + * 设置QR码的错误纠正能力级别 + * + * @param {number} level - 错误纠正等级:48=L(7%), 49=M(15%), 50=Q(25%), 51=H(30%) + * @returns {Array} ESC/POS指令数组 + * + * 指令格式:GS ( k 3 0 49 69 n + * 十六进制:1D 28 6B 03 00 31 45 n + * + * 示例: + * QR_ERROR_LEVEL(49) -> [0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x45, 0x31] + */ + QR_ERROR_LEVEL: function(level) { + // 参数验证 + const validLevels = [48, 49, 50, 51]; // L, M, Q, H + if (!validLevels.includes(level)) { + console.warn(`二维码错误纠正等级 ${level} 无效,使用默认值49(M)`); + level = 49; + } + return [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, level]; + }, + + /** + * 存储二维码数据到打印机缓存 + * 将要打印的QR码数据发送到打印机内存 + * + * @param {string} data - 要编码的字符串数据 + * @returns {Array} ESC/POS指令数组 + * + * 指令格式:GS ( k pL pH 49 80 48 d1...dk + * 十六进制:1D 28 6B pL pH 31 50 30 数据... + * + * 数据长度计算:pL = (数据长度+3) & 0xFF, pH = (数据长度+3) >> 8 + * + * 示例:存储"ABC" + * QR_STORE_DATA("ABC") -> [0x1D, 0x28, 0x6B, 0x06, 0x00, 0x31, 0x50, 0x30, 0x41, 0x42, 0x43] + */ + QR_STORE_DATA: function(data) { + const length = data.length + 3; // 包括功能代码(3字节) + const lowByte = length & 0xFF; // 低8位 + const highByte = (length >> 8) & 0xFF; // 高8位 + + // 数据长度限制检查(通常不超过7084字节) + if (length > 7084) { + console.warn(`二维码数据长度 ${length} 可能超出打印机限制`); + } + + // 构建完整指令:头部 + 数据 + const header = [0x1d, 0x28, 0x6b, lowByte, highByte, 0x31, 0x50, 0x30]; + const dataBytes = Array.from(data).map(char => char.charCodeAt(0)); + + console.log(`二维码数据存储: "${data}" (${data.length}字符) -> 指令长度:${length}`); + console.log(`数据十六进制: ${dataBytes.map(b => '0x' + b.toString(16).toUpperCase()).join(' ')}`); + + return header.concat(dataBytes); + }, + + /** + * 打印已存储的二维码 + * 执行QR码打印操作 + * + * @returns {Array} ESC/POS指令数组 + * + * 指令格式:GS ( k 3 0 49 81 48 + * 十六进制:1D 28 6B 03 00 31 51 30 + * + * 注意:执行此指令前必须先设置模块大小、错误纠正等级并存储数据 + * + * 示例: + * QR_PRINT -> [0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30] + */ + QR_PRINT: [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30] +}; + /** * [exports description] * @type {[type]} diff --git a/printer/printerjobs.js b/printer/printerjobs.js index fda31bb..63b091c 100644 --- a/printer/printerjobs.js +++ b/printer/printerjobs.js @@ -144,6 +144,126 @@ printerJobs.prototype.beep = function (n, t) { return this; }; +/** + * 打印二维码(QR Code) + * + * ESC/POS二维码打印完整流程: + * 1. 设置二维码模块大小 + * 2. 设置错误纠正等级 + * 3. 存储二维码数据到打印机缓存 + * 4. 执行二维码打印 + * 5. 换行 + * + * @param {string} content - 要编码到二维码中的文本内容 + * @param {number} moduleSize - 二维码模块大小,范围1-16,默认6 + * 数值越大,二维码尺寸越大 + * 推荐值:4-8(适合58mm打印机) + * @param {string} errorLevel - 错误纠正等级,可选值: + * 'L' - Low (约7%纠错能力) + * 'M' - Medium (约15%纠错能力,默认) + * 'Q' - Quartile (约25%纠错能力) + * 'H' - High (约30%纠错能力) + * + * @returns {printerJobs} 返回this,支持链式调用 + * + * @example + * // 打印默认大小的二维码 + * printerJobs.qrcode('https://github.com/your-repo'); + * + * @example + * // 打印大尺寸、高纠错等级的二维码 + * printerJobs.qrcode('Hello World', 8, 'H'); + * + * @example + * // 链式调用 + * printerJobs + * .setAlign('ct') + * .print('扫码关注') + * .qrcode('https://your-website.com', 6, 'M') + * .lineFeed(2); + */ +printerJobs.prototype.qrcode = function (content, moduleSize = 6, errorLevel = 'M') { + // 参数验证和处理 + if (!content || typeof content !== 'string') { + console.error('二维码内容不能为空且必须是字符串'); + return this; + } + + // 错误纠正等级映射:字符到ESC/POS代码 + const errorLevelMap = { + 'L': 48, // 7%纠错能力 + 'M': 49, // 15%纠错能力(推荐值) + 'Q': 50, // 25%纠错能力 + 'H': 51 // 30%纠错能力 + }; + + // 获取错误纠正等级代码,默认为M(中等) + const errorLevelCode = errorLevelMap[errorLevel.toUpperCase()] || 49; + + // 内容长度检查(ESC/POS通常限制在7084字节以内) + if (content.length > 2000) { + console.warn(`二维码内容长度 ${content.length} 超过推荐值,可能导致打印失败`); + } + + console.log('=== ESC/POS二维码打印参数 ==='); + console.log(`内容: "${content}"`); + console.log(`内容长度: ${content.length} 字符`); + console.log(`模块大小: ${moduleSize}`); + console.log(`错误纠正等级: ${errorLevel} (代码: ${errorLevelCode})`); + + // ESC/POS二维码打印标准流程 + console.log('=== 开始生成二维码ESC/POS指令 ==='); + + // 步骤1:设置二维码模块大小 + console.log('步骤1:设置模块大小'); + const sizeCmd = commands.QR_CODE.QR_MODULE_SIZE(moduleSize); + console.log(`模块大小指令: ${sizeCmd.map(b => '0x' + b.toString(16).toUpperCase().padStart(2, '0')).join(' ')}`); + this._enqueue(sizeCmd); + + // 步骤2:设置错误纠正等级 + console.log('步骤2:设置错误纠正等级'); + const levelCmd = commands.QR_CODE.QR_ERROR_LEVEL(errorLevelCode); + console.log(`纠错等级指令: ${levelCmd.map(b => '0x' + b.toString(16).toUpperCase().padStart(2, '0')).join(' ')}`); + this._enqueue(levelCmd); + + // 步骤3:存储二维码数据到打印机缓存 + console.log('步骤3:存储二维码数据'); + const dataCmd = commands.QR_CODE.QR_STORE_DATA(content); + console.log(`数据存储指令长度: ${dataCmd.length} 字节`); + console.log(`数据存储指令: ${dataCmd.slice(0, 8).map(b => '0x' + b.toString(16).toUpperCase().padStart(2, '0')).join(' ')}...`); + this._enqueue(dataCmd); + + // 步骤4:执行二维码打印 + console.log('步骤4:执行二维码打印'); + const printCmd = commands.QR_CODE.QR_PRINT; + console.log(`打印指令: ${printCmd.map(b => '0x' + b.toString(16).toUpperCase().padStart(2, '0')).join(' ')}`); + this._enqueue(printCmd); + + // 步骤5:换行,确保后续内容位置正确 + console.log('步骤5:添加换行'); + console.log(`换行指令: 0x${commands.LF[0].toString(16).toUpperCase()}`); + this._enqueue(commands.LF); + + console.log('=== 二维码ESC/POS指令生成完成 ==='); + + return this; +}; + +/** + * 打印位图 + * @param {Uint8Array} bitmapData 位图数据 + * @param {number} width 图片宽度(像素) + * @param {number} height 图片高度(像素) + */ +printerJobs.prototype.bitmap = function (bitmapData, width, height) { + const imageUtil = require('../utils/imageutil'); + const commands = imageUtil.bitmapToEscPos(bitmapData, width, height); + this._enqueue(commands); + // 换行 + this._enqueue(commands.LF); + return this; +}; + /** * 清空任务 */ diff --git a/utils/imageutil.js b/utils/imageutil.js new file mode 100644 index 0000000..4cbdc70 --- /dev/null +++ b/utils/imageutil.js @@ -0,0 +1,193 @@ +/** + * 图片处理工具类 + * 用于将图片转换为打印机可识别的位图指令 + */ + +/** + * 将图片转换为位图数据 + * @param {string} imagePath 图片路径 + * @param {number} maxWidth 最大宽度(像素) + * @param {number} maxHeight 最大高度(像素) + * @returns {Promise} 返回包含位图数据和尺寸信息的对象 + */ +function convertImageToBitmap(imagePath, maxWidth = 384, maxHeight = 800) { + return new Promise((resolve, reject) => { + wx.getImageInfo({ + src: imagePath, + success: (res) => { + const { width, height } = res; + + // 计算缩放比例 + let scale = 1; + if (width > maxWidth) { + scale = maxWidth / width; + } + if (height * scale > maxHeight) { + scale = maxHeight / height; + } + + const canvasWidth = Math.floor(width * scale); + const canvasHeight = Math.floor(height * scale); + + // 创建canvas + const query = wx.createSelectorQuery(); + query.select('#imageCanvas') + .fields({ node: true, size: true }) + .exec((canvasRes) => { + if (!canvasRes[0] || !canvasRes[0].node) { + reject(new Error('无法获取canvas节点')); + return; + } + + const canvas = canvasRes[0].node; + const ctx = canvas.getContext('2d'); + + // 设置canvas尺寸 + canvas.width = canvasWidth; + canvas.height = canvasHeight; + + // 绘制图片 + const img = canvas.createImage(); + img.onload = () => { + ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); + + try { + // 获取图像数据 + const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight); + const bitmapData = convertToBitmapData(imageData); + + resolve({ + bitmapData: bitmapData, + width: canvasWidth, + height: canvasHeight, + bytesPerRow: Math.ceil(canvasWidth / 8) + }); + } catch (error) { + reject(error); + } + }; + + img.onerror = () => { + reject(new Error('图片加载失败')); + }; + + img.src = imagePath; + }); + }, + fail: (error) => { + reject(error); + } + }); + }); +} + +/** + * 将图像数据转换为位图数据(1位单色) + * @param {ImageData} imageData 图像数据 + * @returns {Uint8Array} 位图数据 + */ +function convertToBitmapData(imageData) { + const { data, width, height } = imageData; + const bytesPerRow = Math.ceil(width / 8); + const bitmapData = new Uint8Array(bytesPerRow * height); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x += 8) { + let byte = 0; + + for (let bit = 0; bit < 8; bit++) { + if (x + bit < width) { + const index = ((y * width) + (x + bit)) * 4; + const r = data[index]; + const g = data[index + 1]; + const b = data[index + 2]; + + // 转换为灰度值 + const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b); + + // 阈值处理(128为阈值) + if (gray < 128) { + byte |= (1 << (7 - bit)); // 黑色像素 + } + } + } + + bitmapData[y * bytesPerRow + Math.floor(x / 8)] = byte; + } + } + + return bitmapData; +} + +/** + * 将位图数据转换为ESC/POS指令 + * @param {Uint8Array} bitmapData 位图数据 + * @param {number} width 图片宽度(像素) + * @param {number} height 图片高度(像素) + * @returns {Array} ESC/POS指令数组 + */ +function bitmapToEscPos(bitmapData, width, height) { + const bytesPerRow = Math.ceil(width / 8); + const commands = []; + + // GS v 0 命令:打印光栅位图 + // 格式:GS v 0 m xL xH yL yH d1...dk + // m = 0:正常模式,xL xH:水平方向字节数,yL yH:垂直方向点数 + + for (let y = 0; y < height; y++) { + const rowStart = y * bytesPerRow; + const rowData = bitmapData.slice(rowStart, rowStart + bytesPerRow); + + // 每行的ESC/POS命令 + const xL = bytesPerRow & 0xFF; + const xH = (bytesPerRow >> 8) & 0xFF; + const yL = 1 & 0xFF; // 每次打印1行 + const yH = (1 >> 8) & 0xFF; + + commands.push(0x1d, 0x76, 0x30, 0x00, xL, xH, yL, yH); + commands.push(...rowData); + } + + return commands; +} + +/** + * 选择图片并转换 + * @returns {Promise} 返回处理后的图片信息 + */ +function chooseAndProcessImage() { + return new Promise((resolve, reject) => { + wx.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: (res) => { + const imagePath = res.tempFilePaths[0]; + + convertImageToBitmap(imagePath) + .then(result => { + resolve({ + imagePath: imagePath, + bitmapData: result.bitmapData, + width: result.width, + height: result.height, + bytesPerRow: result.bytesPerRow + }); + }) + .catch(error => { + reject(error); + }); + }, + fail: (error) => { + reject(error); + } + }); + }); +} + +module.exports = { + convertImageToBitmap, + convertToBitmapData, + bitmapToEscPos, + chooseAndProcessImage +}; \ No newline at end of file