How does a printer work? — Part II

Shubham Agrawal
6 min readMar 31, 2020

--

So in the previous blog, I shared with you the information about PCL (Printer Control Language).

In this, we are going to understand about ESC/P commands. So as I explained before, 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

As already explained how the image appears in a raster format by taking an example of a bat.

Understanding the command

If you read the documentation you will find that:

Select bit image

The two first numbers (1B and 2A) are the command code, the following are parameters, neither of them must be left to chance. You must know exactly what to send in these parameters, otherwise, the printer would not know what to do, will get blocked or print gibberish.

The m parameter specifies the dot density and can have only 4 possible values m = 0, 1, 2, 3, 4, 6, 32, 33, 38, 39, 40, 71, 72, 73.

m density

The n1 and n2 parameters represent the width of the image in pixels. n1 is the low byte and n2 is the high byte. Let’s say that you want to print an image of 73 pixels, then the parameters would be n1 = 0x49 and n2 = 0. Or if the image is 300 pixels wide, n1 = 0x2C and n2 = 0x01. In our example we are printing an image of 600 pixels wide, n1 = 0x58 and n2 = 0x02.
The formula is
(n1 + n2*256)*6 bytes

Another important concept to grasp is that the printer will print the image in stripes of the selected dot density. Let’s say that your image is of 73x48 pixels and you choose m = 33 (24-dot density), your image would be printed in two stripes of 73x24. An explanation of why the previous parameters of the actual image data are important is that once you feed all the pixel information the printer will resume to normal text printing mode, so if you put for example m = 0 (8-dot density), n1 = 0x05 and n2 = 0x0, then you must provide an array of 5 bytes (8 bits x 5 dots wide, bit density is chosen x width of the image). It means the image will be printed from top to bottom and then left to right according to the size of m.

Understanding how the image is printed

If you’ve done any amount of rendering programming you know that you raster the image on the screen in scan lines left to right, top to bottom. But with these printers, the rasterization is done top to bottom, left to right. It is important to have this in mind when sending the image data. This has to do with how the thermal printer head prints the data.

If we have an image of 8 pixels high and w pixels wide the data will be sent:

In this example, we get the image in an array of pixels in the following order [px0, px1, …, pxw, …, px8w], but the “same” data must be sent the printer in another order: [d0, d1, d2, …, d k].

Let’s start with some warm up exercises:

Send the printer [0x1B, 0x2A, 0x0, 0x5, 0x0, 128, 64, 32, 16, 8] (I changed from hex to decimal for clarity).

You should see a descending line of 5 dots. Let’s explain what have we done here, we’ve selected the 8-bit density mode, and the width of the image is 5px (all in hex). The following decimals are the px data sent:

8 bit example
8-bit example

You can add 3 more dots to the line, I left to you as an exercise to complete the line, but remember you’ll need to change the n1 parameter.

Now let’s get to the real stuff, let’s get the pixels of an image. Let’s see the code:

# Open the label image from url and make greyscale, and get its dimensions
im = Image.open(urllib2.urlopen(uspsLabelUrl)).convert('L')
print(im.size) #1
# Ensure all black pixels are 0 and all white pixels are 1
binary = im.point(lambda p: p < 128 and 1) #2
# Resize to its respective height and width so we can fit on label 4X6 inch,(Actual Label Size is 800 x 1200)
binary = binary.resize((600, 1776), Image.LIBIMAGEQUANT) #3
w, h = binary.size #4
# Convert to Numpy array - because that's how images are best stored and processed in Python
nim = np.array(binary) #5
b = 0 #6
hexString = '1b6961001b40' #7
matString = '' #8
for k in range(0, int(h / 48)): #9
hexString += '1b2a475802' #10
for i in range(0, w): #11
for j in range(b, b + 48): #12
matString += str(nim[j, i]) #13
if len(matString) == 8: #14
if len(hex(int(matString, 2))) == 3: #15
tempString = hex(int(matString, 2)).replace("0x", "") #16
hexString += "0" + tempString #17
else: #18
hexString += hex(int(matString, 2)).replace("0x", "") #19
matString = '' #20
b += 48 #21
print(len(hexString)) #22

Now let’s understand each line.

Line 1, 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 2, 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 3, we are resizing the image size (600 x 1776) for 4" x 6" label.

Line 4, we are taking the image size into variable w and h.

Line 5, converting the data into a NumPy 2D array.

Line 7, 1b6961001b40 are the starting bits of Select ESC/P mode and Initialize ESC/P mode.

Line 6 to 21, since I have already told that image printing is done from top to bottom to left to right in stripes. Since we have taken m = 71, so from the top 6 bytes = 48 bits that’s why we are looping and incrementing by 48 bits. For every 8 bits, we are converting it into bytes from top to bottom till 48 bits and then till width of the image i.e, 600 bits.

Line 10, 1b2a are the common bits which will be added with the row data 47 is the hex form of m = 71.

5802 is calculated by the formula as:
(n1 + n2*256) * 6 bytes= (600 * 48) / 8 bytes
So, n1 = 88 and n2 = 2 which if converted to hex will give 58 and 02 respectively.

That’s it.
Thanks for reading this out. This is the end of the article.

--

--