FGL Programming Guide for Thermal Printers

A practical reference for developers working with Friendly Graphics Language | Українська

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:

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:

  1. <F3> — select font 3 (OCR-B, 17×31)
  2. <HW1,1> — set height and width scaling to 1×
  3. <NR> — no rotation
  4. <RC100,50> — position at row 100, column 50 (in dots)
  5. Hello World — the text to print
  6. <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:

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 DPIAt 300 DPI
2.0" × 5.5"400 × 1100600 × 1650
2.125" × 5.5"425 × 1100637 × 1650
2.5" × 5.5"500 × 1100750 × 1650
2.7" × 5.5"540 × 1100810 × 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:

CommandNameCell (200 DPI)Cell (300 DPI)Style
<F1>Font 15×77×8Basic ASCII
<F2>Font 28×1610×18Basic ASCII
<F3>OCR-B17×3120×33OCR-B standard
<F4>OCR-A5×97×11OCR-A standard
<F6>Large OCR-B30×5234×56OCR-B large
<F7>OCR-A Full15×2920×31Full OCR-A set
<F8>Courier20×4020×33Courier
<F9>Small OCR-B13×2013×22OCR-B compact
<F10>Prestige25×4128×41Bold Prestige
<F11>Script25×4926×49Script
<F12>Orator46×9147×91Tall, bold
<F13>Courier Intl20×4020×42Courier + international
<F16>Cyrillic18×3120×33Cyrillic 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:

CommandRotationDescription
<NR>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.

SymbologyTagDelimiterExample Data
Code 39N**CODE39*
Code 128O^^CODE128^
EAN-13E5901234123457
EAN-8U12345670
UPCU401234567893
CodabarCA123456B
Interleaved 2of5F1234567890

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:

CommandPurposeValues
<Xn>Bar expand factor1–9 (2 recommended for readability)
<BI>Barcode interpretationPrints human-readable text below the barcode
Tag letterSymbology typeN, O, E, U, C (see table above)
OrientationPrint directionP = portrait (fence), L = landscape (ladder)
WidthBar width multiplier1–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):

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:

CommandModulesMax 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 + SizeWidth (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
CommandPurpose
<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:

CommandModeData Format
<Gn>Binary graphicsRaw bytes (values 0–255)
<gn>ASCII graphicsHex 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.

Byte value: 0xA5 = 10100101 in binary Bit 7 (MSB) █ ← top dot (black) Bit 6 ░ ← blank Bit 5 █ ← black Bit 4 ░ ← blank Bit 3 ░ ← blank Bit 2 █ ← black Bit 1 ░ ← blank Bit 0 (LSB) █ ← bottom dot (black)

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:

  1. 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.
  2. 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.
  3. 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.
  4. Encode as hex. Convert each byte to a 2-character hex string (zero-padded). Concatenate all columns into one hex string per strip.
  5. Generate FGL commands. For each strip, emit <NR><RCy,x><gn>hexdata where 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.

CommandDescription
<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:

CommandDescription
<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:

FGL provides several commands for controlling how and when tickets are printed, cut, and counted.

Print & Cut Commands

CommandDescription
<q>Print and cut ticket (standard termination)
<FF> / 0CHForm feed — print and cut (alternative to <q>)
1DHPrint without cutting — useful for multi-part tickets
<NOCM>No-cut mode (driver command)
<CM>Cut mode — re-enable cutting (default)

Repeat & Hold

CommandDescription
<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

CommandDescription
<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

CommandDescription
<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:

CommandDescription
<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:

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

CommandDescription
<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:

CommandDescription
<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 cClear 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:

0° (NR) 90° (RR) 180° (RU) 270° (RL) [ORIGIN]─────┐ ┌─────────────┐ ┌─────────────┐ ┌─────[ORIGIN] │ text ──> │ │ │ │ │ │ │ │ │ │ text │ │ <── text │ │ text │ └─────────────┘ │ │ │ │ │ │ │ │ │ v │ └─────[ORIGIN] │ ^ │ [ORIGIN]──────┘ └─────────────┘ originX: left originX: left originX: right originX: right originY: top originY: bottom originY: bottom originY: top

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:

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:

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:

  1. Use stored logos — download once with <PL>, recall on every ticket
  2. Use Print & Hold<PH> keeps the template in buffer, only send variable data for each ticket
  3. Minimize graphics — ASCII graphics (<g>) transmit 2x the data of binary (<G>). Use binary mode if your transport supports 8-bit data
  4. Use <REPn> for identical copies instead of re-sending the ticket

Common Pitfalls

Debugging Workflow

  1. Start with <DIAG> to verify the printer processes commands.
  2. Test positioning with simple text at known coordinates before adding complex elements.
  3. Use <PROM> to verify firmware version — some commands require specific FGL versions (e.g., 2D barcodes need FGL46).
  4. Send <CB> between test jobs to clear the buffer and reset state.
  5. 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:


Command Reference

Text & Positioning

CommandDescription
<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

CommandDescription
<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

CommandDescription
<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

CommandDescription
<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

CommandDescription
<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