How does a printer work? — Part I

Shubham Agrawal
6 min readMar 18, 2020

--

The printer accepts different commands such as PCL, ESC/POS raster commands, etc. These printers, at last, accept a collection of bytes of data.

These bytes of data can be represented as Hexadecimal codes e.g (1B, 2A, FF, 80)

The commands accepted by printers can be found in the datasheets of printers. As of now, I have worked on two types of commands,

The first one is PCL commands:
PCL stands for Printer Control Language. (PCL) is a language (essentially, a set of command codes) that enables applications to control HP DeskJet, LaserJet, and other HP printers. http://www.hp.com/ctg/Manual/bpl13210.pdf

The second one is ESC/POS commands:
ESC/P, short for Epson Standard Code for Printers and sometimes styled Escape/P, is a printer control language developed by Epson to control computer printers. It was mainly used in dot matrix printers and some inkjet printers and is still widely used in many receipt thermal printers.
https://download.brother.com/welcome/docp100367/cv_ql1100_1110_eng_escp_100.pdf. Check out about this in this article.

How does an image appear in binary format?
You should know how does an image looks in the binary format before sending it to the printer. This is an example of a Bat image. Thermal printers print only 1 and are printed as black and leave 0 as a white blank. These 0 and 1 are bits.

00000000000000000000000000
00000011000000000011000000
00001110000011000001110000
00111110000111100001111100
01111111000111100011111110
01111111111111111111111110
11111111111111111111111111
11111111111111111111111111
01111111111111111111111110
01111001110111101110011110
00110000000011000000001100
00011000000011000000011000
00000000000000000000000000

Any image can be converted to this format. Here is a cool website that does this part: https://www.dcode.fr/binary-image

These 0 and 1 are then converted to Hexadecimal codes and finally, along with some starting bytes they are sent to the printer to be printed.

Note: If we don’t send the starting bits/hex codes to the printer, then it will print some garbage values. So, we will have to tell the printer about what we are going to print. Whether it is an image or simple text.

1 Byte = 8 Bits, this 1 byte is converted to hexadecimal code.
Use this link for bits and bytes conversion:
https://cryptii.com/pipes/integer-encoder

Now, we are going to look at these printer languages separately. They both, at last, will generate a block of hexadecimal codes. But they are somehow different from each other. Let’s see how they are different.

  1. PCL (Printer Control Language)
    The main thing to consider in this language is the width of an image irrespective of height. For now, we are going to print a shipping label.
    See this sample label image for e.g., https://firebasestorage.googleapis.com/v0/b/handydiary-e2ab7.appspot.com/o/usps_label.png?alt=media&token=c1d986a1-f48e-4b73-8485-60c85da23da

All printers have their maximum capacity of resolution to which they can print the image. For some printer, it is 300dpi (dots per inch). So for a label of 4" x 6" in size, the maximum of 1200 dots it can print by width. If there is greater than 1200 dots, these dots are neglected and will not be printed.

1 dot = 1 bit, So for 1200 dots, there can be a maximum of 1200 bits or 150 bytes of data in a row.

There is a cool website that helps you see how these bits are seen. Use this link here: https://www.dcode.fr/binary-image

So now, without wasting time lets see how this works in coding.

import numpy as np # 1
import urllib.request as urllib2 # 2
import requests # 3
import json # 4
from PIL import Image # 5

uspsLabelUrl = "https://firebasestorage.googleapis.com/v0/b/handydiary-e2ab7.appspot.com/o/usps_label.png?alt=media&token=c1d986a1-f48e-4b73-8485-60c85da23da5" # 6

def convert():
# Open the label image from url and make greyscale, and get its dimensions
im = Image.open(urllib2.urlopen(
uspsLabelUrl)).convert(
'L') # 7

print(im.size) # 8

# Ensure all black pixels are 1 and all white pixels are 0
# print(lambda p: p < 128 and 1)
binary = im.point(lambda p: p < 128 and 1) # 9

