FGL Programming Guide for Thermal Printers
1. What is FGL
FGL (Friendly Graphics Language), also known as Ghostwriter Printer Language, is a command language for controlling thermal printers. It's widely used in ticketing, event management, boarding passes, and label printing where thermal (direct or transfer) printers are the standard.
Unlike page description languages such as PostScript or PCL, FGL is a lightweight, stream-based protocol. You send a sequence of angle-bracket commands followed by data, and the printer renders the output directly. There is no document model — you position each element (text, barcode, line, image) using absolute coordinates in dots.
FGL supports:
- Up to 16 built-in fonts (F1–F13, F16) with scaling and international character sets
- 1D barcodes: Code39, Code128, EAN-13, EAN-8, UPC, Codabar, Interleaved 2of5
- 2D barcodes: QR codes, PDF-417, Data Matrix, Aztec
- Line drawing, box drawing, and shading patterns
- Dot-addressable graphics, PCX/BMP image support, and stored logos
- Inverse (white-on-black) printing
- Element rotation in 90-degree increments
- Print control: cut, no-cut, repeat, ticket counting, multi-path
Typical ticket sizes are 2.125" × 5.5" (credit card width) or 2.125" × 3.375" at 200 or 300 DPI resolution. This guide is based on the official BOCA Systems FGL46 Programming Guide combined with practical implementation experience.
Compatibility note: FGL has evolved through several revisions (FGL26, FGL42, FGL44, FGL46). Some commands are version-specific — for example, 2D barcodes (PDF-417, Data Matrix, Aztec) require FGL46, and certain command names (such as overwrite mode) vary between firmware revisions. Use the <PROM> command to check your printer's firmware version and always test on target hardware.
2. Command Structure
Every FGL command is enclosed in angle brackets. Commands are chained together without separators, and the data to be printed follows immediately after the command sequence.
<COMMAND><COMMAND>data
For example, to print text with a specific font at a given position:
<F3><HW1,1><NR><RC100,50>Hello World<WH1,1>
Reading left to right:
<F3>— select font 3 (OCR-B, 17×31)<HW1,1>— set height and width scaling to 1×<NR>— no rotation<RC100,50>— position at row 100, column 50 (in dots)Hello World— the text to print<WH1,1>— reset width/height scaling after text
A complete print job is terminated with a print command. The most commonly used terminator is <q>. On some firmware revisions <q> prints without cutting, while <p> prints and cuts. On others, <q> is the standard end-of-ticket command. Always verify behavior against your specific firmware. The <FF> (form feed, 0CH) command is a universal print-and-cut alternative:
<F3><HW1,1><NR><RC100,50>Hello World<WH1,1>
<q>
3. Coordinate System & DPI
FGL uses a dot-based coordinate system. The <RCrow,column> command positions elements where:
- Row = Y-axis position (distance from top edge, in dots)
- Column = X-axis position (distance from left edge, in dots)
The origin <RC0,0> is the top-left corner of the ticket.
Dots vs. inches: To convert inches to dots, multiply by the printer DPI. At 300 DPI, 1 inch = 300 dots. A position of 0.5" from the top and 1" from the left becomes <RC150,300>.
Common Ticket Sizes
| Size (inches) | At 200 DPI | At 300 DPI |
|---|---|---|
| 2.0" × 5.5" | 400 × 1100 | 600 × 1650 |
| 2.125" × 5.5" | 425 × 1100 | 637 × 1650 |
| 2.5" × 5.5" | 500 × 1100 | 750 × 1650 |
| 2.7" × 5.5" | 540 × 1100 | 810 × 1650 |
When working in landscape orientation, width and height are swapped. A 2.125" × 5.5" ticket in landscape has the longer dimension as its width (1650 dots at 300 DPI).
4. Text & Fonts
FGL provides up to 16 built-in fonts (F1–F13 being the most common, plus F14–F16 on specific models), selected with the <Fn> command. Each font has a fixed character cell size (width × height in dots). Note that F5 is a special-purpose font not available on all printers:
| Command | Name | Cell (200 DPI) | Cell (300 DPI) | Style |
|---|---|---|---|---|
<F1> | Font 1 | 5×7 | 7×8 | Basic ASCII |
<F2> | Font 2 | 8×16 | 10×18 | Basic ASCII |
<F3> | OCR-B | 17×31 | 20×33 | OCR-B standard |
<F4> | OCR-A | 5×9 | 7×11 | OCR-A standard |
<F6> | Large OCR-B | 30×52 | 34×56 | OCR-B large |
<F7> | OCR-A Full | 15×29 | 20×31 | Full OCR-A set |
<F8> | Courier | 20×40 | 20×33 | Courier |
<F9> | Small OCR-B | 13×20 | 13×22 | OCR-B compact |
<F10> | Prestige | 25×41 | 28×41 | Bold Prestige |
<F11> | Script | 25×49 | 26×49 | Script |
<F12> | Orator | 46×91 | 47×91 | Tall, bold |
<F13> | Courier Intl | 20×40 | 20×42 | Courier + international |
<F16> | Cyrillic | 18×31 | 20×33 | Cyrillic character set |
Font Scaling
The <HWheight,width> command scales the selected font. Values are integer multipliers:
# Double-height, normal-width text
<F3><HW2,1><NR><RC100,50>Tall Text<WH1,1>
# Triple-width, double-height
<F3><HW2,3><NR><RC200,50>Wide & Tall<WH1,1>
Always reset scaling with <WH1,1> after your text to prevent it from affecting subsequent elements.
Full Text Command Pattern
<Fn><HWh,w><ROTATION><RCrow,col>text<WH1,1>
5. Rotation
FGL supports four rotation states in 90-degree increments:
| Command | Rotation | Description |
|---|---|---|
<NR> | 0° | No rotation (default, left to right) |
<RR> | 90° | Rotate right (top to bottom) |
<RU> | 180° | Rotate upside down (right to left) |
<RL> | 270° | Rotate left (bottom to top) |
The rotation command is placed before the position command. The <RC> coordinates always refer to the element's anchor point, which shifts depending on the rotation angle.
# Normal text
<F3><HW1,1><NR><RC100,50>Normal<WH1,1>
# Same text rotated 90 degrees clockwise
<F3><HW1,1><RR><RC100,50>Rotated<WH1,1>
Tip: When rotating elements at 180° or 270°, you may need to adjust the <RC> coordinates to account for the shifted anchor point. The top-left origin moves to the opposite corner after rotation.
6. 1D Barcodes
FGL supports several 1D barcode symbologies. Each type has a unique tag letter used in the barcode command.
| Symbology | Tag | Delimiter | Example Data |
|---|---|---|---|
| Code 39 | N | * | *CODE39* |
| Code 128 | O | ^ | ^CODE128^ |
| EAN-13 | E | — | 5901234123457 |
| EAN-8 | U | — | 12345670 |
| UPC | U | — | 401234567893 |
| Codabar | C | — | A123456B |
| Interleaved 2of5 | F | — | 1234567890 |
There are also legacy barcode-select commands: <A=N> (Code39), <A=O> (Code128), <A=E> (EAN-13), <A=U> (UPC/EAN-8), <A=C> (Codabar), <A=F> (Interleaved 2of5). These are rotation-independent and may be found in older implementations. The modern tag-based approach described below is preferred.
Barcode Command Structure
<ROTATION><RCrow,col><Xn><BI><TAGorientationwidth>data
Breaking down the commands:
| Command | Purpose | Values |
|---|---|---|
<Xn> | Bar expand factor | 1–9 (2 recommended for readability) |
<BI> | Barcode interpretation | Prints human-readable text below the barcode |
| Tag letter | Symbology type | N, O, E, U, C (see table above) |
| Orientation | Print direction | P = portrait (fence), L = landscape (ladder) |
| Width | Bar width multiplier | 1–8 |
Barcode Orientation
The orientation suffix (P or L) determines whether bars are printed as a fence (vertical bars, read left to right) or a ladder (horizontal bars, read top to bottom):
<NP5>— Code39, portrait/fence mode, width 5<OL4>— Code128, landscape/ladder mode, width 4
For 180° and 270° rotations, the tag letter becomes lowercase:
# Code39 barcode, no rotation, portrait, width 5
<NR><RC200,50><X2><BI><NP5>*TICKET-001*
# Code128 barcode, with interpretation, portrait, width 4
<NR><RC300,50><X2><BI><OP4>^ABC-12345^
# Same barcode rotated 180 degrees (lowercase tag)
<RU><RC300,50><X2><BI><oP4>^ABC-12345^
7. 2D Barcodes (QR, PDF-417, Data Matrix, Aztec)
FGL46 supports four 2D barcode symbologies. All use a curly-brace data format {data} with optional configuration parameters.
QR Codes
QR codes are the most commonly used 2D barcode in FGL. The command structure uses version and module size parameters:
<NR><RCrow,col><QRVversion><QRsize>data
The <QRVn> command sets the QR version (complexity), and <QRn> sets the module size in dots:
| Command | Modules | Max Chars |
|---|---|---|
<QRV2> | 25 × 25 | ~20 |
<QRV7> | 45 × 45 | ~122 |
<QRV11> | 61 × 61 | ~251 |
<QRV15> | 77 × 77 | ~412 |
Module sizes: <QR4> = 4 dots/module (compact), <QR6> = 6 dots/module (easier to scan). Point sizes range from 3–16.
| Version + Size | Width (300 DPI) | Use Case |
|---|---|---|
<QRV2><QR6> | ~0.47" | Short URLs, small IDs |
<QRV7><QR4> | ~0.59" | Medium data, compact |
<QRV7><QR6> | ~0.87" | Medium data, scannable |
<QRV11><QR4> | ~1.18" | Long URLs, high density |
# Small QR code for a short URL
<NR><RC50,400><QRV2><QR6>https://example.com
# Larger QR code for ticket validation data
<NR><RC50,400><QRV7><QR4>EVT-2024-ABCD-1234-WXYZ
QR codes also support error correction levels 0–3 (L, M, Q, H) and encode modes 0–2 when using the extended syntax.
PDF-417
PDF-417 is a stacked 2D barcode popular in transportation and government IDs. It can encode up to ~1,800 ASCII characters. The FGL command uses positional parameters for columns, rows, error correction, and truncation flag:
# PDF-417: columns=5, rows=20, error correction=3, not truncated
<NR><RC100,50><p4175,20,3,0>Ticket: EVT-2024-001234
# PDF-417 with tilde for control codes
<NR><RC300,50><p4174,15,2,0>~029Data with GS separator
The tilde (~) character is used for embedding control codes. Parameters: <p417cols,rows,errLevel,truncated>.
Data Matrix
Data Matrix encodes data in a compact square or rectangular pattern. Useful when space is limited. FGL supports encode modes 0–3 and preferred format selection (0–29 predefined sizes):
# Data Matrix: encode mode 0 (auto), preferred format 0 (auto size)
<NR><RC100,50><dm0,0>https://example.com/verify/12345
# Data Matrix: encode mode 1 (ASCII), fixed size format 5
<NR><RC100,300><dm1,5>COMPACT-ID-789
Parameters: <dmencodeMode,preferredFormat>. Format 0 = auto-size based on data length.
Aztec
Aztec codes are used on boarding passes (IATA BCBP standard) and do not require a quiet zone, making them space-efficient. FGL supports configurable error correction levels (5–95%) and layer counts:
# Aztec: error correction 23%, auto layers
<NR><RC100,50><az23,0>M1SMITH/JOHN EABC123 JFKLHRBA 0742 231Y
# Aztec: error correction 50%, fixed 8 layers
<NR><RC100,300><az50,8>HIGH-RELIABILITY-DATA
Parameters: <azerrorPercent,layers>. Use 0 for layers to let the printer auto-select based on data size.
Note: 2D barcode rotation support varies by firmware version. Older firmware does not support rotation commands (<RR>, <RU>, <RL>) for 2D barcodes — they print at <NR> orientation only. Some newer FGL46 revisions add partial rotation support. Always test rotation on your specific hardware. If rotation doesn't work, rotate the entire ticket layout instead.
8. Lines & Boxes
FGL draws horizontal lines, vertical lines, and filled boxes with configurable thickness:
<NR><RCrow,col><LTthickness><HXlength> # horizontal
<NR><RCrow,col><LTthickness><VXlength> # vertical
| Command | Purpose |
|---|---|
<LTn> | Line thickness in dots (e.g., <LT1> = 1 dot, <LT3> = 3 dots) |
<HXn> | Draw horizontal line of n dots length |
<VXn> | Draw vertical line of n dots length |
# Thin horizontal separator (1px, 400 dots wide)
<NR><RC250,30><LT1><HX400>
# Thick vertical border (3px, 490 dots tall)
<NR><RC30,95><LT3><VX490>
# Box outline (four lines)
<NR><RC100,100><LT1><HX200> # top
<NR><RC300,100><LT1><HX200> # bottom
<NR><RC100,100><LT1><VX200> # left
<NR><RC100,300><LT1><VX200> # right
Lines always use <NR> (no rotation). To change the line direction, switch between <HX> and <VX>. When drawing lines at 180° or 270° angles programmatically, adjust the starting coordinates by subtracting the line length from the appropriate axis.
Box Drawing
FGL also provides a dedicated box command that draws a filled rectangle without needing four separate line commands:
<NR><RCrow,col><LTthickness><DBheight,width>
The <DBr,c> (Draw Box) command draws a box r dots tall by c dots wide. Line thickness set by <LT> applies to the box border:
# Outlined box 200x400 with 2-dot border
<NR><RC100,50><LT2><DB200,400>
9. Graphics (Images)
FGL's graphics mode lets you control individual dots on the ticket. This is used for logos, icons, and any image content. The printer receives column-by-column data where each byte represents 8 vertical dots.
Graphics Commands
FGL has two graphics modes:
| Command | Mode | Data Format |
|---|---|---|
<Gn> | Binary graphics | Raw bytes (values 0–255) |
<gn> | ASCII graphics | Hex string (2 chars per byte) |
The ASCII mode (<g>) is more commonly used in software implementations because it avoids encoding issues with binary data. The n parameter in <gn> is the total number of hex characters (not byte count). Since each byte requires two hex characters, n must always be even. For example, 60 columns of dot data = 60 bytes = 120 hex chars, so you'd use <g120>.
How Dot Data Works
Each byte represents a column of 8 dots. The MSB is the top dot and the LSB is the bottom dot. A 1 bit prints a black dot; a 0 is blank.
In ASCII graphics mode, this byte is sent as the two characters A5.
Multi-Row Images
Since each graphics command covers only 8 vertical dots, taller images are split into horizontal strips. Each strip is a separate FGL command, positioned 8 dots below the previous one:
# Image: 60px wide, 24px tall = 3 strips of 8px each
# Each strip has 60 columns × 2 hex chars = 120 hex chars
<NR><RC100,50><g120>FF00FF00...hex data for row 0-7...
<NR><RC108,50><g120>AA55AA55...hex data for row 8-15...
<NR><RC116,50><g120>0F0F0F0F...hex data for row 16-23...
Image Conversion Algorithm
To convert a standard image (PNG, BMP) to FGL graphics data:
- Convert to monochrome. Each pixel becomes either black (1) or white (0). A common approach is to threshold the alpha channel: opacity > 120 = black dot.
- Split into 8-pixel strips. Divide the image height into chunks of 8 rows. If the height isn't a multiple of 8, pad the last strip with zeros.
- Scan column by column. For each strip, iterate over each column (x position). Collect the 8 vertical pixels into a single byte, with the topmost pixel as the MSB.
- Encode as hex. Convert each byte to a 2-character hex string (zero-padded). Concatenate all columns into one hex string per strip.
- Generate FGL commands. For each strip, emit
<NR><RCy,x><gn>hexdatawhere y increments by 8 for each strip.
Pseudocode:
// Convert image to FGL ASCII graphics commands
function imageToFGL(imageData, width, height, startRow, startCol) {
const strips = Math.ceil(height / 8)
const commands = []
for (let strip = 0; strip < strips; strip++) {
let hex = ""
for (let x = 0; x < width; x++) {
let byte = 0
for (let bit = 0; bit < 8; bit++) {
let y = strip * 8 + bit
if (isBlackPixel(imageData, x, y))
byte |= 1 << (7 - bit) // MSB = top dot
}
hex += byte.toString(16).padStart(2, "0")
}
const row = startRow + strip * 8
commands.push(
`<NR><RC${row},${startCol}><g${hex.length}>${hex}`
)
}
return commands.join("\n")
}
10. Inverse Printing & Visual Effects
FGL supports inverse (white-on-black) printing for visual emphasis. This is commonly used for section headers, highlight bars, and status indicators on tickets.
| Command | Description |
|---|---|
<EI> | Enable inverse mode — all subsequent text prints white on black background |
<DI> | Disable inverse mode — return to normal black on white |
# Black bar with white text header
<EI><F6><HW1,1><NR><RC40,50> VIP ACCESS <WH1,1><DI>
# Normal text below
<F3><HW1,1><NR><RC100,50>Section A - Row 5<WH1,1>
Tip: Add spaces around inverse text (e.g., VIP ACCESS ) to create padding within the black background area.
Shading Patterns
On FGL42/44/46 printers, you can fill areas with predefined shading patterns:
| Command | Description |
|---|---|
<ES> | Enable shading |
<DS> | Disable shading |
<SPn> | Select pattern number |
<SPBn> | Shade pattern background |
<SPFn> | Shade pattern foreground |
Overwrite Mode
By default, overlapping elements print on top of each other. Overwrite mode replaces previously printed data in the buffer:
<OWE>/<OVR>— enable overwrite (replace existing data at position). Command name varies by firmware revision.<OWD>/<OVD>— disable overwrite (default, additive mode). Command name varies by firmware revision.
11. Print Control & Status
FGL provides several commands for controlling how and when tickets are printed, cut, and counted.
Print & Cut Commands
| Command | Description |
|---|---|
<q> | Print and cut ticket (standard termination) |
<FF> / 0CH | Form feed — print and cut (alternative to <q>) |
1DH | Print without cutting — useful for multi-part tickets |
<NOCM> | No-cut mode (driver command) |
<CM> | Cut mode — re-enable cutting (default) |
Repeat & Hold
| Command | Description |
|---|---|
<REPn> | Print n additional copies (total = n+1) |
<PH> | Print and hold image in buffer for field replacement |
<PNH> | Hold image without cutting until normal print sent |
The <PH> (Print & Hold) command is powerful for high-speed printing: design a template once, then replace only the variable fields (name, seat, barcode) between prints.
Ticket Counting
| Command | Description |
|---|---|
<PTC> | Print the current 7-digit ticket count on the ticket |
<LTCnnnnnnn> | Load/preset the ticket counter (all 7 digits required) |
<RTCn> | Reset ticket count for path n |
Buffer & State
| Command | Description |
|---|---|
<CB> | Clear ticket buffer and restore default font settings |
<CR> | Carriage return — move to next line in current rotation |
<MTM> | Multiple ticket mode (default) |
<STM> | Single ticket mode |
<MBM> | Multiple buffer mode (default) |
<SBM> | Single buffer mode (FGL2 compatibility) |
Status & Diagnostics
Status commands let your application query the printer state for error handling and monitoring:
| Command | Description |
|---|---|
<SR> | Request single-byte status (paper, jam, temperature) |
<PROM> | Request firmware version and ticket count |
<DSA> | Report available download memory (8-digit hex) |
<DIAG> | Enter diagnostic mode |
<CME> | Enable CRT messages (out of tickets, jams, etc.) |
<CMD> | Disable CRT messages |
Print Intensity
The <LVn> command adjusts print head voltage offset from -5 to +5 (default 0). Increase for darker prints on thick stock, decrease for thinner media to prevent smearing.
Dual Path / Multi-Printer
BOCA printers with dual paths can target specific print paths:
<PTH1>/<PTH2>— select print path<DPM>— dual printer mode<DSM>— dual supply mode
12. Logo Storage & Recall
FGL printers can store logos in flash memory for fast recall, avoiding the need to transmit image data with every ticket.
Logo Commands
| Command | Description |
|---|---|
<SPr,c> | Set starting point (position) for the next logo |
<RLn> | Print factory-resident logo ID n |
<PLn> | Print downloaded (custom) logo ID n |
Firmware note: Older BOCA firmware revisions may use <LDn> / <LOn> for logo download and recall instead of the <FI> / <PL> system. Check your firmware's programming guide for the correct syntax.
Downloading Logos
Custom logos are uploaded to the printer's flash memory using file operations:
| Command | Description |
|---|---|
<FIn> | Assign file ID n to the next download |
<PF> | Set permanent file mode (persists across power cycles) |
<TF> | Set temporary file mode (cleared on power off) |
<DFn> | Delete file ID n from memory |
ESC c | Clear entire download area and reset pointers |
PCX & BMP Image Files
On FGL42/44/46 printers, you can send standard image files directly instead of converting to dot data:
# Send a PCX image file (FGL42/44/46)
<SP100,50><pcx><G3500>...3500 bytes of PCX data...
# Send a BMP image file (FGL26/46)
<SP100,50><bmp><G4200>...4200 bytes of BMP data...
The <SPr,c> positions the image, and <Gn> specifies the exact byte count of the file data that follows. There must be no extra characters between the command and the file bytes.
Stored logos vs. inline graphics: For logos that appear on every ticket, download once to flash and recall with <PLn>. This is significantly faster than sending the image data with each ticket. Use inline <g> graphics only for dynamic or one-off images.
13. Building an FGL Generator (Lessons Learned)
If you're building software that generates FGL programmatically (a layout editor, print server, or ticket system), the following patterns come from real production experience. These are the problems the official documentation doesn't warn you about.
The Rotation Origin Problem
This is the single most confusing aspect of FGL programming. When you rotate an element, its origin point shifts to a different corner. The <RC> command always refers to the origin point, and if you don't account for the shift, your elements will "jump" to unexpected positions after rotation.
The origin corner for each rotation angle:
When the <RC> command places an element, it uses the origin corner appropriate for the rotation angle. So when generating FGL, you must compute the correct anchor point based on the current rotation.
Angle Normalization
User interactions (drag-rotate in an editor, API input) can produce arbitrary angles: negative values, values over 360, or decimals like 89.7°. FGL only supports 0/90/180/270. Always normalize:
function normalizeAngle(angle) {
return (Math.round(angle) + 360) % 360
}
// -90 → 270
// 450 → 90
// 89.7 → 90
Center-Point Rotation Technique
If your editor allows visual drag-rotation, rotating around the corner origin causes elements to "jump". The solution is to rotate around the element's center, then recalculate the top-left position for FGL output:
function rotateElement(el, newAngle) {
// 1. Find the center of the element
const cx = el.x + el.width / 2
const cy = el.y + el.height / 2
// 2. Compute the new top-left after rotation around center
const origin = getOriginCorner(newAngle)
const hw = el.width / 2, hh = el.height / 2
const dx = origin.x === "left" ? -hw : hw
const dy = origin.y === "top" ? -hh : hh
el.x = Math.round(cx + dx)
el.y = Math.round(cy + dy)
el.angle = newAngle
}
function getOriginCorner(angle) {
const map = {
0: { x: "left", y: "top" },
90: { x: "left", y: "bottom" },
180: { x: "right", y: "bottom" },
270: { x: "right", y: "top" },
}
return map[angle]
}
This ensures the element stays visually anchored at its center during rotation, then the coordinates are recalculated for FGL output.
Coordinate Rounding — Always Round to Integers
FGL coordinates are integers (dot positions). Fractional values cause unpredictable behavior. After any calculation, round and clamp:
function sanitizeCoords(props) {
return {
top: Math.max(0, Math.round(props.top)),
left: Math.max(0, Math.round(props.left)),
strokeWidth: Math.round(props.strokeWidth),
length: Math.round(props.length),
}
}
Negative coordinates are silently accepted by some printers but produce no output. Always clamp to 0 minimum. If an element partially extends off-ticket, the printer will clip — but negative RC values may cause firmware-specific behavior.
The Landscape Lock
Most BOCA printers expect FGL data in landscape orientation regardless of how the ticket physically feeds through the printer. In landscape mode, width and height are swapped:
function toTicketDots(widthInch, heightInch, dpi, landscape) {
// In landscape, swap so the longer edge is always width
if (landscape) {
return { w: Math.round(heightInch * dpi), h: Math.round(widthInch * dpi) }
}
return { w: Math.round(widthInch * dpi), h: Math.round(heightInch * dpi) }
}
// 2.125" x 5.5" ticket at 300 DPI, landscape:
// w = 5.5 * 300 = 1650 dots
// h = 2.125 * 300 = 637 dots
If your system allows portrait editing, you must convert to landscape coordinates before generating FGL:
function buildFGL(elements, ticket) {
// Ensure landscape coordinates for FGL
const size = toTicketDots(ticket.width, ticket.height, ticket.dpi, true)
const commands = elements.map(el => {
const coords = toLandscapeCoords(el, size)
return elementToFGL(el.type, coords, el.props)
})
return commands.join("\n") + "\n<q>"
}
Line Coordinate Adjustment for Rotation
Lines in FGL are always drawn with <NR> (no rotation tag). Instead of rotating the line, you switch between <HX> (horizontal) and <VX> (vertical). But at 180° and 270°, the starting coordinate must be adjusted by subtracting the line length:
function lineFGL(x, y, angle, thickness, length) {
const dir = (angle === 0 || angle === 180) ? "HX" : "VX"
// Shift start point for 180/270 so the line extends correctly
if (angle === 180) x -= length
if (angle === 270) y -= length
return `<NR><RC${y},${x}><LT${thickness}><${dir}${length}>`
}
Without this adjustment, 180° and 270° lines will start at the wrong end and extend off-ticket.
Image Rotation Without FGL Rotation
FGL graphics mode (<g>) does not support rotation commands. To print a rotated image, you must pre-rotate the source image on the canvas/server side before converting to dot data:
function rotateImage(img, angle) {
const c = document.createElement("canvas")
const ctx = c.getContext("2d")
const swap = angle === 90 || angle === 270
c.width = swap ? img.height : img.width
c.height = swap ? img.width : img.height
ctx.translate(c.width / 2, c.height / 2)
ctx.rotate(angle * Math.PI / 180)
ctx.drawImage(img, -img.width / 2, -img.height / 2)
return ctx.getImageData(0, 0, c.width, c.height)
}
Then feed the rotated pixel data into the 1-bit compression algorithm from Section 9.
Schema Serialization — Save/Load Designs
If you're building an editor, you'll need a serialization format to save and reload layouts. A practical JSON schema:
{
"version": 1,
"ticket": {
"width": 2.125, "height": 5.5,
"dpi": 300,
"landscape": true
},
"elements": [
{
"type": "text",
"x": 50, "y": 100, "angle": 0,
"font": 3, "scaleH": 1, "scaleW": 1,
"content": "Hello World",
"bind": null
},
{
"type": "barcode",
"x": 50, "y": 200, "angle": 0,
"symbology": "code128",
"expand": 2, "barWidth": 5,
"interpretation": true
}
]
}
Key practices:
- Version field — always validate before loading. Prevents loading incompatible files from older/newer formats.
- Ticket metadata is essential — without DPI and orientation, coordinates are meaningless. Always store them alongside elements.
- Keep elements minimal — only persist properties that define the layout, not runtime state or computed values.
- Use
bindfor dynamic fields — elements with abindkey get their content replaced at print time (see Dynamic Data Fields below).
Dynamic Data Fields (Placeholders)
In production, most tickets have a mix of static layout (logos, borders, labels) and dynamic data (name, seat, barcode). Use a bind field to mark elements whose content is replaced at print time:
const ROTATIONS = { 0: "NR", 90: "RR", 180: "RU", 270: "RL" }
function textToFGL(el, data) {
// Use bound data if available, otherwise static content
const text = el.bind && data[el.bind] ? data[el.bind] : el.content
const rot = ROTATIONS[el.angle]
return `<F${el.font}><HW${el.scaleH},${el.scaleW}>`
+ `<${rot}><RC${el.y},${el.x}>${text}<WH1,1>`
}
// Usage: fill in dynamic fields at print time
const fgl = textToFGL(
{ font: 3, scaleH: 1, scaleW: 1, angle: 0, x: 50, y: 100, bind: "guest_name" },
{ guest_name: "John Smith", seat: "A-12" }
)
The editor stores placeholder values like "Guest Name" in content, while the bind key maps to a data field. At print time, your server supplies real values through the data object.
Parallel FGL Generation
Image-to-FGL conversion can be slow (canvas rendering, pixel scanning). Since element order doesn't matter in FGL — only coordinates determine placement — you can generate all elements in parallel:
// Slow: each image blocks the next
let fgl = ""
for (const el of elements) {
fgl += await toFGL(el, data)
}
// Fast: all conversions run concurrently
const parts = await Promise.all(elements.map(el => toFGL(el, data)))
const fgl = parts.join("\n") + "\n<q>"
This is safe because each element produces an independent FGL command — there is no shared state between them.
14. Composing a Full Layout
A complete FGL layout is simply a concatenation of individual element commands, terminated by <q>. Each element occupies one or more lines, and the printer processes them sequentially.
Example: Event Ticket
# ── Text elements ──
<F12><HW1,1><NR><RC40,50>SUMMER FEST 2024<WH1,1>
<F3><HW1,1><NR><RC140,50>Main Stage - Section A<WH1,1>
<F9><HW1,1><NR><RC180,50>June 15, 2024 - 7:00 PM<WH1,1>
<F9><HW1,1><NR><RC210,50>Gate: North Row: 12 Seat: 34<WH1,1>
# ── Separator line ──
<NR><RC250,40><LT1><HX560>
# ── Barcode ──
<NR><RC280,50><X2><BI><OP4>^EVT-2024-001234^
# ── QR code (right side) ──
<NR><RC270,480><QRV7><QR4>https://verify.example.com/EVT-2024-001234
# ── Rotated side text ──
<F1><HW1,1><RR><RC30,620>ADMIT ONE<WH1,1>
# ── End of ticket ──
<q>
Key points:
- Each element is independent — order doesn't affect rendering, only the
<RC>coordinates determine placement. - The
<q>at the end triggers the ticket to be cut and ejected. - Multiple elements can overlap if their coordinates intersect (later elements print over earlier ones).
- All coordinates must be within the physical ticket dimensions. Content outside the print area is clipped.
15. Practical Tips
DPI Awareness
Always know your printer's DPI before calculating coordinates. A layout designed for 300 DPI will print at half the intended physical size on a 200 DPI printer. Keep coordinates as DPI-relative calculations:
// Calculate position from inches
const DPI = 300
const row = Math.round(0.5 * DPI) // 150 dots = 0.5 inch from top
const col = Math.round(1.0 * DPI) // 300 dots = 1.0 inch from left
Orientation Locking
Many FGL printers expect ticket data in landscape orientation regardless of how the physical ticket is oriented in the printer. If your printer uses landscape-first coordinate mapping, ensure all coordinates are calculated with the longer dimension as width. Swap your width/height values accordingly before generating FGL.
Placeholder Patterns
When building dynamic systems where data is injected at print time, use placeholder tokens in your FGL templates:
<F3><HW1,1><NR><RC100,50>{{event_name}}<WH1,1>
<NR><RC280,50><X2><BI><OP4>^{{ticket_code}}^
<NR><RC280,480><QRV7><QR4>{{validation_url}}
<q>
Your print server replaces the {{...}} tokens with actual data before sending the FGL stream to the printer.
High-Speed Printing
For high-throughput scenarios (event gates, transit turnstiles), minimize per-ticket data transfer:
- Use stored logos — download once with
<PL>, recall on every ticket - Use Print & Hold —
<PH>keeps the template in buffer, only send variable data for each ticket - Minimize graphics — ASCII graphics (
<g>) transmit 2x the data of binary (<G>). Use binary mode if your transport supports 8-bit data - Use
<REPn>for identical copies instead of re-sending the ticket
Common Pitfalls
- Forgetting
<q>— Without the termination command, the printer will buffer indefinitely and never print. - Binary vs. ASCII graphics — If your system can't send raw bytes, always use the lowercase
<g>command for ASCII hex encoding. The byte count in<gn>must be even. - Font bleeding — Always reset with
<WH1,1>after scaled text, or the next text element may inherit the scaling. - Coordinate overflow — Verify that all positions plus element dimensions fit within the ticket's dot area. Out-of-bounds content is silently clipped.
- Inverse mode leaking — Always pair
<EI>with<DI>. If you forget to disable inverse mode, all subsequent text will be white-on-black. - Barcode rotation tags — Remember to lowercase the barcode type letter when rotating 180° or 270°. An uppercase
Oat 180° will produce incorrect output. - Status polling — Use
<SR>to check printer status before sending a job. Common issues: out of paper, cutter jam, print head overheated. - Logo vs. inline graphics — Download frequently-used logos to flash memory once and recall with
<PLn>. Sending inline graphics with every ticket wastes bandwidth and slows throughput. - Buffer overflow — Complex tickets with large images may exceed the buffer. Use
<DSA>to check available memory. Consider splitting image data or reducing resolution.
Debugging Workflow
- Start with
<DIAG>to verify the printer processes commands. - Test positioning with simple text at known coordinates before adding complex elements.
- Use
<PROM>to verify firmware version — some commands require specific FGL versions (e.g., 2D barcodes need FGL46). - Send
<CB>between test jobs to clear the buffer and reset state. - Verify barcode data integrity: scan printed barcodes and compare with the input data.
Ticket Length Configuration
If your ticket stock differs from the factory default, configure the printing length:
<PPLn>— permanently store printing length (persists across power cycles)<PTLn>— permanently store ticket length<DPL>— delete permanent length settings (return to factory default)
Command Reference
Text & Positioning
| Command | Description |
|---|---|
<Fn> | Select font (n = 1–13, 16) |
<HWh,w> | Set font height/width scaling |
<WHw,h> | Reset font width/height (use <WH1,1>) |
<BSw,h> | Modify character box size (width, height) |
<SDn> | Scale down font by factor n |
<RCr,c> | Position at row, column (in dots) |
<CR> | Carriage return / next line |
<NR> | No rotation (0°) |
<RR> | Rotate right (90°) |
<RU> | Rotate upside down (180°) |
<RL> | Rotate left (270°) |
<EI> | Enable inverse printing (white on black) |
<DI> | Disable inverse printing |
<ECM> | Enable extended character mode (>127) |
<ECMD> | Disable extended character mode |
Barcodes
| Command | Description |
|---|---|
<Xn> | Barcode expansion factor (1–9) |
<Yn> | Barcode ratio adjust (3:1 or 5:2) |
<BI> | Barcode interpretation (human-readable text) |
<QRVn> | QR code version (2–15) |
<QRn> | QR module size (3–16) |
Lines, Boxes & Graphics
| Command | Description |
|---|---|
<LTn> | Line thickness in dots |
<HXn> | Horizontal line of n dots |
<VXn> | Vertical line of n dots |
<DBr,c> | Draw box r dots tall, c dots wide |
<Gn> | Binary graphics mode (n bytes) |
<gn> | ASCII graphics mode (n hex chars) |
Logo & File Operations
| Command | Description |
|---|---|
<SPr,c> | Set starting point for logo (row, col) |
<RLn> | Print resident (factory) logo ID n |
<PLn> | Print downloaded logo ID n |
<FIn> | Assign file ID for download |
<PF> | Permanent file mode |
<TF> | Temporary file mode |
<DFn> | Delete file ID n |
Print Control
| Command | Description |
|---|---|
<q> | End of ticket (print/cut behavior is firmware-specific) |
<FF> | Form feed (print and cut) |
<REPn> | Print n additional copies |
<PH> | Print and hold image for reuse |
<CB> | Clear buffer and reset fonts |
<LVn> | Print intensity (-5 to +5) |
<PTC> | Print ticket count on ticket |
<SR> | Request printer status |
<PROM> | Request firmware/count info |
<DSA> | Report available memory |
<DIAG> | Enter diagnostic mode |
Further Reading
- BOCA Systems FGL46 Programming Guide (PDF) — Official reference for the latest FGL command set
- BOCA Systems, Inc. — Manufacturer of FGL-compatible thermal printers