Last active
July 1, 2024 14:52
-
-
Save bdm-k/fe903491a051251db688866b7d554065 to your computer and use it in GitHub Desktop.
Print images on the Phomemo M110 printer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Minimal example code