# Resize to its respective height and width so we can fit on label 4X6 inch, get new dimensions.
binary = binary.resize((1200, 1800), Image.LIBIMAGEQUANT) # 10
w, h = binary.size # 11

# print(binary.size)

# Convert to Numpy array - because that's how images are best stored and processed in Python
nim = np.array(binary) # 12

# Print that label image out. This below numpy array shows the same result as shown via this webiste https://www.dcode.fr/binary-image
newStr = '' # 13
for r in range(h): # 14
for c in range(w): # 15
newStr += str(nim[r, c]) # 16
newStr += '' # newStr += '\n' (Do this if you want to visualize these bits) and uncomments below lines # 17
# print(newStr)
# file1 = open("data.txt", "w")
# file1.write(newStr)
# file1.close()

hexString = '' # 18
for i in range(0, len(newStr), 8): # 18
if len(hex(int(newStr[i:i + 8], 2))) == 3: # 20
tempString = hex(int(newStr[i:i + 8], 2)).replace("0x", "") # 21
hexString += "0x0" + tempString # 22
else:
hexString += hex(int(newStr[i:i + 8], 2)) # 23

# Appending extra bytes for printing raster images
binaryString = '0x1B0x2A0x740x330x300x300x520x1B0x2A0x720x310x410x1B0x2A0x620x310x350x300x57' # 24
binaryString += '0x1B0x2A0x620x310x350x300x57'.join(hexString[i:i + int(w / 2)] for i in range(0, len(hexString), int(w / 2))) # 25

# print(newStr)
# file1 = open("data.txt", "w")
# file1.write(binaryString)
# file1.close()

if __name__ == "__main__":
convert()

Now let's understand each line.

Line 1 to 5 are the python libraries that are imported and which we are going to be needed in the code. If you want to understand what each library does, just Google it. I am sure it will help you understand better.

Line 6 is a shipping label png URL.

Line 7, Here we are converting the image into greyscale. Since thermal label printers only deal with black/white pixels. ‘L’ is converting into grey pixels, for more conversion refer to this link of Pillow Library: https://pillow.readthedocs.io/en/3.1.x/reference/Image.html#PIL.Image.Image.convert

Line 8 prints the original size of the image.

Line 9, The point() method can be used to translate the pixel values of an image (e.g. image contrast manipulation). Since printer prints, the pixels that are in black so ensuring all black pixels are 1 and all white pixels are 0.

Line 10, we are resizing the image size (1200 x 1800) as explained above that we need 1200 bits per row.

Line 11, taking the image size in variables w, h.

Line 12, converting the w, h into NumPy array.

Lines 13 to 17, we are converting the whole data into a single string so that we can perform python string operation easily.

Lines 18 to 23, we are converting the newStr which contains the whole bits data into hexadecimal. This is necessary as the printer accepts the data in bytes format.
So, we are taking 8 bits and converting them to bytes first and then to hexadecimal format. We are appending 0 in front of the length of the string is 2.

Line 24 & 25 are the most important lines that need proper attention.
Refer to this link: https://www.intellitech-intl.com/techsupp/techdocs/programmersrefman/section9.pdf

These bytes are the Raster Graphic Commands-
For better understanding, I am removing 0x in every byte:

  1. Raster Graphic Resolution:
    1B2A7433303052 for 300 dpi
    for 75 dpi, it will be 1B2A74373552
    for 100 dpi, it will be 1B2A7431303052
  2. Start Raster Graphics:
    1B2A723141
  3. Transfer Raster Data:
    1B2A6231353057
    Here 313530, 1200 bits = 150 bytes and if we convert 150 bytes into individual hexadecimal as in 1 in ASCII to 31 in Hex, 5 in ASCII to 35 in Hex, 0 in ASCII to 30 in Hex.
    These row bytes are appended with every row data till the height of the image i.e., 1800 times.
    Refer to this link to understand this conversion: http://www.asciitable.com/

That’s it.
Thanks for reading this out. Check out the explanation of the second printer command here.

--

--