Files
labelprint/lib/receipt_builder.rb
T
2026-09-13 12:12:28 +02:00

126 lines
3.3 KiB
Ruby

# Minimal chainable ESC/POS builder for the Epson TM-T20II.
#
# Written instead of pulling in the `escpos` gem: the gem's diacritics
# handling needed the same manual UTF-8->CP852 `.encode` call anyway, and a
# couple of its helpers didn't behave as documented against this printer --
# fewer moving parts doing it directly.
class ReceiptBuilder
INIT = "\x1B\x40".freeze
CP852 = "\x1B\x74\x12".freeze # code page for Slovak diacritics
ALIGN_LEFT = "\x1B\x61\x00".freeze
ALIGN_CENT = "\x1B\x61\x01".freeze
ALIGN_RIGH = "\x1B\x61\x02".freeze
BOLD_ON = "\x1B\x45\x01".freeze
BOLD_OFF = "\x1B\x45\x00".freeze
UNDER_ON = "\x1B\x2D\x01".freeze
UNDER_OFF = "\x1B\x2D\x00".freeze
TXT_NORM = "\x1D\x21\x00".freeze
TXT_BIG = "\x1D\x21\x11".freeze # double height and width
TXT_HIGH = "\x1D\x21\x01".freeze # double height
TXT_WIDE = "\x1D\x21\x10".freeze # double width
CUT = "\x1D\x56\x00".freeze
def initialize
@buffer = "".b
init
end
def init
@buffer << INIT << CP852
self
end
def text(str)
@buffer << str.encode('CP852', invalid: :replace, undef: :replace, replace: '?')
self
end
# Appends already-encoded/raw bytes as-is (e.g. pre-parsed ESC/POS command
# sequences), bypassing the CP852 text encoding `text` applies.
def raw(bytes)
@buffer << bytes
self
end
def align(mode)
@buffer << case mode
when :center then ALIGN_CENT
when :right then ALIGN_RIGH
else ALIGN_LEFT
end
self
end
def bold(enable = true)
@buffer << (enable ? BOLD_ON : BOLD_OFF)
self
end
def underline(enable = true)
@buffer << (enable ? UNDER_ON : UNDER_OFF)
self
end
def size(mode)
@buffer << case mode
when :big then TXT_BIG
when :high then TXT_HIGH
when :wide then TXT_WIDE
else TXT_NORM
end
self
end
# Native ESC/POS QR code
def qr(data, size: 6)
qr_data = data.b
store_len = qr_data.bytesize + 3
@buffer << "\x1D\x28\x6B\x03\x00\x31\x43" << size.chr
@buffer << "\x1D\x28\x6B\x03\x00\x31\x45\x30" # error correction L
@buffer << "\x1D\x28\x6B" << [store_len % 256, store_len / 256].pack('CC') << "\x31\x50\x30" << qr_data
@buffer << "\x1D\x28\x6B\x03\x00\x31\x51\x30" # print
self
end
# Native ESC/POS EAN-13. height is in dots (GS h) -- the printer's default
# (~162 dots) prints noticeably tall; 50 is a shorter, still-scannable size.
def ean13(code, height: 50)
@buffer << "\x1D\x68" << height.chr # GS h n -- set barcode height
@buffer << "\x1D\x6B\x02" << code.b << "\x00"
self
end
# Raster bit image (GS v 0) -- bytes must already be packed 1bpp,
# row-major, (width_px/8 rounded up) bytes per row, MSB-first, 1=black.
def image(width_px, height_px, bytes)
width_bytes = (width_px + 7) / 8
@buffer << "\x1D\x76\x30\x00"
@buffer << [width_bytes % 256, width_bytes / 256, height_px % 256, height_px / 256].pack('C4')
@buffer << bytes
self
end
def feed(lines = 3)
@buffer << ("\n" * lines)
self
end
def cut
@buffer << CUT
self
end
def render
@buffer
end
def print!(device)
File.open(device, 'wb') { |f| f.write(@buffer) }
end
end