12 releases

0.4.3 Feb 5, 2024
0.4.2 Jul 19, 2023
0.3.1 Mar 10, 2022
0.3.0 Aug 23, 2021
0.1.0 Aug 7, 2021

#358 in Hardware support


Used in rawprinter

MIT license

110KB
1.5K SLoC

escpos-rs: A Rust crate for thermal printers

Work in progress. Not ready for production.

Escpos-rs builds a bit on top of escpospp, which aims to bring relatively easy communication to thermal printers that understand the ESC/POS protocol. Here is an example of a simple print with escpos-rs

use escpos_rs::{Printer, PrinterProfile};

fn main() {
    // We create the printer details
    let mut printer_details = PrinterProfile::usb_builder(0x0001, 0x0001).build();
    // We pass it to the printer
    let printer = match Printer::new(printer_details) {
        Ok(maybe_printer) => match maybe_printer {
            Some(printer) => printer,
            None => panic!("No printer was found :(")
        },
        Err(e) => panic!("Error: {}", e)
    };
    // We print simple text
    match printer.println("Hello, world!") {
        Ok(_) => (),
        Err(e) => println!("Error: {}", e)
    }
}

Connecting to a printer

In order to connect to a printer, you need to know both the vendor id, and the product id of your printer. Commonly known printers have these details published online, while for the least common you will have at least the following two options

  • Some printers do print their information on test prints (often holding the feed button)
  • If you are on linux, the lsusb command shows you this information.

With this information, you can start a connection to the printer

// Here goes the vendor id, and the product it (in that order)
let mut printer_details = PrinterProfile::usb_builder(0x0001, 0x0001).build();
// We pass it to the printer
let printer = match Printer::new(printer_details) {
    Ok(maybe_printer) => match maybe_printer {
        Some(printer) => printer,
        None => panic!("No printer was found :(")
    },
    Err(e) => panic!("Error: {}", e)
};

Sending raw information

The printer has the raw method, which allows you to send raw bytes to the printer. Pretty straightforward if you need to operate on the low-level.

use escpos_rs::{
    Printer, PrinterModel,
    command::Command
};

fn main() {
    let printer = match Printer::new(PrinterModel::ZKTeco.usb_profile()) {
        Ok(maybe_printer) => match maybe_printer {
            Some(printer) => printer,
            None => panic!("No printer was found :(")
        },
        Err(e) => panic!("Error: {}", e)
    };
    match printer.raw(b"Hello, world!\n") {
        Ok(_) => (),
        Err(e) => println!("Error: {}", e)
    }

    match printer.raw(Command::Cut.as_bytes()) {
        Ok(_) => (),
        Err(e) => println!("Error: {}", e)
    }
}

You can take a look at the Command enum to see which commands are implemented (the list will grow).

Printing images

You can also send images to the printer (assuming it is supported) through the EscposImage structure.

use escpos_rs::{
    EscposImage, Printer, PrinterProfile, Justification
};

fn main() {
    let printer_profile = PrinterProfile::usb_builder(0x0001, 0x0001).build();
    let printer = match Printer::new(printer_profile) {
        Ok(maybe_printer) => match maybe_printer {
            Some(printer) => printer,
            None => panic!("No printer was found :(")
        },
        Err(e) => panic!("Error: {}", e)
    };
    let img = image::open("logo.jpg").unwrap();
    let escpos_image = EscposImage::new(img, 128, Justification::Center).unwrap();
    match printer.image(escpos_image) {
        Ok(_) => (), // Image should be printed
        Err(e) => println!("Error: {}", e)
    };
}

The EscposImage's constructor takes as first argument a DynamicImage, as second a width scale (from 0 to 255), and as third, a justification.

Network functionality

To be added soon.

The Instruction structure

The Instruction structure has as primary goal the construction of a template, which can be used to print multiple documents with dynamic data.

use escpos_rs::{Printer, PrintData, PrinterProfile, Instruction, Justification, command::Font};

fn main() {
    // Printer profile...
    let printer_profile = PrinterProfile::usb_builder(0x0001, 0x0001)
        .with_font_width(Font::FontA, 32)
        .build();
    // We pass it to the printer
    let printer = match Printer::new(printer_profile) {
        Ok(maybe_printer) => match maybe_printer {
            Some(printer) => printer,
            None => panic!("No printer was found :(")
        },
        Err(e) => panic!("Error: {}", e)
    };
    // We create a simple instruction with a single substitution
    let instruction = Instruction::text(
        "Hello, %name%!",
        Font::FontA,
        Justification::Center,
        // Words that will be replaced in this specific instruction
        Some(vec!["%name%".into()].into_iter().collect())
    );
    // We create custom information for the instruction
    let print_data_1 = PrintData::builder()
        .replacement("%name%", "Carlos")
        .build();
    // And a second set...
    let print_data_2 = PrintData::builder()
        .replacement("%name%", "John")
        .build();
    // We send the instruction to the printer, along with the custom data
    // for this particular print
    match printer.instruction(&instruction, Some(&print_data_1)) {
        Ok(_) => (), // "Hello, Carlos!" should've been printed.
        Err(e) => println!("Error: {}", e)
    }
    // Now we print the second data
    match printer.instruction(&instruction, Some(&print_data_2)) {
        Ok(_) => (), // "Hello, John!" should've been printed.
        Err(e) => println!("Error: {}", e)
    }
}

Instructions can be added up to form a complex instruction. Moreover, you can use serde to serialize and deserialize these instructions so you can save your templates.

Running the examples

You can run the examples contained in the examples folder, invoking them through cargo.

cargo run --example basic

About building this library on Windows

Lib usb is needed for the compilation. Go to https://github.com/libusb/libusb/releases and download the compiled binaries, and put them in your include and bin folders for mingw. You will also need a pkg config file.

  • Execute the command pkg-config.exe --variable pc_path pkg-config to know where pkg-config looks up pc files

  • Add, in any of those routes, the file libusb-1.0.pc with the following content

prefix=c:/mingw-w64/x86_64-8.1.0-posix-seh-rt_v6-rev0/mingw64
exec_prefix=${prefix}
libdir=${prefix}/lib
includedir=${prefix}/include

Name: libusb-1.0
Description: C API for USB device access from Linux, Mac OS X, Windows, OpenBSD/NetBSD and Solaris userspace
Version: 1.0.23
Libs: -L${libdir} -lusb-1.0
Libs.private: -ludev -pthread
Cflags: -I${includedir}/libusb-1.0

Note: The version must match your libusb version, and the prefix must also match your main include and lib folders for MinGW.

The following steps are based on this stackoverflow post.

We assume your mingw installation has its binaries in C:\MinGW\bin

Using the library on Windows

I've only been able to use this library when WinUSB driver is in use for the chosen printer. You can use a tool like Zadig to change your printer's driver. Just bear in mind that this driver change might make the printer invisible to other tools ;).

Dependencies

~8MB
~127K SLoC