Printing Labels Directly from the Browser using WebUSB

While I was organizing some of the cables and adapters in the room in our house I lovingly refer to as “the lab”, I had a need to print some labels for the bins I keep my piles of USB and HDMI cables, power bricks, and video adapters in. I have a small collection of Brother label printers, including a QL700 and a QL 1050, and my work has a QL1110NWB that I occasionally use.

Most of the time, I throw a shipping label image in the Brother P-Touch Editor, hit print, and go about my day. For the labels I use to organize my electronic components stock, I use a little Python script and a HTML+CSS+JS label template to talk to my label printers using the brother_ql Python library by Philipp Klaus:

Well, like any good yak-shaving engineer, I decided that neither of those options were good enough for printing some simple labels with the words “USB power bricks and cables”. Instead, I had to build a new tool that I could use to print labels directly from my browser. And to do that, I needed to build a library like brother_ql, but in JavaScript, replacing the PyUSB usage with WebUSB API calls.

A few hours (and a large pile of test labels) later, and I had the beginnings of a neat browser-only label-printing prototype!

Note: the above embedded webpage will only work on browsers that support WebUSB, which is currently Chrome and Chrome-based browsers like Edge.

The code to actually communicate over WebUSB is actually fairly straightforward. First, you need to request access from the user for a USB device (filtered by Brother’s USB vendor ID), open a connection to the device, and configure it.

device = await navigator.usb.requestDevice({
  filters: [{ vendorId: 0x04f9 }],
});
await device.open();
if (device.configuration === null) {
  await device.selectConfiguration(1);
}

Then we need to find the printer interface and its input and output endpoints, and then claim the interface.

const interfaces = device.configuration.interfaces;
let printerInterfaceIndex = null;
interfaces.forEach((element) => {
  element.alternates.forEach((alternate) => {
    if (alternate.interfaceClass === 7) {
      printerInterfaceIndex = element.interfaceNumber;
      alternate.endpoints.forEach((endpoint) => {
        if (endpoint.direction === "in") {
          endpointIn = endpoint.endpointNumber;
        }
        if (endpoint.direction === "out") {
          endpointOut = endpoint.endpointNumber;
        }
      });
    }
  });
});
if (printerInterfaceIndex === null) {
  throw new Error("Printer interface not found");
} else {
  await device.claimInterface(printerInterfaceIndex);
}

Sending data is as simple as transferring out Uint8Array‘s:

await device.transferOut(endpointOut, commandBuffer);

All of the crunchy parts were in processing the images to generate raster data, which was then used to generate printer commands to send to the label printer. This wasn’t too tricky, since the brother_ql library pretty clearly lays out the bytes and headers that need to be constructed.

You can check out the code on GitHub or play with the demo on GitHub Pages (or in the nifty embedded iframe above)!

There is still a lot to add, but it was super cool being able to send enough data over USB to my QL700 and watching it spit out a cut label! There was obviously a huge pile of incorrect labels printing during testing, and many times labels just weren’t printed at all.

And of course, I still haven’t labeled my bins yet.

♥ tyler crumpton