@opuu/cat-printer
Version:
SDK to interact with chinese non standard thermal printers (cat printers) over bluetooth
553 lines (552 loc) • 19.3 kB
JavaScript
var C = Object.defineProperty;
var R = (s, t, e) => t in s ? C(s, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : s[t] = e;
var m = (s, t, e) => R(s, typeof t != "symbol" ? t + "" : t, e);
const F = 44848, N = 44592, M = 44545, V = 44546, B = 384, H = 32, U = 24e3, O = 100;
var v = /* @__PURE__ */ ((s) => (s[s.ApplyEnergy = 190] = "ApplyEnergy", s[s.GetDeviceState = 163] = "GetDeviceState", s[s.GetDeviceInfo = 168] = "GetDeviceInfo", s[s.UpdateDevice = 169] = "UpdateDevice", s[s.SetDpi = 164] = "SetDpi", s[s.Lattice = 166] = "Lattice", s[s.Retract = 160] = "Retract", s[s.Feed = 161] = "Feed", s[s.Speed = 189] = "Speed", s[s.Energy = 175] = "Energy", s[s.Bitmap = 162] = "Bitmap", s))(v || {}), x = /* @__PURE__ */ ((s) => (s[s.Transfer = 0] = "Transfer", s[s.Response = 1] = "Response", s))(x || {});
function T(s) {
let t = 0;
for (const e of s) {
t ^= e;
for (let i = 0; i < 8; i++)
t & 128 ? t = t << 1 ^ 7 : t <<= 1;
t &= 255;
}
return t;
}
function E(s, t = 128) {
const e = new Uint8Array(Math.ceil(s.length / 8));
for (let i = 0; i < s.length; i++) {
const r = Math.floor(i / 8), o = i % 8, n = s[i], h = n & 255, a = n >> 8 & 255, c = n >> 16 & 255;
(h + a + c) / 3 < t && (e[r] |= 1 << o);
}
return e;
}
class _ {
/**
* Applies threshold dithering to the image data.
* @param imageData The ImageData object.
* @param options Image options, specifically the brightness threshold.
* @returns The dithered ImageData.
*/
dither(t, e) {
const i = t.data, r = e.brightness !== void 0 ? e.brightness : 128;
for (let o = 0; o < i.length; o += 4) {
const h = (i[o] + i[o + 1] + i[o + 2]) / 3 < r ? 0 : 255;
i[o] = i[o + 1] = i[o + 2] = h;
}
return t;
}
}
class A {
constructor() {
m(this, "thresholdMap", [
[0, 2],
[3, 1]
]);
}
/**
* Applies Bayer dithering to the image data.
* @param imageData The ImageData object.
* @param _options Image options (not directly used in this algorithm).
* @returns The dithered ImageData.
*/
dither(t, e) {
const i = t.data, r = t.width, o = t.height;
for (let n = 0; n < o; n++)
for (let h = 0; h < r; h++) {
const a = (n * r + h) * 4, c = i[a], l = i[a + 1], w = i[a + 2], u = (c + l + w) / 3, y = (this.thresholdMap[n % 2][h % 2] + 1) * (255 / 5), f = u < y ? 0 : 255;
i[a] = i[a + 1] = i[a + 2] = f;
}
return t;
}
}
class D {
/**
* Applies Floyd-Steinberg dithering to the image data.
* @param imageData The ImageData object.
* @param _options Image options (not directly used in this algorithm).
* @returns The dithered ImageData.
*/
dither(t, e) {
const i = t.data, r = t.width, o = t.height;
for (let n = 0; n < o; n++)
for (let h = 0; h < r; h++) {
const a = (n * r + h) * 4, c = i[a], l = i[a + 1], w = i[a + 2], y = (c + l + w) / 3 < 128 ? 0 : 255, f = y, g = y, b = y;
i[a] = f, i[a + 1] = g, i[a + 2] = b;
const S = c - f, d = l - g, p = w - b;
this.distributeError(
i,
r,
h + 1,
n,
S * 7 / 16,
d * 7 / 16,
p * 7 / 16
), this.distributeError(
i,
r,
h - 1,
n + 1,
S * 3 / 16,
d * 3 / 16,
p * 3 / 16
), this.distributeError(
i,
r,
h,
n + 1,
S * 5 / 16,
d * 5 / 16,
p * 5 / 16
), this.distributeError(
i,
r,
h + 1,
n + 1,
S * 1 / 16,
d * 1 / 16,
p * 1 / 16
);
}
return t;
}
distributeError(t, e, i, r, o, n, h) {
if (i >= 0 && i < e && r >= 0 && r < t.length / (e * 4)) {
const a = (r * e + i) * 4;
t[a] = Math.max(0, Math.min(255, t[a] + o)), t[a + 1] = Math.max(0, Math.min(255, t[a + 1] + n)), t[a + 2] = Math.max(0, Math.min(255, t[a + 2] + h));
}
}
}
class I {
dither(t, e) {
const i = t.data, r = t.width, o = t.height, n = 4, h = new Uint8ClampedArray(i.length);
for (let a = 3; a < i.length; a += 4)
h[a] = i[a];
for (let a = 0; a < o; a++)
for (let c = 0; c < r; c++) {
const l = (a * r + c) * 4, w = (i[l] + i[l + 1] + i[l + 2]) / 3, u = Math.floor((255 - w) / 50), y = Math.floor(c / n) * n, f = Math.floor(a / n) * n, g = c - y, b = a - f, d = Math.sqrt(
(g - n / 2) ** 2 + (b - n / 2) ** 2
) < u ? 0 : 255;
h[l] = h[l + 1] = h[l + 2] = d;
}
return new ImageData(h, r, o);
}
}
const G = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
BayerDithering: A,
DotDithering: I,
FloydSteinbergDithering: D,
ThresholdDithering: _
}, Symbol.toStringTag, { value: "Module" }));
class k {
/**
* Creates an instance of the CatPrinter.
* @param options Optional settings for the SDK.
*/
constructor(t = {}) {
m(this, "device", null);
m(this, "server", null);
m(this, "txCharacteristic", null);
m(this, "rxCharacteristic", null);
m(this, "printerState", {
outOfPaper: !1,
coverOpen: !1,
overheat: !1,
lowPower: !1,
paused: !1,
busy: !1
});
m(this, "options");
m(this, "modelName", "");
this.options = {
debug: !1,
speed: 32,
energy: 24e3,
finishFeed: 100,
...t
};
}
/**
* Connect to a Cat Printer device.
* @returns A Promise that resolves with the initial printer state when connected.
* @throws Error if Bluetooth is not available or connection fails.
*/
async connect() {
if (!navigator.bluetooth)
throw new Error("Bluetooth API is not available in this browser");
try {
if (this.device = await navigator.bluetooth.requestDevice({
filters: [{ services: [44848] }],
optionalServices: [44592]
}), this.modelName = this.device.name || "", this.log(`Connected to printer model: ${this.modelName}`), !this.device.gatt)
throw new Error("Bluetooth GATT not available");
this.server = await this.device.gatt.connect();
const t = await this.server.getPrimaryService(44592);
return this.txCharacteristic = await t.getCharacteristic(44545), this.rxCharacteristic = await t.getCharacteristic(44546), await this.rxCharacteristic.startNotifications(), this.rxCharacteristic.addEventListener(
"characteristicvaluechanged",
this.handleNotification.bind(this)
), await this.getDeviceState(), await this.prepare(this.options.speed, this.options.energy), this.printerState;
} catch (t) {
throw console.error("Connection error:", t), t;
}
}
/**
* Disconnect from the printer.
* @returns A Promise that resolves when disconnected.
*/
async disconnect() {
if (this.rxCharacteristic)
try {
await this.rxCharacteristic.stopNotifications();
} catch (t) {
this.log("Error stopping notifications:", t);
}
this.server && this.server.connected && this.server.disconnect(), this.device = null, this.server = null, this.txCharacteristic = null, this.rxCharacteristic = null, this.log("Disconnected from printer");
}
/**
* Get the current printer state.
* @returns The current printer state object.
*/
getState() {
return { ...this.printerState };
}
/**
* Check if the printer is currently connected.
* @returns True if connected, false otherwise.
*/
isConnected() {
var t;
return ((t = this.server) == null ? void 0 : t.connected) === !0 && this.txCharacteristic !== null;
}
/**
* Print text content.
* @param text The text to print.
* @param options Optional text formatting options.
* @returns A Promise that resolves when printing is complete.
* @throws Error if the printer is not connected.
*/
async printText(t, e = {}) {
if (!this.isConnected())
throw new Error("Printer not connected");
const r = { ...{
fontFamily: "sans-serif",
fontSize: 16,
align: "start",
lineSpacing: 8,
rotate: 0,
flipH: !1,
flipV: !1,
brightness: 128,
offset: 0
}, ...e }, o = await this.textToBitmap(t, r);
r.offset && r.offset !== 0 && (await this.setSpeed(8), r.offset > 0 ? await this.feed(r.offset) : await this.retract(-r.offset), await this.setSpeed(this.options.speed)), await this.printBitmap(o);
}
/**
* Print an image from a URL.
* @param imageUrl The URL of the image to print.
* @param options Optional image formatting options.
* @returns A Promise that resolves when printing is complete.
* @throws Error if the printer is not connected or the image fails to load.
*/
async printImage(t, e = {}) {
if (!this.isConnected())
throw new Error("Printer not connected");
const r = { ...{
dither: "bayer",
// Default to Bayer dithering as suggested
rotate: 0,
flipH: !1,
flipV: !1,
brightness: 128,
offset: 0
}, ...e }, o = await this.imageToBitmap(t, r);
r.offset && r.offset !== 0 && (await this.setSpeed(8), r.offset > 0 ? await this.feed(r.offset) : await this.retract(-r.offset), await this.setSpeed(this.options.speed)), await this.printBitmap(o);
}
/**
* Print multiple items (text or images) in sequence.
* @param items An array of items to print, each with a type ('text' or 'image'), content, and optional options.
* @returns A Promise that resolves when all items are printed.
* @throws Error if the printer is not connected.
*/
async printMultiple(t) {
if (!this.isConnected())
throw new Error("Printer not connected");
await this.prepare(this.options.speed, this.options.energy);
for (const e of t)
e.type === "text" ? await this.printText(e.content, e.options) : e.type === "image" && await this.printImage(e.content, e.options);
await this.finish(this.options.finishFeed);
}
/**
* Feed paper forward by a specified number of lines.
* @param lines The number of lines to feed.
* @returns A Promise that resolves when the feed command is sent.
*/
async feed(t) {
if (t <= 0) return;
const e = this.makeCommand(
v.Feed,
new Uint8Array([t & 255, t >> 8 & 255])
);
await this.write(e);
}
/**
* Retract paper (reverse feed) by a specified number of lines.
* @param lines The number of lines to retract.
* @returns A Promise that resolves when the retract command is sent.
*/
async retract(t) {
if (t <= 0) return;
const e = this.makeCommand(
v.Retract,
new Uint8Array([t & 255, t >> 8 & 255])
);
await this.write(e);
}
/**
* Set the print speed.
* @param speed The print speed value.
* @returns A Promise that resolves when the speed command is sent.
*/
async setSpeed(t) {
const e = this.makeCommand(v.Speed, new Uint8Array([t]));
await this.write(e);
}
/**
* Set the print energy/density.
* @param energy The energy value.
* @returns A Promise that resolves when the energy command is sent.
*/
async setEnergy(t) {
const e = new Uint8Array([
t & 255,
t >> 8 & 255,
t >> 16 & 255,
t >> 24 & 255
]), i = this.makeCommand(v.Energy, e);
await this.write(i);
}
/**
* Apply the current energy settings to the printer.
* @returns A Promise that resolves when the apply energy command is sent.
*/
async applyEnergy() {
const t = this.makeCommand(v.ApplyEnergy, new Uint8Array([1]));
await this.write(t);
}
/**
* Get the current device state from the printer.
* @returns A Promise that resolves with the current printer state.
*/
async getDeviceState() {
const t = this.makeCommand(
v.GetDeviceState,
new Uint8Array([1])
);
return await this.write(t), this.printerState;
}
/**
* Prepare the printer for printing by setting speed and energy.
* @param speed The print speed.
* @param energy The print energy/density.
* @returns A Promise that resolves when the prepare commands are sent.
*/
async prepare(t, e) {
await this.setSpeed(t), await this.setEnergy(e), await this.applyEnergy();
}
/**
* Finish printing with an optional paper feed.
* @param feed The number of lines to feed at the end of printing. Default is 0.
* @returns A Promise that resolves when the finish commands are sent.
*/
async finish(t = 0) {
t > 0 && (await this.setSpeed(8), await this.feed(t));
}
/**
* Draw a single line of bitmap data to the printer.
* @param line The bitmap line data as a Uint8Array.
* @returns A Promise that resolves when the draw command is sent.
* @throws Error if the printer is not connected.
*/
async draw(t) {
if (!this.isConnected())
throw new Error("Printer not connected");
const e = this.makeCommand(v.Bitmap, t);
await this.write(e);
}
/**
* Print a full bitmap.
* @param bitmap An object containing the width, height, and bitmap data.
* @returns A Promise that resolves when the entire bitmap is printed.
*/
async printBitmap(t) {
const { width: e, height: i, data: r } = t, o = Math.ceil(e / 8);
for (let n = 0; n < i; n++) {
const h = n * o, a = h + o, c = r.slice(h, a);
c.every((l) => l === 0) || await this.draw(c);
}
}
// Private methods
/**
* Create a command packet to send to the printer.
* @param command The command to send.
* @param payload The data payload for the command.
* @param type The type of command (Transfer or Response). Default is Transfer.
* @returns The complete command packet as a Uint8Array.
*/
makeCommand(t, e, i = x.Transfer) {
return new Uint8Array([
81,
120,
t,
i,
e.length & 255,
e.length >> 8,
...e,
T(e),
255
]);
}
/**
* Send data to the printer via the TX characteristic.
* @param data The data to send as a Uint8Array.
* @returns A Promise that resolves when the data is written.
* @throws Error if the printer is not properly connected.
*/
async write(t) {
if (!this.txCharacteristic)
throw new Error("Printer not properly connected");
this.log(
"Sending data:",
Array.from(t).map((e) => e.toString(16).padStart(2, "0")).join(" ")
);
try {
await this.txCharacteristic.writeValueWithoutResponse(t);
} catch (e) {
throw this.log("Error writing to printer:", e), e;
}
}
/**
* Handle incoming notifications from the printer via the RX characteristic.
* @param event The characteristicvaluechanged event.
*/
handleNotification(t) {
const e = t.target;
if (!e.value) return;
const i = new Uint8Array(e.value.buffer);
if (this.log(
"Received notification:",
Array.from(i).map((r) => r.toString(16).padStart(2, "0")).join(" ")
), i.length >= 7 && i[2] === v.GetDeviceState) {
const r = i[6];
this.printerState = {
outOfPaper: !!(r & 1),
coverOpen: !!(r & 2),
overheat: !!(r & 4),
lowPower: !!(r & 8),
paused: !!(r & 16),
busy: !!(r & 128)
}, this.log("Printer state updated:", this.printerState);
}
}
/**
* Convert text to a bitmap for printing.
* @param text The text to convert.
* @param options Text formatting options.
* @returns A Promise that resolves with the bitmap data.
*/
async textToBitmap(t, e) {
const i = document.createElement("canvas"), r = i.getContext("2d");
i.width = 384, i.height = 500, r.fillStyle = "white", r.fillRect(0, 0, i.width, i.height), r.fillStyle = "black";
const o = e.fontWeight ? `${e.fontWeight} ` : "";
r.font = `${o}${e.fontSize}px ${e.fontFamily}`, r.textAlign = e.align === "start" ? "left" : e.align === "end" ? "right" : e.align;
const n = t.split(`
`), h = e.fontSize + (e.lineSpacing || 8);
let a = e.fontSize;
for (const f of n) {
const g = e.align === "start" ? 0 : e.align === "end" ? i.width : i.width / 2;
r.fillText(f, g, a), a += h;
}
const c = document.createElement("canvas");
c.width = i.width, c.height = a;
const l = c.getContext("2d");
if (l.drawImage(i, 0, 0), e.rotate !== 0 || e.flipH || e.flipV) {
const f = document.createElement("canvas");
f.width = c.width, f.height = c.height;
const g = f.getContext("2d");
g.save(), g.translate(
f.width / 2,
f.height / 2
), e.rotate && e.rotate !== 0 && g.rotate(e.rotate * Math.PI / 180), e.flipH && g.scale(-1, 1), e.flipV && g.scale(1, -1), g.drawImage(
c,
-c.width / 2,
-c.height / 2
), g.restore(), c.width = f.width, c.height = f.height, l.drawImage(f, 0, 0);
}
const w = l.getImageData(
0,
0,
c.width,
c.height
), u = new Uint32Array(w.data.buffer), y = E(u);
return {
width: c.width,
height: c.height,
data: y
};
}
async imageToBitmap(t, e) {
const i = new Image();
i.crossOrigin = "anonymous";
const o = await new Promise(
(d, p) => {
i.onload = () => d(i), i.onerror = () => p(new Error(`Failed to load image: ${t}`)), i.src = t;
}
), n = document.createElement("canvas"), h = n.getContext("2d"), a = 384, c = o.width / o.height, l = Math.floor(a / c);
if (n.width = a, n.height = l, h.fillStyle = "white", h.fillRect(0, 0, n.width, n.height), h.drawImage(o, 0, 0, a, l), e.rotate !== 0 || e.flipH || e.flipV) {
const d = document.createElement("canvas");
d.width = n.width, d.height = n.height;
const p = d.getContext("2d");
p.save(), p.translate(
d.width / 2,
d.height / 2
), e.rotate && e.rotate !== 0 && p.rotate(e.rotate * Math.PI / 180), e.flipH && p.scale(-1, 1), e.flipV && p.scale(1, -1), p.drawImage(n, -n.width / 2, -n.height / 2), p.restore(), n.width = d.width, n.height = d.height, h.drawImage(d, 0, 0);
}
const w = h.getImageData(0, 0, n.width, n.height);
let u = w;
e.dither && e.dither !== "none" && (e.dither === "threshold" ? u = new _().dither(w, e) : e.dither === "bayer" ? u = new A().dither(w, e) : e.dither === "floyd-steinberg" ? u = new D().dither(w, e) : e.dither === "dot" && (u = new I().dither(w, e)), h.putImageData(u, 0, 0));
const y = h.getImageData(0, 0, n.width, n.height), f = new Uint32Array(y.data.buffer);
let g = 128;
(e.dither === "threshold" || e.dither === "none") && (g = e.brightness !== void 0 ? e.brightness : 128);
const b = E(f, g), S = n.toDataURL("image/png");
return {
width: n.width,
height: n.height,
data: b,
dataurl: S
};
}
/**
* Log debug information to the console if debugging is enabled.
* @param args Arguments to log.
*/
log(...t) {
this.options.debug && console.log("[CatPrinter]", ...t);
}
}
export {
F as CAT_ADV_SRV,
V as CAT_PRINT_RX_CHAR,
N as CAT_PRINT_SRV,
M as CAT_PRINT_TX_CHAR,
k as CatPrinter,
v as Command,
x as CommandType,
B as DEF_CANVAS_WIDTH,
U as DEF_ENERGY,
O as DEF_FINISH_FEED,
H as DEF_SPEED,
G as Dithering,
T as crc8,
E as rgbaToBits
};