Dev.to WebDev 🛠 Dev 👁 0 📖 7 min read

Your receipt printer prints ç instead of €. Mine printed Chinese.

People have been asking the same question about receipt printers for years. In 2017 someone opened an issue on escpos-php: their Epson TM-T88IV printed ç where the € should be. They had tried the character itself, the U

People have been asking the same question about receipt printers for years.

In 2017 someone opened an issue on escpos-php: their Epson TM-T88IV printed ç where the € should be. They had tried the character itself, the UTF-8 bytes, and chr(128). In 2022 another person turned up in the same thread with a Bixolon and the same fix. In 2020, on an Android printing library, a Munbyn printer was printing ? instead.

Different printers, different languages, same cause. I wanted to see exactly what goes wrong, so I worked through the bytes.

The printer doesn't speak UTF-8

A receipt printer in text mode reads one byte, prints one character. Bytes below 128 are plain ASCII, which is why English receipts just work. Bytes from 128 to 255 mean whatever the printer's current code page says they mean.

Your JavaScript string is Unicode. Somewhere it has to become those single bytes, and there are two easy ways to get that wrong.

I took the string Café €4.50, produced the bytes each way, and looked at what a printer would show for them in the code pages most printers start in.

Mistake 1: sending UTF-8. This is what TextEncoder gives you, and what most "string to bytes" helpers do. é becomes two bytes and € becomes three.

Printer's code page What comes out
CP437 (the usual default) Café €4.50
CP850 / CP858 Caf├® Ôé¼4.50
Windows-1252 Café €4.50

If your receipt shows Γé¼ or €, that's your answer: the printer got UTF-8.

Mistake 2: taking the low byte of each character. The charCodeAt(i) & 0xFF trick from a lot of old snippets. é becomes E9, and € (U+20AC) becomes AC.

Printer's code page What comes out
CP437 CafΘ ¼4.50
CP858 CafÚ ¼4.50
Windows-1252 Café ¬4.50

That last row is the cruel one. The accents are right, so it looks like you've fixed it, and the euro still isn't there. No code page has € at AC.

What you have to do instead

Three things, and all three have to agree.

1. Pick a code page that contains your characters. CP437, the default on most printers, has no euro sign at all. Neither does CP850. The two usual choices for Western Europe:

Code page € is byte é is byte
CP858 D5 82
Windows-1252 80 E9

2. Encode your text into that code page. Not UTF-8, not the low byte.

3. Tell the printer to switch to it with ESC t n (bytes 1B 74 n) before the text.

Step 3 is where the GitHub threads above ended up. The number n is the printer's own index for a code page, and it isn't the code page number. On Epson's table, CP437 is 0, CP850 is 2, Windows-1252 is 16 and CP858 is 19. Other brands reuse some of these and change others. In the escpos-php thread, the library selected table 0F and sent byte A4, which on that Epson was ç. The workaround that fixed it was selecting table 16 and sending byte 80: Windows-1252.

Find your printer's numbers with a probe

Don't guess n. Print a slip that tries several and read it. For each candidate, switch the code page and print the bytes that would be € and é in the two code pages above:

const ESC = 0x1b;
const bytes = [ESC, 0x40]; // reset
const ascii = (s) => [...s].map((c) => c.charCodeAt(0));

for (const n of [0, 2, 16, 19]) {
  bytes.push(ESC, 0x74, n);                 // ESC t n
  bytes.push(...ascii(`n=${n} D5[`), 0xd5, ...ascii('] 80['), 0x80,
             ...ascii('] 82['), 0x82, ...ascii('] E9['), 0xe9, ...ascii(']'), 0x0a);
}
bytes.push(0x0a, 0x0a, 0x0a, 0x1d, 0x56, 0x01); // feed and cut
// send new Uint8Array(bytes) to the printer as RAW

The row where D5 prints € and 82 prints é is your CP858. The row where 80 prints € and E9 prints é is your Windows-1252. Write the n down per printer model.

What my printer printed: Chinese

I ran exactly that probe on my own printer, a BIXOLON SRP-352plusIII. I expected one row of euro signs. Instead, every probe line came out as Chinese characters.

