Unified /labels page (badges, edit, preview), mnemonic ESC/POS parser for the raw receipt field
deploy / deploy (push) Successful in 10s

- Saved labels/receipts now show on one page instead of split across
  Shipping labels and Receipts, each with a badge for its type/printer,
  Edit (name+content, in place), Print, and Delete.
- ZPL labels get a real visual preview via Labelary's public render API;
  ESC/POS ones show their raw content (no good general preview for that).
- The Receipts free-text field only ever encoded whatever was typed as
  literal printable characters -- pasting mnemonic ESC/POS notation
  (ESC/GS/LF as command names, 0xNN as raw bytes) just printed as text
  instead of being interpreted. Added a small line-based parser for that
  notation (ReceiptBuilder#raw appends already-resolved bytes verbatim),
  verified byte-for-byte against an example with align/bold/barcode/cut.
This commit is contained in:
randy committed 2026-09-13 11:21:46 +02:00
1 parent 66e2e7a5c0
commit 148f7dc248
8 files changed
+180 -51

No files matched your search

+85 -11
View File
@@ -107,16 +107,64 @@ helpers do
receipt.print!(PRINTER_DEVICE_EPSON)
end
# Prints already-formatted ESC/POS text as-is (just the reset + code page
# prefix and a trailing cut) -- for content prepared elsewhere or reprinted
# from the label repository, unlike print_receipt's title/body template.
ESCPOS_MNEMONICS = {
'ESC' => "\x1B", 'GS' => "\x1D", 'FS' => "\x1C", 'DLE' => "\x10",
'LF' => "\x0A", 'CR' => "\x0D", 'HT' => "\x09", 'NUL' => "\x00"
}.freeze
# Parses a line-based mnemonic ESC/POS notation, e.g.:
# ESC a 0x01 # Align: Center
# Hello # literal text
# LF
# GS k 0x03 12345678 0x00
# Each `#...` trailing comment is stripped, then each line's
# whitespace-separated tokens become either a known control mnemonic, a
# raw `0xNN` byte, or literal text (CP852-encoded) -- consecutive literal
# tokens are rejoined with a single space so plain prose still reads
# correctly, while whitespace around control/byte tokens is dropped since
# those are just for human readability, not printer output.
def parse_escpos_notation(text)
bytes = "".b
text.each_line do |line|
prev_was_text = false
line.sub(/#.*/, '').split.each do |token|
if ESCPOS_MNEMONICS.key?(token)
bytes << ESCPOS_MNEMONICS[token]
prev_was_text = false
elsif token =~ /\A0x([0-9A-Fa-f]{2})\z/
bytes << [Regexp.last_match(1)].pack('H2')
prev_was_text = false
else
bytes << ' ' if prev_was_text
bytes << token.encode('CP852', invalid: :replace, undef: :replace, replace: '?')
prev_was_text = true
end
end
end
bytes
end
# Prints already-formatted ESC/POS notation (see parse_escpos_notation) --
# for content prepared elsewhere or reprinted from the label repository,
# unlike print_receipt's title/body template.
def print_raw_escpos(content)
receipt = ReceiptBuilder.new
receipt.align(:left).text(content)
receipt.align(:left).raw(parse_escpos_notation(content))
receipt.feed(3).cut
receipt.print!(PRINTER_DEVICE_EPSON)
end
# Renders ZPL to a PNG via Labelary's public API for an inline preview.
# Fixed at the Datamax label size (4x6in @ 8dpmm ~= 203dpi) -- good enough
# for a thumbnail, not meant to be exact for other sizes.
def render_zpl_preview(zpl)
uri = URI('https://api.labelary.com/v1/printers/8dpmm/labels/4x6/0/')
res = Net::HTTP.post(uri, zpl, 'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8')
return nil unless res.is_a?(Net::HTTPSuccess)
res.body
end
# Transparently reverse-proxies the current request to one of the
# brother_ql_web instances, so all three printers (web UI and API alike)
# are reachable through this single sinatra port instead of their own
@@ -166,7 +214,6 @@ get '/' do
end
get '/upload' do
@labels = LABEL_REPO.all(type: 'zpl')
erb :upload
end
@@ -234,7 +281,6 @@ post '/test-print' do
end
get '/epson' do
@labels = LABEL_REPO.all(type: 'escpos')
erb :epson
end
@@ -264,6 +310,37 @@ post '/epson/raw' do
redirect '/epson'
end
get '/labels' do
@labels = LABEL_REPO.all
erb :labels
end
get '/labels/:id/edit' do
@label = LABEL_REPO.find(params[:id])
halt 404, 'unknown label' unless @label
erb :label_edit
end
post '/labels/:id' do
label = LABEL_REPO.update(params[:id], name: params[:name].to_s, content: params[:content].to_s)
halt 404, 'unknown label' unless label
flash "Saved '#{label['name']}'", 'success'
redirect '/labels'
end
get '/labels/:id/preview.png' do
label = LABEL_REPO.find(params[:id])
halt 404 unless label && label['type'] == 'zpl'
png = render_zpl_preview(label['content'])
halt 502, 'preview unavailable' unless png
content_type 'image/png'
png
end
post '/labels/:id/print' do
label = LABEL_REPO.find(params[:id])
halt 404, 'unknown label' unless label
@@ -279,16 +356,13 @@ post '/labels/:id/print' do
rescue => e
flash "Print failed (#{e.message})", 'danger'
end
redirect_target = label['type'] == 'zpl' ? '/upload' : '/epson'
redirect redirect_target
redirect '/labels'
end
post '/labels/:id/delete' do
label = LABEL_REPO.find(params[:id])
redirect_target = label && label['type'] == 'zpl' ? '/upload' : '/epson'
LABEL_REPO.delete(params[:id])
flash 'Label deleted', 'success'
redirect redirect_target
redirect '/labels'
end
get '/brother' do