From 2ea45b5f21d5158df6ee397eb72dfa4d73eeb609 Mon Sep 17 00:00:00 2001 From: Roman Priesol Date: Sat, 12 Sep 2026 00:22:35 +0200 Subject: [PATCH] Initial commit: ZPL-native PDF printing, drop CUPS dependency --- .gitignore | 3 + Gemfile | 10 +++ Gemfile.lock | 56 ++++++++++++++++ app.rb | 138 ++++++++++++++++++++++++++++++++++++++ config.json | 32 +++++++++ config.ru | 6 ++ gemset.gems | 127 +++++++++++++++++++++++++++++++++++ manifest.yml | 7 ++ public/files/.gitkeep | 0 test-label.pdf | Bin 0 -> 6097 bytes tools/build_test_label.rb | 115 +++++++++++++++++++++++++++++++ views/index.erb | 61 +++++++++++++++++ views/layout.erb | 56 ++++++++++++++++ views/login_form.erb | 4 ++ views/upload.erb | 11 +++ 15 files changed, 626 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 app.rb create mode 100644 config.json create mode 100644 config.ru create mode 100644 gemset.gems create mode 100644 manifest.yml create mode 100644 public/files/.gitkeep create mode 100644 test-label.pdf create mode 100644 tools/build_test_label.rb create mode 100644 views/index.erb create mode 100644 views/layout.erb create mode 100644 views/login_form.erb create mode 100644 views/upload.erb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b573bec --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.session_secret +public/files/* +!public/files/.gitkeep diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..1108559 --- /dev/null +++ b/Gemfile @@ -0,0 +1,10 @@ +source 'https://rubygems.org' + +gem 'sinatra' +gem 'sinatra-contrib' +gem 'puma' +gem 'rqrcode' +gem 'logger' +gem 'ostruct' + +gem 'rackup', "~> 2.1" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..6701673 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,56 @@ +GEM + remote: https://rubygems.org/ + specs: + base64 (0.3.0) + chunky_png (1.4.0) + logger (1.7.0) + multi_json (1.21.1) + mustermann (3.1.1) + nio4r (2.7.5) + ostruct (0.6.3) + puma (8.0.2) + nio4r (~> 2.0) + rack (3.2.6) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rackup (2.3.1) + rack (>= 3) + rqrcode (3.2.0) + chunky_png (~> 1.0) + rqrcode_core (~> 2.0) + rqrcode_core (2.1.0) + sinatra (4.2.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.2.1) + rack-session (>= 2.0.0, < 3) + tilt (~> 2.0) + sinatra-contrib (4.2.1) + multi_json (>= 0.0.2) + mustermann (~> 3.0) + rack-protection (= 4.2.1) + sinatra (= 4.2.1) + tilt (~> 2.0) + tilt (2.7.0) + +PLATFORMS + aarch64-linux + armv7l-linux-eabihf + +DEPENDENCIES + logger + ostruct + puma + rackup (~> 2.1) + rqrcode + sinatra + sinatra-contrib + +BUNDLED WITH + 4.0.14 diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..685db40 --- /dev/null +++ b/app.rb @@ -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 diff --git a/config.json b/config.json new file mode 100644 index 0000000..2884ebe --- /dev/null +++ b/config.json @@ -0,0 +1,32 @@ +{ + "printers":[ + { + "uri": "file:///dev/usb/lp0", + "model": "E4204B", + "dimensions": "100x150", + "name": "Datamax-ONeil_E4204B_Mark_III", + "driver": "zpl" + }, + { + "uri": "file:///dev/usb/lp1", + "model": "QL-700", + "dimensions": "62", + "name": "Brother QL-700", + "driver": "brother_ql" + }, + { + "uri": "file:///dev/usb/lp2", + "model": "QL-1100", + "dimensions": "102", + "name": "Brother QL-1100", + "driver": "brother_ql" + }, + { + "uri": "file:///dev/usb/lp3", + "model": "QL-560", + "dimensions": "12+19", + "name": "Brother QL-560", + "driver": "brother_ql" + } + ] +} diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..8fd417a --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +require 'bundler' + +Bundler.require + +require './app' +run Sinatra::Application diff --git a/gemset.gems b/gemset.gems new file mode 100644 index 0000000..61bdffb --- /dev/null +++ b/gemset.gems @@ -0,0 +1,127 @@ +# gemset.gem.gems generated gem export file. Note that any env variable settings will be missing. Append these after using a ';' field separator + +error_highlight -v0.3.0 +did_you_mean -v1.6.1 +abbrev -v0.1.0 +awesome_print -v1.9.2 +base64 -v0.1.1 +benchmark -v0.2.0 +bigdecimal -v3.1.2 +bigdecimal -v3.1.1 +bundler -v2.3.10 +bundler -v2.3.6 +cgi -v0.3.2 +cgi -v0.3.1 +chunky_png -v1.4.0 +csv -v3.2.2 +cupsffi -v0.1.9 +date -v3.2.2 +delegate -v0.2.0 +digest -v3.1.0 +domain_name -v0.5.20190701 +drb -v2.1.0 +english -v0.7.1 +erb -v2.2.3 +etc -v1.3.0 +fcntl -v1.0.1 +ffi -v1.15.5 +fiddle -v1.1.0 +fileutils -v1.6.0 +find -v0.1.1 +forwardable -v1.3.2 +getoptlong -v0.1.1 +http-accept -v2.1.1 +http-accept -v1.7.0 +http-cookie -v1.0.4 +io-console -v0.5.11 +io-console -v0.5.10 +io-nonblock -v0.1.0 +io-wait -v0.2.1 +ipaddr -v1.2.4 +ipaddr -v1.2.3 +irb -v1.4.1 +json -v2.6.1 +logger -v1.5.1 +logger -v1.5.0 +mime-types -v3.4.1 +mime-types-data -v3.2022.0105 +multi_json -v1.15.0 +mustermann -v2.0.2 +mustermann -v1.1.1 +mutex_m -v0.1.1 +net-http -v0.2.0 +net-imap -v0.2.3 +net-protocol -v0.1.2 +netrc -v0.11.0 +nio4r -v2.5.8 +nkf -v0.1.1 +observer -v0.1.1 +open-uri -v0.2.0 +open3 -v0.1.1 +openssl -v3.0.0 +optparse -v0.2.0 +ostruct -v0.5.4 +ostruct -v0.5.3 +ostruct -v0.5.2 +pathname -v0.2.0 +pp -v0.3.0 +prettyprint -v0.1.1 +pstore -v0.1.1 +psych -v4.0.3 +puma -v5.6.5 +puma -v5.6.2 +puma -v5.6.1 +racc -v1.6.0 +rack -v2.2.4 +rack -v2.2.3 +rack-protection -v2.2.2 +rack-protection -v2.2.0 +rack-protection -v2.1.0 +rbs -v2.2.2 +rbs -v2.1.0 +rdoc -v6.4.0 +readline -v0.0.3 +readline-ext -v0.1.4 +reline -v0.3.1 +reline -v0.3.0 +resolv -v0.2.1 +resolv-replace -v0.1.0 +rest-client -v2.1.0 +rinda -v0.1.1 +rqrcode -v2.1.2 +rqrcode -v2.1.1 +rqrcode -v2.1.0 +rqrcode_core -v1.2.0 +ruby2_keywords -v0.0.5 +rubygems-update -v3.3.10 +rubygems-update -v3.3.6 +securerandom -v0.2.0 +securerandom -v0.1.1 +set -v1.0.2 +shellwords -v0.1.0 +sinatra -v2.2.2 +sinatra -v2.2.0 +sinatra -v2.1.0 +sinatra-contrib -v2.2.2 +sinatra-contrib -v2.2.0 +sinatra-contrib -v2.1.0 +singleton -v0.1.1 +stringio -v3.0.1 +strscan -v3.0.1 +syslog -v0.1.0 +tempfile -v0.1.2 +tilt -v2.0.11 +tilt -v2.0.10 +time -v0.2.0 +timeout -v0.2.0 +tmpdir -v0.1.2 +tsort -v0.1.0 +typeprof -v0.21.2 +un -v0.2.0 +unf -v0.1.4 +unf_ext -v0.0.8.1 +unf_ext -v0.0.8 +uri -v0.11.0 +weakref -v0.1.1 +yaml -v0.2.0 +zlib -v2.1.1 diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 0000000..56b6276 --- /dev/null +++ b/manifest.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: bootstrap + memory: 128M + instances: 1 + url: bootstrap.${target-base} + path: . diff --git a/public/files/.gitkeep b/public/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test-label.pdf b/test-label.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c173738ef7eadc7cdfa4fed23f033a8b6ec71ec1 GIT binary patch literal 6097 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 S$~cWH6ro^(?;R8s4E+yW{r&v_ literal 0 HcmV?d00001 diff --git a/tools/build_test_label.rb b/tools/build_test_label.rb new file mode 100644 index 0000000..216a700 --- /dev/null +++ b/tools/build_test_label.rb @@ -0,0 +1,115 @@ +require 'tmpdir' +require 'rqrcode' + +DPI = 203 +SRC_PDF = ARGV[0] || '/home/randy/EPH266757312_adresne_stitky_a6.pdf' +OUT_PS = ARGV[1] || '/tmp/test-label-v2.ps' + +# barcode crop region in the source label, in native pixels at 203dpi +BARCODE_BOX = { x0: 388, y0: 222, x1: 772, y1: 322 } + +def read_pbm(path) + bytes = File.binread(path) + header = /\AP4\s+(?:#[^\n]*\n\s*)*(\d+)\s+(\d+)\s/.match(bytes) + raise "bad pbm #{path}" unless header + + width, height = header[1].to_i, header[2].to_i + bytes_per_row = (width + 7) / 8 + body = bytes.byteslice(header.end(0), bytes_per_row * height) + { width: width, height: height, bytes_per_row: bytes_per_row, body: body } +end + +def pbm_bit(pbm, x, y) + byte = pbm[:body].getbyte(y * pbm[:bytes_per_row] + (x / 8)) + (byte >> (7 - (x % 8))) & 1 +end + +# Packs a [x,y] => 0/1 (1 = black) pixel getter into row-padded, bit-inverted +# (PostScript DeviceGray 1bpc: 0=black) bytes ready for the `image` operator. +def pack_for_ps_image(width, height) + bytes_per_row = (width + 7) / 8 + rows = Array.new(height) { "\x00".b * bytes_per_row } + height.times do |y| + row = rows[y].dup.b + width.times do |x| + next unless yield(x, y) == 1 + + byte_index = x / 8 + bit = 0x80 >> (x % 8) + row.setbyte(byte_index, row.getbyte(byte_index) | bit) + end + rows[y] = row + end + packed = rows.join + inverted = packed.bytes.map { |b| (~b) & 0xFF }.pack('C*') + { width: width, height: height, hex: inverted.unpack1('H*').upcase } +end + +def ps_image_block(x_pt, y_pt, w_pt, h_pt, img) + <<~PS + gsave + #{x_pt} #{y_pt} translate + #{w_pt} #{h_pt} scale + #{img[:width]} #{img[:height]} 1 + [#{img[:width]} 0 0 -#{img[:height]} 0 #{img[:height]}] + { <#{img[:hex]}> } image + grestore + PS +end + +Dir.mktmpdir do |dir| + pbm_path = File.join(dir, 'label.pbm') + system('gs', '-q', '-dNOPAUSE', '-dBATCH', '-dSAFER', + '-sDEVICE=pbmraw', "-r#{DPI}", '-dFirstPage=1', '-dLastPage=1', + "-sOutputFile=#{pbm_path}", SRC_PDF) or raise 'gs failed' + + pbm = read_pbm(pbm_path) + bw = BARCODE_BOX[:x1] - BARCODE_BOX[:x0] + bh = BARCODE_BOX[:y1] - BARCODE_BOX[:y0] + barcode_img = pack_for_ps_image(bw, bh) do |x, y| + pbm_bit(pbm, BARCODE_BOX[:x0] + x, BARCODE_BOX[:y0] + y) + end + + qr = RQRCode::QRCode.new('TEST-LABEL-PRINTSERVER') + modules = qr.modules + quiet = 4 + qsize = modules.size + quiet * 2 + qr_img = pack_for_ps_image(qsize, qsize) do |x, y| + mx, my = x - quiet, y - quiet + next 0 if mx.negative? || my.negative? || mx >= modules.size || my >= modules.size + + modules[my][mx] ? 1 : 0 + end + + # physical sizes on the A6 (298x420pt) page + barcode_pt_w = 220 + barcode_pt_h = barcode_pt_w * bh / bw + qr_pt = 100 + + ps = <<~PS + %!PS + << /PageSize [298 420] >> setpagedevice + 1 setlinewidth + 10 10 278 400 rectstroke + /Helvetica-Bold findfont 24 scalefont setfont + 30 380 moveto (TEST LABEL) show + /Helvetica findfont 9 scalefont setfont + 30 362 moveto (Generic print-quality test page - safe to reprint anytime) show + /Helvetica-Bold findfont 13 scalefont setfont + 30 330 moveto (Odosielatel: Test Sender, BA) show + 30 312 moveto (Adresat: Test Recipient, KE) show + /Helvetica findfont 8 scalefont setfont + 30 292 moveto (Fine print 8pt: quick brown fox 0123456789) show + #{ps_image_block(30, 210, barcode_pt_w, barcode_pt_h, barcode_img)} + /Helvetica findfont 8 scalefont setfont + 30 198 moveto (1D: reused known-good Code128 from a real courier label) show + #{ps_image_block(30, 60, qr_pt, qr_pt, qr_img)} + /Helvetica findfont 8 scalefont setfont + 140 100 moveto (QR: TEST-LABEL-PRINTSERVER) show + 140 88 moveto (rendered natively via rqrcode) show + showpage + PS + + File.write(OUT_PS, ps) + puts "wrote #{OUT_PS}" +end diff --git a/views/index.erb b/views/index.erb new file mode 100644 index 0000000..2207b5f --- /dev/null +++ b/views/index.erb @@ -0,0 +1,61 @@ +
+
+
Brother Label Designer
+

A web service to print labels on Brother QL label printers.

+ QL-560 + QL-700 + QL-1100 +
+
+
+
+
Sticker Factory
+

Print images, text and cats on a Brother QL label printer

+ Go +
+
+
+
+
Cups
+

The standards-based, open source printing system developed by Apple for iOS®, iPadOS®, and macOS®. CUPS uses IPP Everywhere™ to support printing to local and network printers.

+ Go +
+
+
+
+
Cockpit
+

Cockpit is a web-based graphical interface for servers, intended for everyone

+ Go +
+
+
+
+
ZPL Viewer
+

An useful tool for labels in ZPL format.

+ Go +
+
+
+
+
Creality Sonic Pad
+

3D Printer

+ Fluidd + Mainsail + Moonraker +
+
+
+
+
Bigtreetech Pad 7 CNC
+

CNC

+ Mainsail + Moonraker +
+
+
+
+
Bigtreetech Pad 7 CAN
+

CAN

+ Mainsail +
+
diff --git a/views/layout.erb b/views/layout.erb new file mode 100644 index 0000000..55d886e --- /dev/null +++ b/views/layout.erb @@ -0,0 +1,56 @@ + + + + Print server + + + + + + + +
+ +
+ <% if @alert_success then %> +
<%=@alert_success%>
+ <% end %> + <% if @alert_danger then %> +
<%=@alert_danger%>
+ <% end %> + <% if @alert_warning then %> +
<%=@alert_warning%>
+ <% end %> +<%= yield %> +
+
+ + + + + + diff --git a/views/login_form.erb b/views/login_form.erb new file mode 100644 index 0000000..a765bd0 --- /dev/null +++ b/views/login_form.erb @@ -0,0 +1,4 @@ +
+ + +
diff --git a/views/upload.erb b/views/upload.erb new file mode 100644 index 0000000..64baf5c --- /dev/null +++ b/views/upload.erb @@ -0,0 +1,11 @@ +
+
+ + +
+ +
+ +
+ +