That isn't a code page problem. It's a different mode. Printers sold for Chinese, Japanese or Korean markets can run in double-byte mode: a byte of 128 or above isn't a character on its own any more, it's the first half of a two-byte character, and it swallows the byte after it. So D5 plus the next byte becomes one Chinese character, and ESC t n makes no difference because the code page table isn't being used at all.

I'm not the first to hit this. In a qz-tray issue from 2018, a Fukun POS80's self-test said its default codepage was GB18030, a Chinese double-byte encoding. In that thread the point was to print Chinese, so it was the right mode. If you want €, it's the wrong one.

What I tried, and what happened:

  1. FS . (bytes 1C 2E). This is the ESC/POS command to cancel Chinese/Kanji mode, and the same qz-tray thread mentions it as the exit command. I added it after ESC @ and printed the probe again. Still Chinese. On my printer the mode isn't something a command in the job switches off.
  2. The self-test slip. Hold FEED while you switch the printer on and it prints its settings. Mine shows firmware V01.00 GSR 013015 and highlights memory switch MSW2 bit 1 as on.
  3. The manual. I couldn't find the memory switch table for my exact model, but in the manual for the previous generation, the SRP-350plusII, MSW2 switch 1 is "Specification for Destination": Double byte Country or Single byte Country. That fits what I see. Memory switches live in the printer's flash and are changed with BIXOLON's utility or the FEED-button menu, not from your print job.

I haven't flipped that switch yet, so I can't tell you it fixes it. What I can tell you is the order to debug in:

  • Every high byte prints as Chinese, Japanese or Korean? Print the self-test first. Look for a double-byte or "destination" setting, or a default code page like GB18030, Big5, Shift-JIS or KS C 5601. Fix that before you touch ESC t.
  • Single wrong symbols like Γé¼, ç or ?? That's the code page. Use the probe above.

If you can't change the printer's settings (it's at a customer's shop, say), there's still the way out at the end of this post: don't send text at all.

An encoder that works in the browser

Here's something I didn't know until I checked. Browsers can decode a few legacy code pages with TextDecoder, and you can turn a decoder around to build an encoder. But the list is short. In Chrome 155 and Node 22:

Label new TextDecoder(label)
windows-1252 works
ibm866 (Cyrillic) works
ibm437, ibm850, ibm858 throws, not supported

So in a browser, Windows-1252 is the Western European code page you can encode to without shipping your own table:

function encoderFor(label) {
  const dec = new TextDecoder(label);
  const map = new Map();
  for (let b = 0x80; b < 256; b++) {
    const ch = dec.decode(new Uint8Array([b]));
    if (ch !== '�') map.set(ch, b);
  }
  return (text) => Uint8Array.from([...text].map((ch) => {
    const code = ch.codePointAt(0);
    return code < 0x80 ? code : (map.get(ch) ?? 0x3f); // '?' for anything the page can't show
  }));
}

const encode = encoderFor('windows-1252');
encode('Café €4.50'); // 43 61 66 e9 20 80 34 2e 35 30

Then send 1B 74 n with your printer's number for Windows-1252, followed by those bytes. For CP858 you'd need a lookup table of your own, since the browser won't decode it for you.

One more habit worth having: send the ESC t n at the start of every job. A printer that has been switched off, or printed something from another program, may be back on its default.

When no code page will do

A code page holds 128 extra characters. That's fine for one alphabet. It doesn't work for a receipt with Arabic, Urdu, Chinese or Hindi on it, or for a shop that prints customer names from anywhere. Arabic and Urdu also need the letters joined and laid out right to left, which the printer's built-in font doesn't do.

For those, stop sending text. Draw the receipt as an image and print the image. The printer just prints dots, so anything your font can draw will come out.

That's the approach I took in Thermalink, the print agent I sell: by default it renders the whole receipt as a bitmap, so code pages don't come into it, and it has a text mode with CP437, CP850, CP858, CP866 and Windows-1252 for when you want the printer's own font. Disclosure: I built it. Nothing above needs it.

What I haven't tested

The tables of garbled output come from running the bytes through each code page on my computer, not from photographs of paper. The ESC t numbers are Epson's published ones, and I haven't confirmed them on paper on any printer: my only printer is stuck in double-byte mode, as above. The MSW2 meaning comes from the previous model's manual, not mine. The TextDecoder results are from Node 22 and Chrome. If you run the probe on your printer, I'd like to hear what it printed, especially if it was Chinese too.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.