thermal 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -20
- data/README.md +200 -27
- data/data/db.yml +1987 -0
- data/data/original.yml +1635 -0
- data/lib/thermal/byte_buffer.rb +48 -0
- data/lib/thermal/db/charset.rb +36 -0
- data/lib/thermal/db/cjk_encoding.rb +46 -0
- data/lib/thermal/db/data.rb +77 -0
- data/lib/thermal/db/device.rb +79 -0
- data/lib/thermal/db/encoding.rb +35 -0
- data/lib/thermal/db/loader.rb +65 -0
- data/lib/thermal/db.rb +43 -0
- data/lib/thermal/dsl.rb +28 -0
- data/lib/thermal/escpos/buffer.rb +167 -0
- data/lib/thermal/escpos/cmd.rb +11 -0
- data/lib/thermal/escpos/writer.rb +93 -0
- data/lib/thermal/escpos_star/buffer.rb +38 -0
- data/lib/thermal/escpos_star/writer.rb +17 -0
- data/lib/thermal/printer.rb +56 -21
- data/lib/thermal/profile.rb +71 -0
- data/lib/thermal/stargraphic/capped_byte_buffer.rb +20 -0
- data/lib/thermal/stargraphic/chunked_byte_buffer.rb +62 -0
- data/lib/thermal/stargraphic/writer.rb +318 -0
- data/lib/thermal/starprnt/buffer.rb +46 -0
- data/lib/thermal/starprnt/writer.rb +81 -0
- data/lib/thermal/util.rb +74 -0
- data/lib/thermal/version.rb +5 -3
- data/lib/thermal/writer_base.rb +122 -0
- data/lib/thermal.rb +81 -8
- metadata +59 -57
- data/.gitignore +0 -3
- data/.rspec +0 -2
- data/.travis.yml +0 -6
- data/Gemfile +0 -3
- data/Rakefile +0 -1
- data/lib/devices/btpr880.rb +0 -33
- data/lib/devices/html.rb +0 -14
- data/lib/thermal/parser.rb +0 -30
- data/spec/btpr880_spec.rb +0 -36
- data/spec/fixtures/receipt.html +0 -6
- data/spec/printer_spec.rb +0 -29
- data/spec/spec_helper.rb +0 -3
- data/spec/thermal_spec.rb +0 -7
- data/tasks/console.rake +0 -9
- data/tasks/spec.rake +0 -3
- data/thermal.gemspec +0 -16
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thermal
|
4
|
+
module Escpos
|
5
|
+
class Writer < ::Thermal::WriterBase
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
DEFAULT_QR_CODE_SIZE = 400
|
9
|
+
|
10
|
+
def_delegators :buffer,
|
11
|
+
:write,
|
12
|
+
:write_text,
|
13
|
+
:sequence
|
14
|
+
|
15
|
+
def self.format
|
16
|
+
'escpos'
|
17
|
+
end
|
18
|
+
|
19
|
+
def print(flush: true, output: nil)
|
20
|
+
@buffer = nil
|
21
|
+
super
|
22
|
+
feed(5)
|
23
|
+
cut
|
24
|
+
if output == :byte_array
|
25
|
+
flush ? buffer.flush : buffer.to_a
|
26
|
+
else
|
27
|
+
flush ? buffer.flush_base64 : buffer.to_base64
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def text(str, feed: true, replace: nil, no_cjk: false, **_kwargs)
|
32
|
+
write_text(str, replace: replace, no_cjk: no_cjk)
|
33
|
+
self.feed if feed
|
34
|
+
end
|
35
|
+
|
36
|
+
def hr(style = nil, width: col_width)
|
37
|
+
write_text(hr_char(style) * width)
|
38
|
+
feed
|
39
|
+
end
|
40
|
+
|
41
|
+
def feed(lines = 1)
|
42
|
+
write("\n" * lines)
|
43
|
+
end
|
44
|
+
|
45
|
+
def cut
|
46
|
+
seq = supports?(:paper_partial_cut) ? ::Escpos::PAPER_PARTIAL_CUT : ::Escpos::PAPER_FULL_CUT
|
47
|
+
sequence(seq)
|
48
|
+
end
|
49
|
+
|
50
|
+
def image(path, opts = {})
|
51
|
+
write(::Escpos::Image.new(path, opts).to_escpos)
|
52
|
+
end
|
53
|
+
|
54
|
+
def qr_code(url, size = DEFAULT_QR_CODE_SIZE)
|
55
|
+
image(::RQRCode::QRCode.new(url).as_png(size: size))
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def write_bold!
|
61
|
+
cmd = @bold ? ::Escpos::TXT_BOLD_ON : ::Escpos::TXT_BOLD_OFF
|
62
|
+
sequence(cmd)
|
63
|
+
end
|
64
|
+
|
65
|
+
def write_underline!
|
66
|
+
cmd = if !@underline
|
67
|
+
::Escpos::TXT_UNDERL_OFF
|
68
|
+
elsif @underline.is_a?(Numeric) && @underline >= 2
|
69
|
+
::Escpos::TXT_UNDERL2_ON
|
70
|
+
else
|
71
|
+
::Escpos::TXT_UNDERL_ON
|
72
|
+
end
|
73
|
+
sequence(cmd)
|
74
|
+
end
|
75
|
+
|
76
|
+
def write_align!
|
77
|
+
cmd = case @align
|
78
|
+
when :right
|
79
|
+
::Escpos::TXT_ALIGN_RT
|
80
|
+
when :center
|
81
|
+
::Escpos::TXT_ALIGN_CT
|
82
|
+
else
|
83
|
+
::Escpos::TXT_ALIGN_LT
|
84
|
+
end
|
85
|
+
sequence(cmd)
|
86
|
+
end
|
87
|
+
|
88
|
+
def buffer
|
89
|
+
@buffer ||= ::Thermal::Escpos::Buffer.new(@profile)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thermal
|
4
|
+
module EscposStar
|
5
|
+
class Buffer < ::Thermal::ByteBuffer
|
6
|
+
def initialize
|
7
|
+
super
|
8
|
+
init_buffer!
|
9
|
+
end
|
10
|
+
|
11
|
+
def write_text(text, replace: nil, **_kwargs)
|
12
|
+
text = ::Thermal::Util.normalize_utf8(text, replace: replace)
|
13
|
+
write(text) if text
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def init_buffer!
|
19
|
+
sequence(::Escpos::HW_INIT)
|
20
|
+
set_cjk_off
|
21
|
+
set_charset_zero
|
22
|
+
set_codepage_utf8
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_cjk_off
|
26
|
+
sequence([0x1c, 0x2e, 0x1c, 0x43, 0])
|
27
|
+
end
|
28
|
+
|
29
|
+
def set_charset_zero
|
30
|
+
sequence([0x1b, 0x52, 0])
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_codepage_utf8
|
34
|
+
sequence([0x1b, 0x1d, 0x74, 128])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thermal
|
4
|
+
module EscposStar
|
5
|
+
class Writer < ::Thermal::Escpos::Writer
|
6
|
+
def self.format
|
7
|
+
'escpos_star'
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def buffer
|
13
|
+
@buffer ||= ::Thermal::EscposStar::Buffer.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/thermal/printer.rb
CHANGED
@@ -1,21 +1,56 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thermal
|
4
|
+
class Printer
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
# TODO: align(:symbol)
|
8
|
+
PRINT_METHODS = %i[ text
|
9
|
+
hr
|
10
|
+
bold
|
11
|
+
underline
|
12
|
+
align
|
13
|
+
feed
|
14
|
+
cut
|
15
|
+
image
|
16
|
+
qr_code ].freeze
|
17
|
+
|
18
|
+
WRITERS = [
|
19
|
+
::Thermal::Escpos::Writer,
|
20
|
+
::Thermal::EscposStar::Writer,
|
21
|
+
::Thermal::Stargraphic::Writer,
|
22
|
+
::Thermal::Starprnt::Writer
|
23
|
+
].freeze
|
24
|
+
|
25
|
+
WRITER_MAP = ::Thermal::Util.index_by(WRITERS, &:format).freeze
|
26
|
+
|
27
|
+
attr_reader :device
|
28
|
+
|
29
|
+
# opts includes :cjk_encoding
|
30
|
+
def initialize(device, **opts)
|
31
|
+
@device = device
|
32
|
+
@opts = opts
|
33
|
+
end
|
34
|
+
|
35
|
+
def profile
|
36
|
+
@profile ||= ::Thermal::Profile.new(device, **@opts)
|
37
|
+
end
|
38
|
+
|
39
|
+
def writer
|
40
|
+
@writer ||= WRITER_MAP[format].new(profile)
|
41
|
+
end
|
42
|
+
|
43
|
+
def_delegators :profile,
|
44
|
+
:format,
|
45
|
+
:device_name,
|
46
|
+
:codepages,
|
47
|
+
:charsets,
|
48
|
+
:cjk_encoding,
|
49
|
+
:supports?,
|
50
|
+
:col_width
|
51
|
+
|
52
|
+
def_delegators :writer,
|
53
|
+
:print,
|
54
|
+
*PRINT_METHODS
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thermal
|
4
|
+
class Profile
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
# TODO: some of these methods can be moved to an Escpos::Encoder class.
|
8
|
+
|
9
|
+
# These characters should always use codepage, even if present
|
10
|
+
# in CJK. HR chars have issues with double-printing in CJK encodings
|
11
|
+
# on Epson printers. This has been observed on Japanese (Shift-JIS)
|
12
|
+
# with \u2500 and Simplified Chinese (GB18030) with \u2584.
|
13
|
+
CODEPOINTS_CJK_SKIP = [
|
14
|
+
"\u2500".."\u259F", # box drawing + block elements
|
15
|
+
"\u2660".."\u2667" # card suits
|
16
|
+
].map(&:to_a).flatten.join.each_codepoint.to_a.freeze
|
17
|
+
|
18
|
+
# These characters exist in the Katakana codepage,
|
19
|
+
# but should use CJK encoding if available.
|
20
|
+
CODEPOINTS_CJK_FORCE = '円年月日時分秒〒市区町村人'.each_codepoint.to_a.freeze
|
21
|
+
|
22
|
+
# Wraps a Device object with CJK encoding support.
|
23
|
+
def initialize(device, **opts)
|
24
|
+
@device_key = device.to_s
|
25
|
+
@cjk = ::Thermal::Db.find_cjk_encoding(opts[:cjk_encoding]) || opts[:cjk_encoding]
|
26
|
+
end
|
27
|
+
|
28
|
+
def device_name
|
29
|
+
device.name
|
30
|
+
end
|
31
|
+
|
32
|
+
def cjk_encoding
|
33
|
+
return @cjk_encoding if defined?(@cjk_encoding)
|
34
|
+
|
35
|
+
@cjk_encoding = ::Thermal::Db.cjk_encoding(@cjk)
|
36
|
+
end
|
37
|
+
|
38
|
+
def_delegators :device,
|
39
|
+
:format,
|
40
|
+
:codepages,
|
41
|
+
:charsets,
|
42
|
+
:supports?,
|
43
|
+
:col_width
|
44
|
+
|
45
|
+
def find_encoding(u_codepoint, no_cjk: false)
|
46
|
+
device_encoding = device.find_encoding(u_codepoint)
|
47
|
+
if device_encoding && !codepoint_cjk_force?(u_codepoint)
|
48
|
+
device_encoding
|
49
|
+
elsif !no_cjk && cjk_encoding&.codepoint?(u_codepoint)
|
50
|
+
[:cjk, true].freeze
|
51
|
+
else # rubocop:disable Lint/DuplicateBranch
|
52
|
+
# codepoint_cjk_force failed
|
53
|
+
device_encoding
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def codepoint_cjk_skip?(u_codepoint)
|
58
|
+
CODEPOINTS_CJK_SKIP.include?(u_codepoint)
|
59
|
+
end
|
60
|
+
|
61
|
+
def codepoint_cjk_force?(u_codepoint)
|
62
|
+
CODEPOINTS_CJK_FORCE.include?(u_codepoint)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def device
|
68
|
+
@device ||= ::Thermal::Db.device(@device_key)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thermal
|
4
|
+
module Stargraphic
|
5
|
+
class CappedByteBuffer < ByteBuffer
|
6
|
+
def initialize(max_bytes)
|
7
|
+
super()
|
8
|
+
@max_bytes = max_bytes
|
9
|
+
@byte_counter = 0
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def append(*bytes)
|
15
|
+
@byte_counter += bytes.size
|
16
|
+
super unless @byte_counter >= @max_bytes
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thermal
|
4
|
+
module Stargraphic
|
5
|
+
# TODO: This is not used yet.
|
6
|
+
# It should be refactored as it doesn't transparently extend ByteBuffer
|
7
|
+
class ChunkedByteBuffer
|
8
|
+
def initialize(max_chunk_bytes)
|
9
|
+
@max_chunk_bytes = max_chunk_bytes
|
10
|
+
@byte_counter = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def <<(obj)
|
14
|
+
check_length(obj)
|
15
|
+
append(obj)
|
16
|
+
end
|
17
|
+
|
18
|
+
def chunks
|
19
|
+
@chunks ||= [::Thermal::ByteBuffer.new]
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_base64
|
23
|
+
chunks.map(&:to_base64)
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_a
|
27
|
+
chunks.map(&:to_a)
|
28
|
+
end
|
29
|
+
|
30
|
+
def flush
|
31
|
+
tmp = chunks
|
32
|
+
@chunks = nil
|
33
|
+
@byte_counter = 0
|
34
|
+
tmp
|
35
|
+
end
|
36
|
+
|
37
|
+
def flush_base64
|
38
|
+
tmp = to_base64
|
39
|
+
flush
|
40
|
+
tmp
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
# This method approximates base64 byte counting. The base64 encoded size
|
46
|
+
# will be greater than or equal to the raw bytes, so this is safe.
|
47
|
+
# Note that `obj` can be either a String or Array; both respond to size.
|
48
|
+
def check_length(obj)
|
49
|
+
@byte_counter += obj.size
|
50
|
+
add_chunk if @byte_counter > @max_chunk_bytes
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_chunk
|
54
|
+
chunks << ::Thermal::ByteBuffer.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def append(obj)
|
58
|
+
chunks.last << obj
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thermal
|
4
|
+
module Stargraphic
|
5
|
+
# TODO: image buffer
|
6
|
+
# TODO: page limit
|
7
|
+
class Writer < ::Thermal::WriterBase
|
8
|
+
DOT_WIDTH = 576
|
9
|
+
LINE_HEIGHT = 34
|
10
|
+
PAGE_LENGTH = 32_000
|
11
|
+
MAX_BYTES = 500_000 # Star API allows 500KB max
|
12
|
+
|
13
|
+
def initialize(profile)
|
14
|
+
@width = DOT_WIDTH
|
15
|
+
@align = :left
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.format
|
20
|
+
'stargraphic'
|
21
|
+
end
|
22
|
+
|
23
|
+
def print(flush: true)
|
24
|
+
seq_init_raster_mode
|
25
|
+
seq_enter_raster_mode
|
26
|
+
seq_set_page_length
|
27
|
+
super
|
28
|
+
feed(5)
|
29
|
+
cut
|
30
|
+
finalize(flush: flush)
|
31
|
+
end
|
32
|
+
|
33
|
+
def text(str, feed: true, replace: nil, **_kwargs)
|
34
|
+
str = ::Thermal::Util.normalize_utf8(str, replace: replace)
|
35
|
+
str = "#{str}\n" if feed
|
36
|
+
append_text(str) if str
|
37
|
+
end
|
38
|
+
|
39
|
+
def hr(style = nil, width: col_width) # rubocop:disable Lint/UnusedMethodArgument
|
40
|
+
append_image(hr_image(style))
|
41
|
+
# append_raw(hr_seq(style))
|
42
|
+
end
|
43
|
+
|
44
|
+
def bold
|
45
|
+
@bold = true
|
46
|
+
yield
|
47
|
+
@bold = false
|
48
|
+
end
|
49
|
+
|
50
|
+
def underline(weight: nil) # rubocop:disable Lint/UnusedMethodArgument
|
51
|
+
@underline = true
|
52
|
+
yield
|
53
|
+
@underline = false
|
54
|
+
end
|
55
|
+
|
56
|
+
def feed(lines = 1)
|
57
|
+
feed_rows(lines * LINE_HEIGHT)
|
58
|
+
end
|
59
|
+
|
60
|
+
def image(path)
|
61
|
+
width, height = ::MiniMagick::Image.open(path).dimensions
|
62
|
+
output, = Open3.capture3("convert #{path} -depth 1 R:-", binmode: true)
|
63
|
+
output = output.chomp
|
64
|
+
append_image([width, height, output])
|
65
|
+
end
|
66
|
+
|
67
|
+
def qr_code(url)
|
68
|
+
append_image(qr_code_image(url))
|
69
|
+
end
|
70
|
+
|
71
|
+
def cut
|
72
|
+
seq_set_em_mode
|
73
|
+
seq_exec_em
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
def commands
|
79
|
+
@commands ||= []
|
80
|
+
end
|
81
|
+
|
82
|
+
def buffer
|
83
|
+
@buffer ||= ::Thermal::Stargraphic::CappedByteBuffer.new(MAX_BYTES)
|
84
|
+
end
|
85
|
+
|
86
|
+
def finalize(flush: true, output: nil)
|
87
|
+
rasterize_text
|
88
|
+
write_commands_to_buffer
|
89
|
+
@commands = []
|
90
|
+
if output == :byte_array
|
91
|
+
[flush ? buffer.flush : buffer.to_a]
|
92
|
+
else
|
93
|
+
[flush ? buffer.flush_base64 : buffer.to_base64]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def rasterize_text
|
98
|
+
commands.each_index do |i|
|
99
|
+
if commands[i][0] == :text
|
100
|
+
data = commands[i][1]
|
101
|
+
@commands[i] = [:image, text_image(data[0], align: data[1])]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def write_commands_to_buffer
|
107
|
+
commands.each do |type, obj|
|
108
|
+
case type
|
109
|
+
when :raw then write(obj)
|
110
|
+
when :image then write_image(obj)
|
111
|
+
else raise "Cannot write type #{type.inspect}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def write(obj)
|
117
|
+
buffer << obj
|
118
|
+
end
|
119
|
+
|
120
|
+
def write_image(obj)
|
121
|
+
width, _, data = obj
|
122
|
+
raise "Width #{width}px must be a multiple of 8" unless width % 8 == 0
|
123
|
+
raise "Width #{width}px cannot be greater than #{@width}px" if width > @width
|
124
|
+
|
125
|
+
byte_width = width / 8
|
126
|
+
row_cmd = [98, byte_width, 0]
|
127
|
+
data.each_chunk(byte_width) do |row|
|
128
|
+
buffer << row_cmd
|
129
|
+
buffer << row
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# control chars:
|
134
|
+
# \e{ - converts to <
|
135
|
+
# \e} - converts to >
|
136
|
+
def append_text(str, font_size: 21, spacing: -2)
|
137
|
+
str = str
|
138
|
+
.gsub('&', '&')
|
139
|
+
.gsub('\\', '\')
|
140
|
+
.gsub('<', '<')
|
141
|
+
.gsub('>', '>')
|
142
|
+
.gsub('&', '&') # do a second-pass on & char
|
143
|
+
.gsub("\e{", '<')
|
144
|
+
.gsub("\e}", '>')
|
145
|
+
|
146
|
+
str = "<i>#{str}</i>" if @italic
|
147
|
+
str = "<b>#{str}</b>" if @bold
|
148
|
+
str = "<u>#{str}</u>" if @underline
|
149
|
+
str = "<span size='#{(font_size * 1024).to_i}' letter_spacing='#{(spacing * 1024).to_i}'>#{str}</span>"
|
150
|
+
str = str.encode_utf8('')
|
151
|
+
|
152
|
+
if commands[-1] && commands[-1][0] == :text && commands[-1][1][1] == @align # align match
|
153
|
+
commands[-1][1][0] << str
|
154
|
+
else
|
155
|
+
commands << [:text, [str, @align]]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def append_image(obj)
|
160
|
+
obj[2] = (+obj[2]).force_encoding('UTF-8')
|
161
|
+
if commands[-1] && commands[-1][0] == :image && commands[-1][1][0] == obj[0] # width match
|
162
|
+
commands[-1][1][1] += obj[1] # add height
|
163
|
+
commands[-1][1][2] << obj[2] # append data
|
164
|
+
else
|
165
|
+
commands << [:image, obj]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def append_raw(obj)
|
170
|
+
commands << [:raw, obj]
|
171
|
+
end
|
172
|
+
|
173
|
+
def seq_set_page_length(length = 32_000)
|
174
|
+
append_raw([27, 42, 114, 80] + length.to_s.each_char.map(&:ord) + [0])
|
175
|
+
end
|
176
|
+
|
177
|
+
def seq_init_raster_mode
|
178
|
+
append_raw [27, 42, 114, 82]
|
179
|
+
end
|
180
|
+
|
181
|
+
def seq_enter_raster_mode
|
182
|
+
append_raw [27, 42, 114, 65]
|
183
|
+
end
|
184
|
+
|
185
|
+
def seq_set_em_mode(cut = true) # rubocop:disable Style/OptionalBooleanParameter
|
186
|
+
append_raw [27, 42, 114, 101, cut ? 13 : 0, 0]
|
187
|
+
end
|
188
|
+
|
189
|
+
def seq_exec_em
|
190
|
+
append_raw [27, 12, 25]
|
191
|
+
end
|
192
|
+
|
193
|
+
def font
|
194
|
+
self.class.font
|
195
|
+
end
|
196
|
+
|
197
|
+
class << self
|
198
|
+
# Persists font as singleton
|
199
|
+
def font
|
200
|
+
@font ||= if OS.windows?
|
201
|
+
'Sans'
|
202
|
+
else
|
203
|
+
font_list = begin
|
204
|
+
`fc-list`
|
205
|
+
rescue StandardError
|
206
|
+
''
|
207
|
+
end
|
208
|
+
case font_list
|
209
|
+
when /NotoSans/ then 'NotoSans'
|
210
|
+
else 'Sans'
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def text_image(markup, width: @width, align: :left, font: 'Sans', delete: true)
|
217
|
+
tmp_path = ::Thermal.tmp_path("#{SecureRandom.uuid}.png")
|
218
|
+
|
219
|
+
begin
|
220
|
+
::MiniMagick::Tool::Convert.new do |i|
|
221
|
+
i << '+antialias'
|
222
|
+
i << '+dither'
|
223
|
+
i.size width
|
224
|
+
i.font font
|
225
|
+
# replace next line with align when released
|
226
|
+
i.gravity :center if align == :center
|
227
|
+
# after this is released https://github.com/ImageMagick/ImageMagick6/commit/b82aa924
|
228
|
+
# i.define "pango:align=#{align}"
|
229
|
+
i.pango markup
|
230
|
+
i.threshold '30%'
|
231
|
+
i.depth 1
|
232
|
+
i.negate
|
233
|
+
i << tmp_path
|
234
|
+
end
|
235
|
+
rescue StandardError => e
|
236
|
+
Bugsnag.notify(e) do |r|
|
237
|
+
r.add_metadata('data',
|
238
|
+
text: markup,
|
239
|
+
width: width,
|
240
|
+
align: align,
|
241
|
+
font: font)
|
242
|
+
end
|
243
|
+
raise e
|
244
|
+
end
|
245
|
+
|
246
|
+
output, = Open3.capture3("convert #{tmp_path} -depth 1 R:-", binmode: true)
|
247
|
+
File.delete(tmp_path) if delete
|
248
|
+
output = output.chomp
|
249
|
+
height = (8 * output.size / width.to_f).ceil
|
250
|
+
[width, height, output]
|
251
|
+
end
|
252
|
+
|
253
|
+
# TODO: assumes 8px width
|
254
|
+
# TODO: centering is off
|
255
|
+
def qr_code_image(url, width: @width)
|
256
|
+
cols = width / 8
|
257
|
+
qr_lines = ::RQRCode::QRCode.new(url).to_s(dark: 'x', light: ' ').split("\n")
|
258
|
+
height = qr_lines.size * 8
|
259
|
+
qr_lines = qr_lines.map { |line| line.each_char.map { |d| d == 'x' ? 255 : 0 } }
|
260
|
+
qr_cols = qr_lines.first.size
|
261
|
+
margin = (cols - qr_cols) / 2.0
|
262
|
+
zero = [0]
|
263
|
+
output = qr_lines.map { |line| ((zero * margin.floor) + line + (zero * margin.ceil)) * 8 }.flatten.pack('C*')
|
264
|
+
[width, height, output]
|
265
|
+
end
|
266
|
+
|
267
|
+
def feed_rows(rows)
|
268
|
+
append_raw(feed_rows_seq(rows))
|
269
|
+
end
|
270
|
+
|
271
|
+
def feed_rows_seq(rows)
|
272
|
+
[27, 42, 114, 89] + rows.to_s.each_char.map(&:ord) + [0]
|
273
|
+
end
|
274
|
+
|
275
|
+
def hr_image(style = nil, width: @width)
|
276
|
+
byte_width = width / 8
|
277
|
+
pixels = hr_pixels(style)
|
278
|
+
output = +''
|
279
|
+
(0...LINE_HEIGHT).each do |row|
|
280
|
+
output << ((row.in?(pixels) ? "\xFF" : "\x00") * byte_width)
|
281
|
+
end
|
282
|
+
output.freeze
|
283
|
+
[width, LINE_HEIGHT, output]
|
284
|
+
end
|
285
|
+
|
286
|
+
# More efficient
|
287
|
+
# def hr_seq(style = nil, width: @width)
|
288
|
+
# byte_width = width / 8
|
289
|
+
# pixels = hr_pixels(style)
|
290
|
+
# output = []
|
291
|
+
# i = 0
|
292
|
+
# (0...LINE_HEIGHT).each do |row|
|
293
|
+
# if row.in?(pixels)
|
294
|
+
# output += feed_rows_seq(i) if i > 0
|
295
|
+
# output += [98, byte_width, 0] + [255] * byte_width
|
296
|
+
# i = 0
|
297
|
+
# else
|
298
|
+
# i += 1
|
299
|
+
# end
|
300
|
+
# end
|
301
|
+
# output += feed_rows_seq(i) if i > 0
|
302
|
+
# output
|
303
|
+
# end
|
304
|
+
|
305
|
+
def hr_pixels(style)
|
306
|
+
case style
|
307
|
+
when :bold then 11..14
|
308
|
+
when :double then [9, 10, 15, 16]
|
309
|
+
when :underline then 25..26
|
310
|
+
when :half_upper then 0..12
|
311
|
+
when :half_lower then 13..29
|
312
|
+
when :full then 0..29
|
313
|
+
else 12..13
|
314
|
+
end.to_a
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|