zpl.tools
Fonts and text

^CI - Change International Font / Encoding

Selects the character encoding the field data is written in.

^CI tells the printer how to decode the bytes that arrive in ^FD. Almost every use of it today is ^CI28, which selects UTF-8. The low-numbered sets are the 1980s arrangement, where a single byte position held a different accented letter in each country. ^CI does not change the font, so it cannot reach a glyph the selected font never had. A correct encoding and a missing glyph look much the same on the label.

Syntax

^CIa,s1,d1,s2,d2

Support

Partially supportedparsed: full · state: full · rendered: partial

The command renders, with the caveat below.

The renderer honors encodings 0–17, 24, 26–31 and 33–36. A number outside that set defaults to encoding 0 and reports ARGUMENT_INVALID with the value. Earlier versions aborted the render. The parser reads the character remapping (s1,d1,…) and ignores it. — not pinned by a test

Labelary renders this command.

Parameters

aCharacter setoptional · one of a set

Encoding used to read the bytes in ^FD. A format is a byte stream, so the encoding applies one byte at a time: a no-break space written as the UTF-8 bytes C2 A0 prints two characters under every set but 28, ┬á under 0 and 13 and  plus a no-break space under 27. A format whose first three bytes are the UTF-8 byte order mark reads as though it opened with ^CI28, and any later ^CI overrides that. We measured both against Labelary on 2026-08-17.

Default
0
Invalid value
discarded, and the default applies. Values 18–23, 25, 32 and 37 or above name no encoding. They reset the character set to 0, the set a format starts in, and report ARGUMENT_INVALID. We measured this against Labelary on 2026-07-30: Labelary resets it too but stays silent. So ^CI27^CI99 renders pixel-identically to ^CI27^CI0 and differently from ^CI27.
Test coverage
Bounds pinned: past what the shape can hold (99999999999999999999). Values exercised: 1–12, 27, 28. Not exercised: 0, 13, 14, 15, 16, 17, 24, 26, 29, 30, 31, 33–36. The invalid-value rule is pinned by 4 tests, including the diagnostic it emits.
0
Single-byte U.S.A. 1 (Zebra code page 850 with US replacements) (default)
1–12
Single-byte national variants of code page 850, in order: U.S.A. 2, U.K., Holland, Denmark/Norway, Sweden/Finland, Germany, France 1, France 2, Italy, Spain, miscellaneous, and Japan (ASCII with the yen sign)
13
Zebra code page 850
14
Double-byte Asian encodings
15
Shift-JIS
16
EUC-JP and EUC-CN
17
UCS-2 big endian, deprecated
24
Single-byte Asian encodings
26
Multibyte Asian encodings, ASCII transparent
27
Zebra code page 1252
28
UTF-8, the usual choice for anything beyond ASCII
29
UTF-16 big endian
30
UTF-16 little endian
31
Zebra code page 1250
33–36
Code pages 1251, 1253, 1254 and 1255
s1Source characteroptional · integernot honoured by zpl.tools

The glyph to print. Up to 256 s,d pairs can follow the character set. The renderer ignores them.

Default
none, which leaves the encoding's own mapping in place
Accepted range
0 to 255
Invalid value
discarded, and the default applies
Test coverage
No test at the minimum, maximum or one step outside the range. The invalid-value rule is pinned by 1 test.
d1Destination characteroptional · integernot honoured by zpl.tools

The byte in ^FD that must print as s1. The renderer ignores it.

Default
none
Accepted range
0 to 255
Invalid value
discarded, and the default applies
Test coverage
No test at the minimum, maximum or one step outside the range. The invalid-value rule is not pinned by a test.

Behavior

The setting applies to the fields that follow it, and persists until the next ^CI, including into the next label of the same data stream. One label can mix sets: put a ^CI immediately before the field that needs it.

Sets 0 to 13 are all Zebra code page 850, with a few byte positions changed for the national letters. 28 and above are the modern encodings, where one character can occupy several bytes. For UTF-8 field data, ^CI28 is the only correct choice, and it belongs at the top of the format before any ^FD.

Character remap

The arguments after the character set are source, destination pairs, and they run in the direction the names do not suggest. The destination is the byte the format sends, and the source is the glyph that prints. ^CI0,65,66 prints every B in the field data as an A. Up to 256 pairs can follow. A remap reaches a glyph that has no byte position of its own in a single-byte code page, and it has no purpose under ^CI28.

Hexadecimal escapes

^CI decides how the printer interprets bytes, not how a format types them. To put a byte into a format that cannot carry it literally, use ^FH and write it as _xx in the field data. _ is the default indicator and always takes two hexadecimal digits. ^CI then decodes the result as usual.

