deploy / deploy (push) Successful in 9s
ReceiptBuilder#image implements GS v 0 raster printing (no image library needed for the demo -- a procedural 8px checkerboard proves it end to end). Skipped attempting the printer's real-time clock: unsure the TM-T20II actually has a battery-backed RTC (that's usually an option on higher-end models), not worth guessing at an undocumented command.
124 lines
3.1 KiB
Ruby
124 lines
3.1 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
|
|
def ean13(code)
|
|
@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
|