├── docs ├── .gitignore ├── memobird.png ├── serve.js └── index.html ├── .gitignore ├── src ├── index.ts ├── date.ts ├── image.ts └── memobird.ts ├── jest.config.js ├── tsconfig.json ├── package.json ├── rollup.config.mjs └── README.md /docs/.gitignore: -------------------------------------------------------------------------------- 1 | account.json 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /docs/memobird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sherluok/memobird/HEAD/docs/memobird.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Memobird as default, 3 | type MemobirdInit, 4 | type PrintContentId, 5 | type PrintFlag 6 | } from './memobird'; 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.[tj]sx?$': ['@swc/jest', { 4 | jsc: { 5 | parser: { 6 | syntax: 'typescript', 7 | tsx: true, 8 | decorators: true, 9 | dynamicImport: true 10 | }, 11 | transform: { 12 | legacyDecorator: false, 13 | decoratorMetadata: false, 14 | decoratorVersion: '2022-03', 15 | }, 16 | }, 17 | }], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/date.ts: -------------------------------------------------------------------------------- 1 | export function timestamp(date = new Date(), format = 'YYYY-MM-DD HH:mm:ss'): string { 2 | const Y = date.getFullYear(); 3 | const M = date.getMonth() + 1; 4 | const D = date.getDate(); 5 | const H = date.getHours(); 6 | const m = date.getMinutes(); 7 | const s = date.getSeconds(); 8 | 9 | return format 10 | .replace('YYYY', Y.toString()) 11 | .replace('MM', M.toString().padStart(2, '0')) 12 | .replace('DD', D.toString().padStart(2, '0')) 13 | .replace('HH', H.toString().padStart(2, '0')) 14 | .replace('mm', m.toString().padStart(2, '0')) 15 | .replace('ss', s.toString().padStart(2, '0')) 16 | ; 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src"], 3 | "exclude": ["./dist", "node_modules"], 4 | "compilerOptions": { 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "lib": ["ESNext", "WebWorker"], 8 | "target": "ES2020", 9 | "module": "ESNext", 10 | "strict": true, 11 | "pretty": true, 12 | "noEmit": true, 13 | "allowJs": true, 14 | "checkJs": false, 15 | "sourceMap": true, 16 | "composite": false, 17 | "incremental": false, 18 | "declaration": true, 19 | "skipLibCheck": true, 20 | "noEmitOnError": true, 21 | "declarationMap": true, 22 | "isolatedModules": true, 23 | "esModuleInterop": true, 24 | "moduleDetection": "auto", 25 | "moduleResolution": "Bundler", 26 | "strictNullChecks": true, 27 | "preserveSymlinks": false, 28 | "resolveJsonModule": true, 29 | "downlevelIteration": true, 30 | "allowSyntheticDefaultImports": true, 31 | "forceConsistentCasingInFileNames": true, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memobird", 3 | "version": "1.0.1", 4 | "main": "dist/index.cjs", 5 | "module": "dist/index.mjs", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "doc": "node ./docs/serve.js", 9 | "test": "jest --config jest.config.js --rootDir .", 10 | "watch": "rollup --config rollup.config.mjs --watch", 11 | "build": "rollup --config rollup.config.mjs" 12 | }, 13 | "dependencies": { 14 | "binary-bmp": "workspace:^", 15 | "iconv-lite": "^0.6.3", 16 | "sharp": "^0.33.2" 17 | }, 18 | "devDependencies": { 19 | "@jest/globals": "^29.7.0", 20 | "@rollup/plugin-commonjs": "^25.0.7", 21 | "@rollup/plugin-node-resolve": "^15.2.3", 22 | "@rollup/plugin-terser": "^0.4.4", 23 | "@rollup/plugin-typescript": "^11.1.6", 24 | "@swc/jest": "^0.2.36", 25 | "@types/node": "^20.11.22", 26 | "canvas": "^2.11.2", 27 | "jest": "^29.7.0", 28 | "rollup": "^4.12.0", 29 | "rollup-plugin-polyfill-node": "^0.13.0", 30 | "tslib": "^2.6.2", 31 | "typescript": "^5.3.3" 32 | }, 33 | "publishConfig": { 34 | "access": "public", 35 | "registry": "https://registry.npmjs.org", 36 | "directory": "dist" 37 | }, 38 | "author": "sherluok", 39 | "bugs": { 40 | "url": "https://github.com/sherluok/memobird/issues" 41 | }, 42 | "homepage": "https://github.com/sherluok/memobird#readme", 43 | "license": "MIT", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/sherluok/memobird.git" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/image.ts: -------------------------------------------------------------------------------- 1 | import { Bitmap, Bits } from 'binary-bmp'; 2 | import { readFile } from 'node:fs/promises'; 3 | import { default as createSharp } from 'sharp'; 4 | 5 | export function fetchImage(src: string): Buffer | Promise { 6 | if (/^https?\:\/\//.test(src)) { 7 | return fetch(src).then((res) => res.arrayBuffer()).then((arrayBuffer) => Buffer.from(arrayBuffer)); 8 | } else if (/^data:image\/\w+;base64,/.test(src)) { 9 | const base64Data = src.replace(/^data:image\/\w+;base64,/, ''); 10 | return Buffer.from(base64Data, 'base64'); 11 | } else { 12 | return readFile(src); 13 | } 14 | } 15 | 16 | export async function sharpImage(input: Buffer, resizeToWidth: number): Promise { 17 | const realResizeWidth = resizeToWidth <= 0 ? 384 : resizeToWidth; 18 | const sharp = createSharp(input); 19 | const raw = await sharp 20 | .ensureAlpha() 21 | .flatten({ background: "#ffffff" }) 22 | .resize({ width: realResizeWidth }) 23 | .flip() 24 | .raw() 25 | .toBuffer({ resolveWithObject: true }) 26 | ; 27 | 28 | const { info: { width, height, channels }, data } = raw; 29 | 30 | if (channels === 3 && data.byteLength === width * height * 3) { 31 | return new Bitmap({ bits: Bits.RGB, width, height, data }).bits(Bits.BINARY).uint8Array(); 32 | } 33 | 34 | if (channels === 4 && data.byteLength === width * height * 4) { 35 | return new Bitmap({ bits: Bits.RGBA, width, height, data }).bits(Bits.BINARY).uint8Array(); 36 | } 37 | 38 | throw new Error('Cannot convert input image into RGBA/RGB channels!'); 39 | } 40 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjsPlugin from '@rollup/plugin-commonjs'; 2 | import nodeResolvePlugin from '@rollup/plugin-node-resolve'; 3 | import terserPlugin from '@rollup/plugin-terser'; 4 | import typescriptPlugin from '@rollup/plugin-typescript'; 5 | import { rm } from 'fs/promises'; 6 | import packageJson from './package.json' with { type: 'json' }; 7 | 8 | export default { 9 | input: 'src/index.ts', 10 | output: [{ 11 | file: 'dist/index.cjs', 12 | sourcemap: true, 13 | exports: 'default', 14 | format: 'cjs', 15 | }, { 16 | file: 'dist/index.mjs', 17 | sourcemap: true, 18 | format: 'es', 19 | }], 20 | external: [ 21 | ...Object.keys(packageJson.dependencies ?? {}), 22 | ...Object.keys(packageJson.devDependencies ?? {}), 23 | ], 24 | plugins:[ 25 | commonjsPlugin(), 26 | nodeResolvePlugin(), 27 | typescriptPlugin(), 28 | terserPlugin(), 29 | { 30 | name: 'publish-config-plugin', 31 | async buildStart() { 32 | if (!this.meta.watchMode) { 33 | await rm(new URL('./dist', import.meta.url), { 34 | force: true, 35 | recursive: true, 36 | }); 37 | } 38 | }, 39 | async buildEnd() { 40 | const { ...pkg } = packageJson; 41 | delete pkg.scripts; 42 | delete pkg.devDependencies; 43 | delete pkg.publishConfig.directory; 44 | pkg.main = 'index.cjs'; 45 | pkg.module = 'index.mjs'; 46 | pkg.types = 'index.d.ts'; 47 | this.emitFile({ 48 | type: 'asset', 49 | fileName: 'package.json', 50 | source: JSON.stringify(pkg, null, 2), 51 | }); 52 | }, 53 | } 54 | ], 55 | }; 56 | -------------------------------------------------------------------------------- /docs/serve.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 在同级文件夹中创建 `account.json` 文件并写入: 3 | * ```json 4 | * { 5 | * "ak": "xxxxxxxxxxxxxxxxx", 6 | * "memobirdID": "xxxxxxxxx", 7 | * "useridentifying": "xxxx" 8 | * } 9 | * ``` 10 | * 然后执行以下命令: 11 | * ```sh 12 | * node ./docs/serve.js 13 | * ``` 14 | */ 15 | 16 | const { createReadStream, readFileSync } = require('node:fs'); 17 | const { createServer } = require('node:http'); 18 | const { join } = require('node:path'); 19 | 20 | const Memobird = require('..'); 21 | 22 | const account = JSON.parse(readFileSync(join(__dirname, 'account.json'), 'utf-8')); 23 | 24 | const memobird = new Memobird(account); 25 | 26 | createServer(async (req, res) => { 27 | switch (req.url) { 28 | case '/': 29 | case '/index.html': { 30 | createReadStream(join(__dirname, 'index.html')).pipe(res); 31 | break; 32 | } 33 | case '/print-binary-bmp': { 34 | const base64 = await new Promise((resolve, reject) => { 35 | const chunks = []; 36 | req.on('data', (chunk) => chunks.push(chunk)); 37 | req.once('error', (error) => reject(error)); 38 | req.once('end', () => { 39 | resolve(Buffer.concat(chunks).toString('base64')); 40 | }); 41 | }); 42 | 43 | const printContentId = await memobird.print(`P:${base64}`); 44 | const printFlag = await memobird.watch(printContentId, 3000, 9000); 45 | res.write(JSON.stringify({ printContentId, printFlag })); 46 | res.end(); 47 | break; 48 | } 49 | default: { 50 | res.writeHead(404); 51 | res.end(); 52 | break; 53 | } 54 | } 55 | }).listen(3456, () => { 56 | console.log('Canvas Example Server Running at http://localhost:3456'); 57 | }); 58 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Canvas Example 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./docs/memobird.png) 2 | 3 | **memobird** 是一个咕咕机 Node.js SDK,基于[咕咕机开放平台](https://open.memobird.cn/)。 4 | 5 | - [安装](#安装) 6 | - 打印 7 | - [打印文本](#打印文本) 8 | - [打印图片](#打印图片) 9 | - [打印网页](#打印网页) 10 | - [打印 Canvas](#打印-canvas) 11 | - [获取打印状态](#获取打印状态) 12 | - 示例 13 | - [`./docs/serve.js`](./docs/serve.js) 14 | 15 | ## 更新日志 16 | 17 | **`V1.0.0 2024-03-01`** 18 | 19 | - 支持 Typescript 20 | - 调用的咕咕机开放平台接口切换至 https 协议 21 | - 图片本地打印切换至 sharp 库 22 | - 减小编译产物体积 23 | 24 | ### 安装 25 | 26 | ```sh 27 | npm install memobird 28 | ``` 29 | 30 | ### 初始化 31 | 32 | ```typescript 33 | import Memobird from 'memobird'; 34 | 35 | const memobird = new Memobird({ 36 | ak: 'xxxxxxxxxxxxxxxx', // 第三方应用软件签名 37 | memobirdID: 'xxxxxxxx', // 咕咕机的设备编号 38 | useridentifying: 'xxx', // 用户唯一标识符 39 | }); 40 | ``` 41 | 42 | ### 打印文本 43 | 44 | **`printText(text: string): Promise`** 45 | 46 | ```typescript 47 | await memobird.printText('你好咕咕机'); 48 | ``` 49 | 50 | ### 打印图片 51 | 52 | 由于咕咕机要求的图片数据必须是单色位图,所以打印 `jpeg` 或者 `png` 等格式的图片需要使用到图片转码的功能。本模块提供了本地转码和在线转码两种方式。 53 | 54 | **`printImage(image: string, width?: number): Promise`** 55 | 56 | - `image` 57 | - 本地图片的文件路径:例如 `"./examples/jpeg420exif.jpg"` 58 | - 网络图片的 URL 地址:例如 `"https://open.memobird.cn/images/logo.png"` 59 | - 图片数据的 Base64 编码:例如 `"data:image/jpeg;base64,..."` 60 | - `width` 61 | - 缺省:使用[在线转码](#在线转码图片) 62 | - 提供:使用[本地转码](#本地转码图片) 63 | 64 | ```typescript 65 | await memobird.printImage('./examples/local-image.png', 0); 66 | await memobird.printImage('https://assets-cdn.github.com/images/modules/logos_page/GitHub-Mark.png', 150); 67 | await memobird.printImage('data:image/jpeg;base64,...'); 68 | ``` 69 | 70 | #### 在线转码图片 71 | 72 | 缺省 `width` 参数时,内部通过咕咕机官方提供的["获取单色位图"]((https://open.memobird.cn/upload/webapi.pdf))接口进行转码。该接口只能处理 `jpeg` 和 `png` 格式的图片,且不能调整图片宽度。 73 | 74 | ```typescript 75 | await memobird.printImage('./examples/local-image.png'); 76 | ``` 77 | 78 | #### 本地转码图片 79 | 80 | 提供 `width` 参数时,内部通过 [sharp](https://github.com/lovell/sharp) 库实现。本地转码支持更多的图片格式,并且能够调整图片的打印宽度。传入的 `width` 小于等于 `0` 时,图片缩放至宽度 `384`(咕咕机最大内容宽度)。 81 | 82 | ```typescript 83 | await memobird.printImage('./examples/local-image.png', 0); 84 | ``` 85 | 86 | ### 打印网页 87 | 88 | 提供 URL 地址或 HTML 源码打印网页式使用了咕咕机官方提供的接口,这些接口存在一些限制,以下情况可能导致打印错误: 89 | 1. 用 AJAX 渲染的页面; 90 | 2. 网页内容数据过大; 91 | 3. 页面加载太慢; 92 | 4. 如果有图片没有采用完整路径; 93 | 5. CSS 没有写在 HTML 页面上。 94 | 95 | #### 通过 URL 地址打印网页 96 | 97 | **`printUrl(url: string | URL): Promise`** 98 | 99 | ```typescript 100 | await memobird.printUrl('http://open.memobird.cn/Home/testview'); 101 | ``` 102 | 103 | #### 通过 HTML 源代码打印网页 104 | 105 | **`printHtml(code: string): Promise`** 106 | 107 | ```typescript 108 | await memobird.printHtml(`