A character set that does not exist

Not every number names an encoding: 18 to 23, 25, 32 and everything from 37 upwards are reserved or invented. Such a value resets the character set to 0, the set a format starts in, and the renderer reports ARGUMENT_INVALID with the number. The label still renders.

A measurement settles that, rather than a choice. On Labelary (2026-07-30) ^CI27^CI99 gives the same pixels as ^CI27^CI0 and different pixels from ^CI27, so Labelary resets the set rather than ignores the unusable value, and it does so silently. Until 2026-07-30 the same input aborted the whole render here, so a one-character typo — ^CI19 for ^CI9 — cost a conversion.

When correct encoding still prints wrong

A correct ^CI guarantees only that the printer knows which character the format means. If the selected font has no glyph for that character, nothing appears, and the two failures look identical on a label. The bitmapped fonts are the usual cause. Zebra drew their cells for code page 850, so a Latin Extended-A letter such as ť is absent from font D on real hardware and on Labelary alike. Use font 0, or a TrueType face, for anything outside the Latin-1 range.

How far each slot reaches into Latin Extended-A was swept position by position on 2026-08-19. Slot N draws all 128, J draws 79, 0 L P Q R S T U V draw the 61 Central European positions, A B C D F G Z draw 10, E draws 2 and H none. A character outside its slot's set prints as a space and the field reports Field data contains characters which the field font cannot display. The differences below record where the fonts here fall short of Labelary's.

Known issues

Tested behavior

Differences from the specification

Labelary against the ZPL II guide

Where Labelary — our compatibility target — behaves differently from the printed specification.

  • Labelary honors only character sets 0–13, 15, 27, 28, 31 and 33–36. — not pinned by a test
  • The 2006 guide documents 0 to 27 only, and reserves 18–23 and 25. Sets 28 to 36 (the UTF encodings and the Windows code pages) come from later firmware, and Labelary documents them rather than the guide. — not pinned by a test
  • Labelary's reference states the remapping pairs apply only to the code page 850 variants, character sets 0 to 13. The guide places no such restriction on them. — not pinned by a test

zpl.tools against the ZPL II guide

Where our renderer behaves differently from the specification.

  • The renderer accepts character remapping pairs and ignores them. We measured that ^CI0,65,66^FDBBB^FS prints BBB here and AAA on Labelary. — not pinned by a test
  • A character-set number the renderer does not map (18–23, 25, 32, ≥37) resets the character set to 0. The guide has the printer keep its current set; Labelary resets it, and the renderer follows Labelary. We measured this on 2026-07-30. — pinned by test_unmappable_character_set_resets_a_set_already_in_force, test_unmappable_character_sets_render_like_ci0

zpl.tools against Labelary

Where our output differs from Labelary for the same input. These are the differences that break a migration.

  • A diagnostic that quotes a raw argument value reads its bytes as code page 850, whatever ^CI is set to. Labelary follows the active set: we measured on 2026-08-19 that ^CI28^CF0,28<C3 A9>XY quotes 28éXY there and two code page 850 characters here. Under ^CI0 and ^CI13, the sets a format starts in, the two agree. — not pinned by a test
  • The renderer also honors sets 14, 16, 17, 24, 26, 29 and 30, which Labelary does not. — not pinned by a test
  • An unmappable character-set number reports ARGUMENT_INVALID; Labelary accepts it silently. The pixels are the same either way. — not pinned by a test
  • Fonts A, C, D, F, G and Z draw - and | from DejaVu Sans Mono rather than from Labelary. Measured off Labelary's own vector page on 2026-08-19: slot A's hyphen inks (0,662,1133,897) of a 2048 unit em against DejaVu's (356,508,877,682), so it is more than twice as wide and centered on the box rule's line rather than below it, and its | inks (480,-512,663,1660) against (530,-512,702,1660). The tweak pass that fitted these faces matched the letterforms. H and M trace back onto the shipped outlines to the unit, and these two did not come with them. The box-drawing characters they were found beside are Labelary's now. — not pinned by a test
  • The warning for an undisplayable character spans the whole ^FD command. Labelary spans only the field data, three bytes further in and three bytes shorter. The message, the command and the one-warning-per-field count all agree, so only the byte span differs. Every warning this renderer attributes to ^FD sits three bytes wide of Labelary's the same way. We measured this on 2026-08-18. — not pinned by a test

Example

UTF-8 field data, with the euro sign and accented letters:

^XA
^CI28
^FO50,40^A0N,40^FDPrix : 12,50 €^FS
^FO50,110^A0N,40^FDMüller, Ñuñez, Åsa, Ægir^FS
^XZ

Open in viewer