Skip to content

Instantly share code, notes, and snippets.

@bdm-k
Last active July 1, 2024 14:52
Show Gist options
  • Save bdm-k/fe903491a051251db688866b7d554065 to your computer and use it in GitHub Desktop.
Save bdm-k/fe903491a051251db688866b7d554065 to your computer and use it in GitHub Desktop.
Print images on the Phomemo M110 printer
/*
* This script allows you to print images on the Phomemo M110 printer.
* It utilizes the Web Serial API, so it only works on browsers supporting it (e.g. Chrome).
* We have confirmed its operation in the following environments:
* - macOS Sonoma 14.5, Chrome 125
* - Windows 11 23H2, Chrome 126
*
* We follow the protocol outlined on this page:
* https://github.com/vivier/phomemo-tools
*
* Basic usage:
* 1. Call 'openSerialPort'. This will prompt the user to select the device.
* 2. Call 'setImage' with the image source (URL) you want to print. The image
* size must be within 320x240 pixels. You might encounter a CORS error if the
* image is not served from the same origin.
* 3. Call 'printImage'.
* 4. If you want to print another image, go back to step 2. If you are done,
* call 'closeSerialPort'. IMPORTANT: There is a known bug where the promise
* returned by 'printImage' resolves before the data has been sent. Ensure the
* image has been printed (by waiting a few seconds, for example) before
* calling 'closeSerialPort'.
*
* 'openSerialPort', 'setImage', and 'printImage' returns promises that resolve
* to true if its operation was successful, and false otherwise. If you want to
* create a robust application, you can use them.
*/
// print configuration
const darkness = 0x06; // range: 0x01 - 0x0f
const speed = 0x01; // range: 0x01 - 0x05
const paperType = 0x0a; // Gap.
const arrayShape = [240, 40]; // [height, width]
let array: Uint8Array | null = null;
let serialPort: any = null;
async function openSerialPort(): Promise<boolean> {
if (serialPort) {
console.error('openSerialPort: There is an open serial port. Close it with \'closeSerialPort\'');
return false;
}
// HACK: We use 'any' because TypeScript thinks 'serial' property does not
// exist on 'Navigator' objects.
const navigator: any = window.navigator;
try {
serialPort = await navigator.serial.requestPort();
await serialPort.open({ baudRate: 128000 });
return true;
} catch (e) {
console.error('openSerialPort:', e);
serialPort = null;
return false;
}
}
async function closeSerialPort(): Promise<void> {
if (!serialPort) {
console.log('closeSerialPort: No open serial port');
return;
}
await serialPort.close();
serialPort = null;
}
async function printImage(): Promise<boolean> {
if (!array) {
console.error('printImage: No image set. Call \'setImage\' first.');
return false;
}
const HEADER = new Uint8Array([0x1b, 0x4e, 0x0d, speed, 0x1b, 0x4e, 0x04, darkness, 0x1f, 0x11, paperType]);
const BLOCK_MARKER = new Uint8Array([0x1d, 0x76, 0x30, 0x00, arrayShape[1], 0x00, arrayShape[0], 0x00]);
const FOOTER = new Uint8Array([0x1f, 0xf0, 0x05, 0x00, 0x1f, 0xf0, 0x03, 0x00]);
try {
if (!serialPort) {
console.error('printImage: No open serial port. Call \'openSerialPort\' first.');
return false;
}
const writer: WritableStreamDefaultWriter = serialPort.writable.getWriter();
await writer.write(HEADER);
await writer.write(BLOCK_MARKER);
await writer.write(array);
await writer.write(FOOTER);
await writer.close();
// NOTE: For some reason, in macOS, if we close the port here, the image
// will not be printed.
return true;
} catch (e) {
console.error(e);
return false;
}
}
async function setImage(imageSrc: string): Promise<boolean> {
const imageElem = new Image();
imageElem.src = imageSrc;
// Wait for the image to be loaded
await imageElem.decode();
// Check if the image is within the acceptable size
if (imageElem.width > arrayShape[1] * 8 || imageElem.height > arrayShape[0]) {
console.error('setImage: Image is too large');
return false;
}
// Decode the image using canvas
const canvas = document.createElement('canvas');
canvas.width = imageElem.width;
canvas.height = imageElem.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('setImage: Failed to get canvas context');
return false;
}
ctx.drawImage(imageElem, 0, 0);
const imageData = ctx.getImageData(0, 0, imageElem.width, imageElem.height);
// offsets to center the image
const xOffset = Math.floor((arrayShape[1] * 8 - imageElem.width) / 2);
const yOffset = Math.floor((arrayShape[0] - imageElem.height) / 2);
// Convert the image to a Uint8Array in the format that Phomemo M110 accepts
array = new Uint8Array(arrayShape[0] * arrayShape[1]);
array.fill(0x00);
for (let y = 0; y < imageData.height; ++y)
for (let x = 0; x < imageData.width; ++x) {
const imageDataIndex = y * 4 * imageData.width + 4 * x;
const r = imageData.data[imageDataIndex];
const g = imageData.data[imageDataIndex + 1];
const b = imageData.data[imageDataIndex + 2];
// if black
if (r < 0x80 && g < 0x80 && b < 0x80) {
const arrayX = x + xOffset;
const arrayY = y + yOffset;
const bitOffset = arrayX % 8;
const arrayIndex = arrayY * arrayShape[1] + ((arrayX - bitOffset) / 8);
array[arrayIndex] += 1 << (7 - bitOffset);
}
}
return true;
}
export { openSerialPort, closeSerialPort, setImage, printImage };
@bdm-k
Copy link
Author

bdm-k commented Jul 1, 2024

Minimal example code

await openSerialPort();
await setImage(QRCodeURL); await printImage();
setTimeout(() => { closeSerialPort() }, 10000);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment