escper 1.0.4 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README +3 -7
- data/escper.gemspec +0 -2
- data/lib/escper.rb +10 -67
- data/lib/escper/asciifier.rb +42 -0
- data/lib/escper/codepages.yml +26 -0
- data/lib/escper/image.rb +67 -0
- data/lib/escper/printer.rb +158 -0
- data/lib/escper/version.rb +1 -1
- metadata +8 -20
data/README
CHANGED
@@ -13,22 +13,18 @@ Or if you read the image from an image upload form in Rails, do this:
|
|
13
13
|
|
14
14
|
Escper::Image.new(data.read, :blob).to_s
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
Or if you have an Magick::Image already loaded, call it like this:
|
19
|
-
|
20
|
-
Escper::Image.new(magickobject, :object)
|
16
|
+
where "data" is a variable containing the image data of a multipart HTML form.
|
21
17
|
|
22
18
|
|
23
19
|
For optimal visual results, image.png should previously be converted to an indexed, black and white 1-bit palette image. In Gimp, click on "Image -> Mode -> Indexed..." and select "Use black and white (1-bit) palette". For dithering, choose "Floyd-Steinberg (reduced color bleeding)". The image size depends on the resolution of the printer.
|
24
20
|
|
25
21
|
To send an image directly to a thermal receipt printer:
|
26
22
|
|
27
|
-
File.open('/dev/usb/lp0','w') { |f| f.write Escper::Image.new('image.png'
|
23
|
+
File.open('/dev/usb/lp0','w') { |f| f.write Escper::Image.new('image.png').to_s }
|
28
24
|
|
29
25
|
== Licence
|
30
26
|
|
31
|
-
Copyright (C) 2011-2012 Michael Franzl <
|
27
|
+
Copyright (C) 2011-2012 Michael Franzl <michael@billgastro.com>
|
32
28
|
|
33
29
|
This program is free software: you can redistribute it and/or modify
|
34
30
|
it under the terms of the GNU Affero General Public License as
|
data/escper.gemspec
CHANGED
data/lib/escper.rb
CHANGED
@@ -1,73 +1,16 @@
|
|
1
|
-
# Escper -- Convert an image to ESCPOS commands for thermal printers
|
2
|
-
# Copyright (C) 2011-2012 Michael Franzl <michael@billgastro.com>
|
3
|
-
#
|
4
|
-
# This program is free software: you can redistribute it and/or modify
|
5
|
-
# it under the terms of the GNU Affero General Public License as
|
6
|
-
# published by the Free Software Foundation, either version 3 of the
|
7
|
-
# License, or (at your option) any later version.
|
8
|
-
#
|
9
|
-
# This program is distributed in the hope that it will be useful,
|
10
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
-
# GNU Affero General Public License for more details.
|
13
|
-
#
|
14
|
-
# You should have received a copy of the GNU Affero General Public License
|
15
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
-
|
17
1
|
require 'RMagick'
|
18
2
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
@image = Magick::Image.read(source).first
|
24
|
-
elsif type == :blob
|
25
|
-
@image = Magick::Image.from_blob(source).first
|
26
|
-
elsif type == :object
|
27
|
-
@image = source
|
28
|
-
end
|
29
|
-
@x = (@image.columns / 8.0).round
|
30
|
-
@y = (@image.rows / 8.0).round
|
31
|
-
@x = 1 if @x.zero?
|
32
|
-
@y = 1 if @y.zero?
|
33
|
-
end
|
34
|
-
|
35
|
-
def convert
|
36
|
-
@image = @image.quantize 2, Magick::GRAYColorspace
|
37
|
-
end
|
38
|
-
|
39
|
-
def crop
|
40
|
-
@image = @image.extent @x * 8, @y * 8
|
41
|
-
end
|
3
|
+
dir = File.dirname(__FILE__)
|
4
|
+
Dir[File.expand_path("#{dir}/escper/*.rb")].uniq.each do |file|
|
5
|
+
require file
|
6
|
+
end
|
42
7
|
|
43
|
-
|
44
|
-
|
45
|
-
end
|
8
|
+
module Escper
|
9
|
+
mattr_accessor :codepage_file
|
46
10
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
bits = []
|
52
|
-
mask = 0x80
|
53
|
-
i = 0
|
54
|
-
temp = 0
|
55
|
-
(@x * @y * 8 * 3 * 8).times do |j|
|
56
|
-
next unless (j % 3).zero?
|
57
|
-
temp |= mask if colorarray[j] == 0 # put 1 in place if black
|
58
|
-
mask = mask >> 1
|
59
|
-
i += 3
|
60
|
-
if i == 24
|
61
|
-
bits << temp
|
62
|
-
mask = 0x80
|
63
|
-
i = 0
|
64
|
-
temp = 0
|
65
|
-
end
|
66
|
-
end
|
67
|
-
result = bits.collect{ |b| b.chr }.join
|
68
|
-
escpos = "\x1D\x76\x30\x00#{@x.chr}\x00#{(@y*8).chr}\x00#{ result }"
|
69
|
-
escpos.force_encoding('ISO-8859-15')
|
70
|
-
return escpos
|
71
|
-
end
|
11
|
+
@@codepage_file = File.join(File.dirname(__FILE__), 'escper', 'codepages.yml')
|
12
|
+
|
13
|
+
def self.setup
|
14
|
+
yield self
|
72
15
|
end
|
73
16
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Escper
|
2
|
+
class Asciifier
|
3
|
+
def initialize(codepage)
|
4
|
+
@codepage = codepage
|
5
|
+
@codepage_lookup_yaml = YAML::load(File.read(Escper.codepage_file))
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.all_chars
|
9
|
+
out = "\e@" # Initialize Printer
|
10
|
+
out.encode!('ascii-8bit')
|
11
|
+
20.upto(255) { |i| out += i.to_s(16) + i.chr + ' ' }
|
12
|
+
out += "\n\n\n\n\n\n" +
|
13
|
+
"\x1D\x56\x00" # paper cut
|
14
|
+
return out
|
15
|
+
end
|
16
|
+
|
17
|
+
def process(text)
|
18
|
+
output = ''
|
19
|
+
output.encode 'ASCII-8BIT'
|
20
|
+
0.upto(text.length - 1) do |i|
|
21
|
+
char_utf8 = text[i]
|
22
|
+
char_ascii = text[i].force_encoding('ASCII-8BIT')
|
23
|
+
if char_ascii.length == 1
|
24
|
+
output += char_ascii
|
25
|
+
else
|
26
|
+
output += codepage_lookup(char_utf8)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
return output
|
30
|
+
end
|
31
|
+
|
32
|
+
def codepage_lookup(char)
|
33
|
+
output = @codepage_lookup_yaml[@codepage][char]
|
34
|
+
if output
|
35
|
+
output = output.chr
|
36
|
+
else
|
37
|
+
output = '?'
|
38
|
+
end
|
39
|
+
return output.force_encoding('ASCII-8BIT')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
---
|
2
|
+
# Codepage 0 corresponds to the codepage 0 of Metapace T family of thermal printers
|
3
|
+
0:
|
4
|
+
ä: 0x84
|
5
|
+
ü: 0x81
|
6
|
+
ö: 0x94
|
7
|
+
Ä: 0x8E
|
8
|
+
Ü: 0x9A
|
9
|
+
Ö: 0x99
|
10
|
+
é: 0x82
|
11
|
+
è: 0x8A
|
12
|
+
ú: 0xA3
|
13
|
+
ù: 0x97
|
14
|
+
á: 0xA0
|
15
|
+
à: 0x85
|
16
|
+
í: 0xA1
|
17
|
+
ì: 0x8D
|
18
|
+
ó: 0xA2
|
19
|
+
ò: 0x95
|
20
|
+
â: 0x83
|
21
|
+
ê: 0x88
|
22
|
+
î: 0x8C
|
23
|
+
ô: 0x93
|
24
|
+
û: 0x96
|
25
|
+
ñ: 0xA4
|
26
|
+
ß: 0xE1
|
data/lib/escper/image.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module Escper
|
2
|
+
class Image
|
3
|
+
def initialize(data, type)
|
4
|
+
if type == :file
|
5
|
+
@image = convert(Magick::Image.read(data).first)
|
6
|
+
elsif type == :blob
|
7
|
+
@image = convert(Magick::Image.from_blob(data).first)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def convert(img=nil)
|
12
|
+
if img.nil? and @image
|
13
|
+
@image = @image.quantize 2, Magick::GRAYColorspace
|
14
|
+
@image = crop(@image)
|
15
|
+
return @image
|
16
|
+
else
|
17
|
+
img = img.quantize 2, Magick::GRAYColorspace
|
18
|
+
img = crop(img)
|
19
|
+
return img
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def crop(image)
|
24
|
+
@x = (image.columns / 8.0).round
|
25
|
+
@y = (image.rows / 8.0).round
|
26
|
+
@x = 1 if @x == 0
|
27
|
+
@y = 1 if @y == 0
|
28
|
+
image = image.extent @x * 8, @y * 8
|
29
|
+
return image
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_a
|
33
|
+
colorarray = @image.export_pixels
|
34
|
+
return colorarray
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
colorarray = self.to_a
|
39
|
+
bits = []
|
40
|
+
mask = 0x80
|
41
|
+
i = 0
|
42
|
+
temp = 0
|
43
|
+
(@x * @y * 8 * 3 * 8).times do |j|
|
44
|
+
next unless (j % 3).zero?
|
45
|
+
temp |= mask if colorarray[j] == 0 # put 1 in place if black
|
46
|
+
mask = mask >> 1 # shift mask
|
47
|
+
i += 3
|
48
|
+
if i == 24
|
49
|
+
bits << temp
|
50
|
+
mask = 0x80
|
51
|
+
i = 0
|
52
|
+
temp = 0
|
53
|
+
end
|
54
|
+
end
|
55
|
+
result = bits.collect{ |b| b.chr }.join
|
56
|
+
escpos = ''
|
57
|
+
escpos.encode! 'ASCII-8BIT'
|
58
|
+
escpos += "\x1D\x76\x30\x00" +
|
59
|
+
@x.chr +
|
60
|
+
"\x00" +
|
61
|
+
(@y*8).chr +
|
62
|
+
"\x00" +
|
63
|
+
result
|
64
|
+
return escpos
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module Escper
|
2
|
+
class Printer
|
3
|
+
# mode can be local or sass
|
4
|
+
# vendor_printers can either be a single VendorPrinter object, or an Array of VendorPrinter objects, or an ActiveRecord Relation containing VendorPrinter objects.
|
5
|
+
def initialize(mode, vendor_printers = nil)
|
6
|
+
@mode = mode
|
7
|
+
@open_printers = Hash.new
|
8
|
+
@codepages_lookup = YAML::load(File.read(Escper.codepage_file))
|
9
|
+
if vendor_printers.kind_of?(ActiveRecord::Relation) or vendor_printers.kind_of?(Array)
|
10
|
+
@vendor_printers = vendor_printers
|
11
|
+
elsif vendor_printers.kind_of? VendorPrinter
|
12
|
+
@vendor_printers = [vendor_printers]
|
13
|
+
else
|
14
|
+
# If no available VendorPrinters are initialized, create a set of temporary VendorPrinters with usual device paths.
|
15
|
+
puts "No VendorPrinters specified. Creating a set of temporary printers with usual device paths"
|
16
|
+
paths = ['/dev/ttyUSB0', '/dev/ttyUSB1', '/dev/ttyUSB2', '/dev/usb/lp0', '/dev/usb/lp1', '/dev/usb/lp2', '/dev/salor-hospitality-front', '/dev/salor-hospitality-top', '/dev/salor-hospitality-back-top-left', '/dev/salor-hospitality-back-top-right', '/dev/salor-hospitality-back-bottom-left', '/dev/salor-hospitality-back-bottom-right']
|
17
|
+
@vendor_printers = Array.new
|
18
|
+
paths.size.times do |i|
|
19
|
+
@vendor_printers << VendorPrinter.new(:name => paths[i].gsub(/^.*\//,''), :path => paths[i], :copies => 1, :codepage => 0)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def print(printer_id, text, raw_text_insertations={})
|
25
|
+
return if @open_printers == {}
|
26
|
+
ActiveRecord::Base.logger.info "[PRINTING]============"
|
27
|
+
ActiveRecord::Base.logger.info "[PRINTING]PRINTING..."
|
28
|
+
printer = @open_printers[printer_id]
|
29
|
+
raise 'Mismatch between open_printers and printer_id' if printer.nil?
|
30
|
+
|
31
|
+
codepage = printer[:codepage]
|
32
|
+
codepage ||= 0
|
33
|
+
output_text = merge_texts(text, raw_text_insertations, codepage)
|
34
|
+
|
35
|
+
ActiveRecord::Base.logger.info "[PRINTING] Printing on #{ printer[:name] } @ #{ printer[:device].inspect }."
|
36
|
+
bytes_written = nil
|
37
|
+
printer[:copies].times do |i|
|
38
|
+
# The method .write works both for SerialPort object and File object, so we don't have to distinguish here.
|
39
|
+
bytes_written = @open_printers[printer_id][:device].write output_text
|
40
|
+
ActiveRecord::Base.logger.info "[PRINTING]ERROR: Byte count mismatch: sent #{text.length} written #{bytes_written}" unless output_text.length == bytes_written
|
41
|
+
end
|
42
|
+
# The method .flush works both for SerialPort object and File object, so we don't have to distinguish here. It is not really neccessary, since the close method will theoretically flush also.
|
43
|
+
@open_printers[printer_id][:device].flush
|
44
|
+
return bytes_written, output_text
|
45
|
+
end
|
46
|
+
|
47
|
+
def merge_texts(text, raw_text_insertations, codepage)
|
48
|
+
asciifier = Escper::Asciifier.new(codepage)
|
49
|
+
asciified_text = asciifier.process(text)
|
50
|
+
raw_text_insertations.each do |key, value|
|
51
|
+
markup = "{::escper}#{key.to_s}{:/}".encode('ASCII-8BIT')
|
52
|
+
asciified_text.gsub!(markup, value)
|
53
|
+
end
|
54
|
+
return asciified_text
|
55
|
+
end
|
56
|
+
|
57
|
+
def identify(chartest=nil)
|
58
|
+
ActiveRecord::Base.logger.info "[PRINTING]============"
|
59
|
+
ActiveRecord::Base.logger.info "[PRINTING]TESTING Printers..."
|
60
|
+
open
|
61
|
+
@open_printers.each do |id, value|
|
62
|
+
init = "\e@"
|
63
|
+
cut = "\n\n\n\n\n\n" + "\x1D\x56\x00"
|
64
|
+
testtext =
|
65
|
+
"\e!\x38" + # double tall, double wide, bold
|
66
|
+
"#{ I18n.t :printing_test }\r\n" +
|
67
|
+
"\e!\x00" + # Font A
|
68
|
+
"#{ value[:name] }\r\n" +
|
69
|
+
"#{ value[:device].inspect }"
|
70
|
+
|
71
|
+
ActiveRecord::Base.logger.info "[PRINTING] Testing #{value[:device].inspect }"
|
72
|
+
if chartest
|
73
|
+
print(id, init + self.all_chars + cut)
|
74
|
+
else
|
75
|
+
ascifiier = Escper::Asciifier.new(value[:codepage])
|
76
|
+
print(id, init + ascifiier.process(testtext) + cut)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
close
|
80
|
+
end
|
81
|
+
|
82
|
+
def open
|
83
|
+
ActiveRecord::Base.logger.info "[PRINTING]============"
|
84
|
+
ActiveRecord::Base.logger.info "[PRINTING]OPEN Printers..."
|
85
|
+
@vendor_printers.size.times do |i|
|
86
|
+
p = @vendor_printers[i]
|
87
|
+
name = p.name
|
88
|
+
path = p.path
|
89
|
+
if @mode != 'local' and SalorHospitality::Application::SH_DEBIAN_SITEID != 'none'
|
90
|
+
path = File.join('/', 'var', 'lib', 'salor-hospitality', SalorHospitality::Application::SH_DEBIAN_SITEID, 'public', 'uploads', "#{path}.salor")
|
91
|
+
end
|
92
|
+
ActiveRecord::Base.logger.info "[PRINTING] Trying to open #{ name } @ #{ path } ..."
|
93
|
+
pid = p.id ? p.id : i
|
94
|
+
begin
|
95
|
+
printer = SerialPort.new path, 9600
|
96
|
+
@open_printers.merge! pid => { :name => name, :path => path, :copies => p.copies, :device => printer }
|
97
|
+
ActiveRecord::Base.logger.info "[PRINTING] Success for SerialPort: #{ printer.inspect }"
|
98
|
+
next
|
99
|
+
rescue Exception => e
|
100
|
+
ActiveRecord::Base.logger.info "[PRINTING] Failed to open as SerialPort: #{ e.inspect }"
|
101
|
+
end
|
102
|
+
|
103
|
+
begin
|
104
|
+
printer = File.open path, 'wb'
|
105
|
+
@open_printers.merge! pid => { :name => name, :path => path, :copies => p.copies, :device => printer }
|
106
|
+
ActiveRecord::Base.logger.info "[PRINTING] Success for File: #{ printer.inspect }"
|
107
|
+
next
|
108
|
+
rescue Errno::EBUSY
|
109
|
+
ActiveRecord::Base.logger.info "[PRINTING] The File #{ path } is already open."
|
110
|
+
ActiveRecord::Base.logger.info "[PRINTING] Trying to reuse already opened printers."
|
111
|
+
previously_opened_printers = @open_printers.clone
|
112
|
+
previously_opened_printers.each do |key, val|
|
113
|
+
ActiveRecord::Base.logger.info "[PRINTING] Trying to reuse already opened File #{ key }: #{ val.inspect }"
|
114
|
+
if val[:path] == p[:path] and val[:device].class == File
|
115
|
+
ActiveRecord::Base.logger.info "[PRINTING] Reused."
|
116
|
+
@open_printers.merge! pid => { :name => name, :path => path, :copies => p.copies, :device => val[:device] }
|
117
|
+
break
|
118
|
+
end
|
119
|
+
end
|
120
|
+
unless @open_printers.has_key? p.id
|
121
|
+
if SalorHospitality::Application::SH_DEBIAN_SITEID == 'none'
|
122
|
+
path = File.join(Rails.root, 'tmp')
|
123
|
+
else
|
124
|
+
path = File.join('/', 'var', 'lib', 'salor-hospitality', SalorHospitality::Application::SH_DEBIAN_SITEID)
|
125
|
+
end
|
126
|
+
printer = File.open(File.join(path, "#{ p.id }-#{ name }-fallback-busy.salor"), 'wb')
|
127
|
+
@open_printers.merge! pid => { :name => name, :path => path, :copies => p.copies, :device => printer }
|
128
|
+
ActiveRecord::Base.logger.info "[PRINTING] Failed to open as either SerialPort or USB File and resource IS busy. This should not have happened. Created #{ printer.inspect } instead."
|
129
|
+
end
|
130
|
+
next
|
131
|
+
rescue Exception => e
|
132
|
+
if SalorHospitality::Application::SH_DEBIAN_SITEID == 'none'
|
133
|
+
path = File.join(Rails.root, 'tmp')
|
134
|
+
else
|
135
|
+
path = File.join('/', 'var', 'lib', 'salor-hospitality', SalorHospitality::Application::SH_DEBIAN_SITEID)
|
136
|
+
end
|
137
|
+
printer = File.open(File.join(path, "#{ p.id }-#{ name }-fallback-notbusy.salor"), 'wb')
|
138
|
+
@open_printers.merge! pid => { :name => name, :path => path, :copies => p.copies, :device => printer }
|
139
|
+
ActiveRecord::Base.logger.info "[PRINTING] Failed to open as either SerialPort or USB File and resource is NOT busy. Created #{ printer.inspect } instead."
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def close
|
145
|
+
ActiveRecord::Base.logger.info "[PRINTING]============"
|
146
|
+
ActiveRecord::Base.logger.info "[PRINTING]CLOSING Printers..."
|
147
|
+
@open_printers.each do |key, value|
|
148
|
+
begin
|
149
|
+
value[:device].close
|
150
|
+
ActiveRecord::Base.logger.info "[PRINTING] Closing #{ value[:name] } @ #{ value[:device].inspect }"
|
151
|
+
@open_printers.delete(key)
|
152
|
+
rescue Exception => e
|
153
|
+
ActiveRecord::Base.logger.info "[PRINTING] Error during closing of #{ value[:device] }: #{ e.inspect }"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
data/lib/escper/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: escper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,24 +9,8 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
13
|
-
dependencies:
|
14
|
-
- !ruby/object:Gem::Dependency
|
15
|
-
name: rmagick
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
|
-
requirements:
|
19
|
-
- - ! '>='
|
20
|
-
- !ruby/object:Gem::Version
|
21
|
-
version: '0'
|
22
|
-
type: :runtime
|
23
|
-
prerelease: false
|
24
|
-
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
|
-
requirements:
|
27
|
-
- - ! '>='
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: '0'
|
12
|
+
date: 2012-11-23 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
30
14
|
description: ''
|
31
15
|
email:
|
32
16
|
- office@michaelfranzl.com
|
@@ -40,6 +24,10 @@ files:
|
|
40
24
|
- Rakefile
|
41
25
|
- escper.gemspec
|
42
26
|
- lib/escper.rb
|
27
|
+
- lib/escper/asciifier.rb
|
28
|
+
- lib/escper/codepages.yml
|
29
|
+
- lib/escper/image.rb
|
30
|
+
- lib/escper/printer.rb
|
43
31
|
- lib/escper/test.png
|
44
32
|
- lib/escper/version.rb
|
45
33
|
homepage: http://michaelfranzl.com
|
@@ -62,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
50
|
version: '0'
|
63
51
|
requirements: []
|
64
52
|
rubyforge_project: escper
|
65
|
-
rubygems_version: 1.8.
|
53
|
+
rubygems_version: 1.8.11
|
66
54
|
signing_key:
|
67
55
|
specification_version: 3
|
68
56
|
summary: Converts bitmaps to the ESC/POS receipt printer command
|