# /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