Hello World!

`); 109 | ``` 110 | 111 | ### 打印 Canvas 112 | 113 | 很多场景下,我们需要打印更丰富的内容。可以使用 [canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) 精确控制自己想呈现的内容,并利用我编写的模块 [binary-bmp](https://github.com/sherluok/binary-bmp) 将 canvas 对象处理成单色点位图 Base64 编码值。得到编码值以后,然后传入 `print` 方法打印。canvas 对象可以是浏览器中的 [`HTMLCanvasElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement)、[`OffscreenCanvas`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) 或者服务端的 [node-canvas](https://github.com/Automattic/node-canvas) 库创建的对象。参考 [`./examples/canvas/server.js`](./examples/canvas/server.js)。 114 | 115 | 浏览器中: 116 | 117 | ```typescript 118 | import { Bits, Bitmap } from 'binary-bmp'; 119 | 120 | const canvas = document.getElementById('canvas-id'); // 获取 canvas 对象 121 | const uint8array = Bitmap.fromCanvas(canvas).flip().bits(Bits.BINARY); // 读取 canvas 数据,垂直镜像,然后转为单色点位图 122 | 123 | // ... 124 | // 省略代码:将 Base64 编码的单色点位图传给服务器 125 | // ... 126 | ``` 127 | 128 | 服务器中: 129 | 130 | ```typescript 131 | // ... 132 | // 省略代码:接收客户端发来的 Base64 字符串 133 | // ... 134 | 135 | const base64 = Buffer.from(uint8array).toString('base64'); 136 | await memobird.print(`P:${base64}`); 137 | ``` 138 | 139 | ### 一次打印多个 140 | 141 | 咕咕机每次打印一份内容都会留出上下间距,如果你不想看到这些上下间距可以选择一次性打印多个内容。 142 | 143 | **`print(...items: (string | Promise)[]): Promise`** 144 | - `item`: 要打印的内容;是以 `encode` 开头命名的方法的返回值,详见[编码方法](#编码方法)。 145 | 146 | ```typescript 147 | await memobird.print( 148 | memobird.encodeText('你好咕咕机,能不能一次性打印所有东西?'), 149 | memobird.encodeImage('./docs/memobird.png', 100), 150 | ); 151 | ``` 152 | 153 | ### 获取打印状态 154 | 155 | #### 延时获取打印状态 156 | 157 | **`glance(printContentId: PrintContentId, delayMs?: number): Promise`** 158 | - `printContentId`:打印内容 ID;由所有以 `print` 开头命名的方法返回。 159 | - `delayMs`:延时;即等待多少毫秒后再执行获取打印状态,默认 `1000` 毫秒。 160 | 161 | ```typescript 162 | const contentId = await memobird.printText('Hello'); 163 | const printFlag = await memobird.glance(contentId); 164 | console.log(printFlag === 1 ? '已打印' : '打印中...'); 165 | ``` 166 | 167 | #### 持续监听打印状态 168 | 169 | **`watch(printContentId: PrintContentId, intervalMs: number, maxTotalMs: number): Promise`** 170 | - `printContentId`:打印内容 ID;由所有以 `print` 开头命名的方法返回。 171 | - `intervalMs`:监听周期;默认 `3000` 毫秒。 172 | - `maxTotalMs`:监听超时;即超出多少毫秒后立即终止监听,默认 `15000` 毫秒。 173 | 174 | 当打印时间难以预测时,`watch` 方法可以隔一段时间获取一次打印状态,只有当打印状态为已完成或监听时间超时时才终止。 175 | 176 | ```typescript 177 | const contentId = await memobird.printImage('https://open.memobird.cn/images/logo.png'); 178 | const printFlag = await memobird.watch(contentId, 3000, 15000); 179 | console.log(printFlag === 1 ? '已打印' : '打印中...'); 180 | ``` 181 | 182 | ### 编码方法 183 | 184 | 方法签名|作用|返回的字符串结构 185 | --|--|-- 186 | **`encodeText(text: string): string`** | 编码文本 | `T:{Base64-GBK}` 187 | **`encodeCanvas(canvas: CanvasLike): string`** | 编码 Canvas 对象 | `P:{Base64-BinaryBMP}` 188 | **`encodeImage(image: string, width?: number): Promise`** | 编码图片 | `P:{Base64-BinaryBMP}` 189 | -------------------------------------------------------------------------------- /src/memobird.ts: -------------------------------------------------------------------------------- 1 | import { Bitmap, Bits, CanvasLike } from 'binary-bmp'; 2 | import { encode } from 'iconv-lite'; 3 | import { timestamp } from './date'; 4 | import { fetchImage, sharpImage } from './image'; 5 | 6 | /** 打印内容的唯一 ID */ 7 | export type PrintContentId = number; 8 | /** 打印状态; `1` 为已打印,其他为未打印。 */ 9 | export type PrintFlag = number; 10 | 11 | export type Text = `T:${string}`; 12 | export type Image = `P:${string}`; 13 | export type Printable = Text | Image; 14 | 15 | export interface MemobirdInit { 16 | /** 第三方应用软件签名 */ 17 | ak: string; 18 | /** 咕咕机的设备编号(双击设备吐出来的设备编号) */ 19 | memobirdID: string; 20 | /** 与咕咕平台进行关联的用户唯一标识符(用户自定义字符串) */ 21 | useridentifying: string; 22 | } 23 | 24 | interface ICommonRequest { 25 | /** 第三方应用签名 */ 26 | ak: string; 27 | /** 客户端时间。格式 2014-11-14 14:22:39 */ 28 | timestamp: string; 29 | } 30 | 31 | interface ICommonResponse { 32 | /** 返回标志,1 为成功,其他为失败。 */ 33 | showapi_res_code: number; 34 | /** Ak 错误信息的显示 */ 35 | showapi_res_error: string; 36 | } 37 | 38 | interface ISetUserBindRequest { 39 | /** 咕咕机的设备编号 */ 40 | memobirdID: string; 41 | /** 与咕咕平台进行关联的用户唯一标识符 */ 42 | useridentifying: string; 43 | } 44 | 45 | interface ISetUserBindResponse { 46 | showapi_userid: number; 47 | } 48 | 49 | interface IPrintPaperRequest { 50 | /** 51 | * 文本内容(汉字要 GBK 格式的 Base64)/图片(图片为单色点位图)的 Base64 编码值 52 | * - T:文本 53 | * - P:图片 54 | */ 55 | printcontent: string; 56 | /** 咕咕机的设备编号 */ 57 | memobirdID: string; 58 | /** 账号关联返回的 showapi_userid 值 */ 59 | userID: number; 60 | } 61 | 62 | interface IPrintResponse { 63 | /** 返回标志,1 为已打印,其他为未打印。 */ 64 | result: PrintFlag; 65 | /** 返回打印内容的唯一 ID */ 66 | printcontentid: PrintContentId; 67 | /** 打印设备的编号 */ 68 | smartGuid: string; 69 | } 70 | 71 | interface IPrintPaperResponse extends IPrintResponse { 72 | } 73 | 74 | interface IGetPrintStatusRequest { 75 | /** 打印内容的唯一 ID */ 76 | printcontentid: PrintContentId; 77 | } 78 | 79 | interface IGetPrintStatusResponse { 80 | /** 返回标志,1 为已打印,其他为未打印。 */ 81 | printflag: PrintFlag; 82 | /** 返回打印内容的唯一 ID */ 83 | printcontentid: PrintContentId; 84 | } 85 | 86 | interface IGetSignalBase64PicRequest { 87 | /** Jpg 或者 png 的 base64 的值 */ 88 | imgBase64String: string; 89 | } 90 | 91 | interface IGetSignalBase64PicResponse { 92 | /** 返回处理后的图片 base64 值,直接用于打印接口 */ 93 | result: string; 94 | } 95 | 96 | interface IPrintPaperFromUrlRequest { 97 | /** 98 | * 打印网页的地址。最好为静态页面或者服务器渲染页面,以下情况可能导致打印错误: 99 | * 1. 用 ajax 渲染的页面; 100 | * 2. 网页内容数据过大; 101 | * 3. 页面加载太慢; 102 | * 4. 如果有图片没有采用完整路径;例如 `src="/img/test.png"`; 103 | * 5. css 没有写在 html 页面上。 104 | */ 105 | printUrl: string; 106 | /** 咕咕机的设备编号 */ 107 | memobirdID: string; 108 | /** 账号关联返回的 showapi_userid 值 */ 109 | userID: number; 110 | } 111 | 112 | interface IPrintPaperFromUrlResponse extends IPrintResponse { 113 | } 114 | 115 | interface IPrintPaperFromHtmlRequest { 116 | /** 117 | * 打印 html 源码。备注: 118 | * 1. 如果有图片必须采用完整路径; 119 | * 2. css 必须写在 html 页面上; 120 | * 3. html 源码需要进行 gbk 的 base64 编码,然后进行 URL 编码。 121 | */ 122 | printHtml: string; 123 | /** 咕咕机的设备编号 */ 124 | memobirdID: string; 125 | /** 账号关联返回的 showapi_userid 值 */ 126 | userID: number; 127 | } 128 | 129 | interface IPrintPaperFromHtmlResponse extends IPrintResponse { 130 | } 131 | 132 | export class Memobird { 133 | #init: MemobirdInit; 134 | #userId?: number; 135 | 136 | constructor(init: MemobirdInit) { 137 | this.#init = init; 138 | } 139 | 140 | private post(url: 'https://open.memobird.cn/home/setuserbind', data: ISetUserBindRequest): Promise; 141 | private post(url: 'https://open.memobird.cn/home/getprintstatus', data: IGetPrintStatusRequest): Promise; 142 | private post(url: 'https://open.memobird.cn/home/getSignalBase64Pic', data: IGetSignalBase64PicRequest): Promise; 143 | private post(url: 'https://open.memobird.cn/home/printpaper', data: IPrintPaperRequest): Promise; 144 | private post(url: 'https://open.memobird.cn/home/printpaperFromUrl', data: IPrintPaperFromUrlRequest): Promise; 145 | private post(url: 'https://open.memobird.cn/home/printpaperFromHtml', data: IPrintPaperFromHtmlRequest): Promise; 146 | 147 | private async post(url: string, data: object): Promise { 148 | const res = await fetch(url, { 149 | method: 'POST', 150 | headers: { 151 | 'Content-Type': 'application/json', 152 | }, 153 | body: JSON.stringify({ 154 | ak: this.#init.ak, 155 | timestamp: timestamp(), 156 | ...data, 157 | }), 158 | }); 159 | 160 | const { 161 | showapi_res_code, 162 | showapi_res_error, 163 | ...rest 164 | } = await res.json() as ICommonResponse; 165 | 166 | if (showapi_res_code === 1) { 167 | return rest; 168 | } else { 169 | throw new Error(showapi_res_error, { 170 | cause: showapi_res_code, 171 | }); 172 | } 173 | } 174 | 175 | /** 账号关联 */ 176 | private async getUserId(): Promise { 177 | if (!this.#userId) { 178 | const { showapi_userid } = await this.post('https://open.memobird.cn/home/setuserbind', { 179 | memobirdID: this.#init.memobirdID, 180 | useridentifying: this.#init.useridentifying, 181 | }); 182 | this.#userId = showapi_userid; 183 | } 184 | return this.#userId; 185 | } 186 | 187 | /** @deprecated 该方法已弃用。调用 `print*` 方法时会自动按需执行初始化操作。 */ 188 | async init(): Promise { 189 | console.warn('init() 方法已弃用。调用 `print*` 方法时会自动按需执行初始化操作。'); 190 | await this.getUserId(); 191 | } 192 | 193 | /** 194 | * 获取打印状态。 195 | * @param printContentId 打印内容 ID;由所有以 **print** 开头命名的方法返回。 196 | * @param delayMs 延迟多少毫秒后再执行获取打印状态;默认 `1000`。 197 | * @returns 打印标志;`1` 为已打印,其他为未打印。 198 | */ 199 | async glance(printContentId: PrintContentId, delayMs = 1000): Promise { 200 | await new Promise((resolve) => setTimeout(resolve, delayMs)); 201 | const res = await this.post('https://open.memobird.cn/home/getprintstatus', { 202 | printcontentid: printContentId, 203 | }); 204 | return res.printflag; 205 | } 206 | 207 | /** 208 | * 监听打印状态。当打印时间难以预测时,该方法可以隔一段时间获取一次打印状态,只有当打印状态为已完成或监听时间超时时才终止。 209 | * @param printContentId 打印内容 ID;由所有以 **print** 开头命名的方法返回。 210 | * @param intervalMs 监听周期;默认 `3000`。 211 | * @param maxTotalMs 监听超时;即超出多少毫秒后立即终止监听,默认 `15000`。 212 | * @returns 打印标志;`1` 为已打印,其他为未打印。 213 | */ 214 | async watch(printContentId: PrintContentId, intervalMs = 3000, maxTotalMs = 15000): Promise { 215 | let printFlag = -1; 216 | const interval = Math.max(intervalMs, 1); 217 | for (let i = 0; i < maxTotalMs; i += interval) { 218 | printFlag = await this.glance(printContentId, interval); 219 | if (printFlag === 1) { 220 | break; 221 | } 222 | } 223 | return printFlag; 224 | } 225 | 226 | /** 227 | * 一次打印多个。咕咕机每次打印一份内容都会留出上下间距,如果你不想看到这些上下间距可以选择一次性打印多个内容。 228 | * @param items 要打印的内容;是以 **encode** 开头命名的方法的返回值。 229 | * @returns 打印标志;`1` 为已打印,其他为未打印。 230 | */ 231 | async print(...items: (Printable | Promise)[]): Promise { 232 | const userId = await this.getUserId(); 233 | const content = (await Promise.all(items)).join('|'); 234 | const res = await this.post('https://open.memobird.cn/home/printpaper', { 235 | memobirdID: this.#init.memobirdID, 236 | userID: userId, 237 | printcontent: content, 238 | }); 239 | return res.printcontentid; 240 | } 241 | 242 | /** 243 | * 打印文本。 244 | * @param content 文本内容。 245 | * @returns 打印标志;`1` 为已打印,其他为未打印。 246 | */ 247 | printText(content: string): Promise { 248 | return this.print(this.encodeText(content)); 249 | } 250 | 251 | printImage(src: string, width?: number): Promise { 252 | return this.print(this.encodeImage(src, width)); 253 | } 254 | 255 | printCanvas(canvas: CanvasLike): Promise { 256 | return this.print(this.encodeCanvas(canvas)); 257 | } 258 | 259 | /** 260 | * 通过 URL 地址打印网页。 261 | * @param url 网页的 URL 地址 262 | * @returns 打印标志;`1` 为已打印,其他为未打印。 263 | */ 264 | async printUrl(url: string | URL): Promise { 265 | const userId = await this.getUserId(); 266 | const href = url.toString(); 267 | const res = await this.post('https://open.memobird.cn/home/printpaperFromUrl', { 268 | memobirdID: this.#init.memobirdID, 269 | userID: userId, 270 | printUrl: href, 271 | }); 272 | return res.printcontentid; 273 | } 274 | 275 | /** 276 | * 通过 HTML 源码打印网页。 277 | * @param code HTML 源代码。 278 | * @returns 打印标志;`1` 为已打印,其他为未打印。 279 | */ 280 | async printHtml(code: string): Promise { 281 | const userId = await this.getUserId(); 282 | // @todo 是否需要进一步进行 URL 编码 283 | const base64 = encode(code, 'gbk').toString('base64'); 284 | const res = await this.post('https://open.memobird.cn/home/printpaperFromHtml', { 285 | memobirdID: this.#init.memobirdID, 286 | userID: userId, 287 | printHtml: base64, 288 | }); 289 | return res.printcontentid; 290 | } 291 | 292 | /** 293 | * 编码文本。 294 | * @param content 文本内容。 295 | * @returns 文本内容格式 `T:` 的字符串。 296 | */ 297 | encodeText(content: string): Text { 298 | return `T:${encode(`${content}\n`, 'gbk').toString('base64')}`; 299 | } 300 | 301 | /** 302 | * 编码 Canvas 303 | * @param canvas 符合 {@link CanvasLike} 接口的对象;包括但不限于 [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement)、[OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas)、[node-canvas](https://github.com/Automattic/node-canvas)。 304 | * @returns 图片内容格式 `P:` 的字符串。 305 | * @example sdadsadasdasd 306 | * ```ts 307 | * ctx.save(); 308 | * ctx.translate(0, canvas.height); 309 | * ctx.scale(1, -1); 310 | * 311 | * // 将绘制内容放置在垂直翻转的上下文中 312 | * 313 | * ctx.restore(); 314 | * 315 | * memobird.print(memobird.encodeCanvas(canvas)); 316 | * ``` 317 | */ 318 | encodeCanvas(canvas: CanvasLike): Image { 319 | const bitmap = Bitmap.fromCanvas(canvas).flip().bits(Bits.BINARY); 320 | const base64 = Buffer.from(bitmap.uint8Array()).toString('base64'); 321 | return `P:${base64}`; 322 | } 323 | 324 | /** 325 | * 编码图片。 326 | * @param src 图片数据;可以是 URL 地址、本地文件路径、图片数据的 base64 编码。 327 | * @param width 图片宽度;缺省或等于 `undefined` 时使用在线转码;否则在本地使用 [sharp](https://github.com/lovell/sharp) 缩放并转码(小于等于 `0` 时,缩放至宽度 `384`)。 328 | * @returns 图片内容格式 `P:` 的字符串。 329 | */ 330 | async encodeImage(src: string, width?: number): Promise { 331 | const imgBuffer = await fetchImage(src); 332 | if (width === undefined) { 333 | const imgBase64String = imgBuffer.toString('base64'); 334 | const binaryBmpBase64 = await this.post('https://open.memobird.cn/home/getSignalBase64Pic', { imgBase64String }).then((res) => res.result); 335 | return `P:${binaryBmpBase64}`; 336 | } else { 337 | const binaryBmpBuffer = await sharpImage(imgBuffer, width); 338 | const binaryBmpBase64 = Buffer.from(binaryBmpBuffer).toString('base64'); 339 | return `P:${binaryBmpBase64}`; 340 | } 341 | } 342 | } 343 | --------------------------------------------------------------------------------