Initial commit: ZPL-native PDF printing, drop CUPS dependency
This commit is contained in:
commit
2ea45b5f21
15 files changed
+626
No files matched your search
@@ -0,0 +1,138 @@
|
||||
# /usr/bin/env ruby
|
||||
require 'sinatra'
|
||||
require "sinatra/reloader"
|
||||
require 'tmpdir'
|
||||
|
||||
set :protection, :except => :frame_options
|
||||
set :environment, :production
|
||||
set :bind, "::"
|
||||
set :port, 4567
|
||||
|
||||
if !File.exist?('config.json') || !File.readable?('config.json')
|
||||
raise RuntimeException, 'Could not read config.json'
|
||||
end
|
||||
|
||||
PRINTER_DEVICE = '/dev/usb/lp0'
|
||||
PRINTER_DPI = 203 # DatamaxONeil E4204B Mark III native head resolution
|
||||
PRINTER_DARKNESS = 8 # lower than factory default to stop thermal dot-gain from fusing barcode bars
|
||||
TEST_LABEL_PDF = File.join(File.dirname(__FILE__), 'test-label.pdf')
|
||||
|
||||
configure do
|
||||
enable :static
|
||||
set :session_secret, File.read(File.join(File.dirname(__FILE__), '.session_secret')).strip
|
||||
enable :sessions
|
||||
|
||||
set :views, File.join(File.dirname(__FILE__), 'views')
|
||||
set :public_folder, File.join(File.dirname(__FILE__), 'public')
|
||||
set :files, File.join(settings.public_folder, 'files')
|
||||
set :unallowed_paths, ['.', '..']
|
||||
end
|
||||
|
||||
configure :production do
|
||||
enable :reloader
|
||||
end
|
||||
|
||||
helpers do
|
||||
def flash(message = '', level)
|
||||
case level
|
||||
when "success"
|
||||
session[:alert_success] = message
|
||||
when "danger"
|
||||
session[:alert_danger] = message
|
||||
when "warning"
|
||||
session[:alert_warning] = message
|
||||
end
|
||||
end
|
||||
|
||||
# Renders a PDF to ZPL ^GFA graphic labels at the printer's native DPI,
|
||||
# bypassing CUPS/Gutenprint's raster pipeline which blurs text and 1px-shifts
|
||||
# output compared to native ZPL rendering.
|
||||
def pdf_to_zpl(pdf_path, dpi: PRINTER_DPI)
|
||||
Dir.mktmpdir do |dir|
|
||||
pattern = File.join(dir, 'page-%03d.pbm')
|
||||
ok = system('gs', '-q', '-dNOPAUSE', '-dBATCH', '-dSAFER',
|
||||
'-sDEVICE=pbmraw', "-r#{dpi}",
|
||||
'-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
|
||||
"-sOutputFile=#{pattern}", pdf_path)
|
||||
raise 'ghostscript rendering failed' unless ok
|
||||
|
||||
Dir.glob(File.join(dir, 'page-*.pbm')).sort.map { |f| pbm_to_zpl_label(f) }.join
|
||||
end
|
||||
end
|
||||
|
||||
def pbm_to_zpl_label(pbm_path)
|
||||
bytes = File.binread(pbm_path)
|
||||
header = /\AP4\s+(?:#[^\n]*\n\s*)*(\d+)\s+(\d+)\s/.match(bytes)
|
||||
raise "unreadable PBM header in #{pbm_path}" unless header
|
||||
|
||||
width, height = header[1].to_i, header[2].to_i
|
||||
bytes_per_row = (width + 7) / 8
|
||||
total_bytes = bytes_per_row * height
|
||||
body = bytes.byteslice(header.end(0), total_bytes)
|
||||
hexdata = body.unpack1('H*').upcase
|
||||
|
||||
"^XA\n^MD#{PRINTER_DARKNESS}\n^PW#{width}\n^LL#{height}\n^FO0,0^GFA,#{total_bytes},#{total_bytes},#{bytes_per_row},#{hexdata}\n^XZ\n"
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
@alert_success = session.delete(:alert_success)
|
||||
@alert_danger = session.delete(:alert_danger)
|
||||
@alert_warning = session.delete(:alert_warning)
|
||||
end
|
||||
|
||||
|
||||
get '/' do
|
||||
erb :index
|
||||
end
|
||||
|
||||
get '/upload' do
|
||||
erb :upload
|
||||
end
|
||||
|
||||
post '/upload' do
|
||||
if params[:file]
|
||||
|
||||
tempfile = params[:file][:tempfile]
|
||||
filename = params[:file][:filename]
|
||||
filetype = File.extname filename
|
||||
filepath = File.join(settings.files, filename)
|
||||
|
||||
File.open(filepath, 'wb') do |f|
|
||||
f.write tempfile.read
|
||||
end
|
||||
|
||||
case filetype
|
||||
when ".pdf"
|
||||
begin
|
||||
zpl = pdf_to_zpl(filepath)
|
||||
File.open(PRINTER_DEVICE, 'wb') { |f| f.write(zpl) }
|
||||
flash "PDF: Upload successful (#{filename})", "success"
|
||||
rescue => e
|
||||
flash "PDF: Failed to print (#{e.message})", "danger"
|
||||
end
|
||||
when ".zpl"
|
||||
File.open(PRINTER_DEVICE, 'wb') { |f| f.write(File.binread(filepath)) }
|
||||
flash "ZPL: Upload successful (#{filename})", "success"
|
||||
else
|
||||
flash "#{filetype} is not supported. Only .pdf and .zpl!", "false"
|
||||
end
|
||||
|
||||
File.delete(filepath) if File.exist?(filepath)
|
||||
else
|
||||
flash 'You have to choose a file', "warning"
|
||||
end
|
||||
|
||||
redirect '/upload'
|
||||
end
|
||||
|
||||
post '/test-print' do
|
||||
begin
|
||||
zpl = pdf_to_zpl(TEST_LABEL_PDF)
|
||||
File.open(PRINTER_DEVICE, 'wb') { |f| f.write(zpl) }
|
||||
flash 'Test label sent to printer', 'success'
|
||||
rescue => e
|
||||
flash "Test print failed (#{e.message})", 'danger'
|
||||
end
|
||||
redirect '/upload'
|
||||
end
|
||||
Reference in new issue
Block a user