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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -20
  3. data/README.md +200 -27
  4. data/data/db.yml +1987 -0
  5. data/data/original.yml +1635 -0
  6. data/lib/thermal/byte_buffer.rb +48 -0
  7. data/lib/thermal/db/charset.rb +36 -0
  8. data/lib/thermal/db/cjk_encoding.rb +46 -0
  9. data/lib/thermal/db/data.rb +77 -0
  10. data/lib/thermal/db/device.rb +79 -0
  11. data/lib/thermal/db/encoding.rb +35 -0
  12. data/lib/thermal/db/loader.rb +65 -0
  13. data/lib/thermal/db.rb +43 -0
  14. data/lib/thermal/dsl.rb +28 -0
  15. data/lib/thermal/escpos/buffer.rb +167 -0
  16. data/lib/thermal/escpos/cmd.rb +11 -0
  17. data/lib/thermal/escpos/writer.rb +93 -0
  18. data/lib/thermal/escpos_star/buffer.rb +38 -0
  19. data/lib/thermal/escpos_star/writer.rb +17 -0
  20. data/lib/thermal/printer.rb +56 -21
  21. data/lib/thermal/profile.rb +71 -0
  22. data/lib/thermal/stargraphic/capped_byte_buffer.rb +20 -0
  23. data/lib/thermal/stargraphic/chunked_byte_buffer.rb +62 -0
  24. data/lib/thermal/stargraphic/writer.rb +318 -0
  25. data/lib/thermal/starprnt/buffer.rb +46 -0
  26. data/lib/thermal/starprnt/writer.rb +81 -0
  27. data/lib/thermal/util.rb +74 -0
  28. data/lib/thermal/version.rb +5 -3
  29. data/lib/thermal/writer_base.rb +122 -0
  30. data/lib/thermal.rb +81 -8
  31. metadata +59 -57
  32. data/.gitignore +0 -3
  33. data/.rspec +0 -2
  34. data/.travis.yml +0 -6
  35. data/Gemfile +0 -3
  36. data/Rakefile +0 -1
  37. data/lib/devices/btpr880.rb +0 -33
  38. data/lib/devices/html.rb +0 -14
  39. data/lib/thermal/parser.rb +0 -30
  40. data/spec/btpr880_spec.rb +0 -36
  41. data/spec/fixtures/receipt.html +0 -6
  42. data/spec/printer_spec.rb +0 -29
  43. data/spec/spec_helper.rb +0 -3
  44. data/spec/thermal_spec.rb +0 -7
  45. data/tasks/console.rake +0 -9
  46. data/tasks/spec.rake +0 -3
  47. data/thermal.gemspec +0 -16
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermal
4
+ class ByteBuffer
5
+ extend Forwardable
6
+
7
+ def buffer
8
+ @buffer ||= []
9
+ end
10
+
11
+ def <<(obj)
12
+ case obj
13
+ when String then append(*obj.bytes)
14
+ when Integer then append(obj)
15
+ when Array then append(*obj)
16
+ else raise "Unknown object type: #{obj.class}"
17
+ end
18
+ end
19
+ alias_method :write, :<<
20
+ alias_method :sequence, :<<
21
+
22
+ def to_base64
23
+ Base64.strict_encode64(to_s)
24
+ end
25
+
26
+ def to_s
27
+ buffer.pack('C*')
28
+ end
29
+
30
+ def to_a
31
+ buffer.dup
32
+ end
33
+
34
+ def flush
35
+ tmp = buffer
36
+ @buffer = nil
37
+ tmp
38
+ end
39
+
40
+ def flush_base64
41
+ tmp = to_base64
42
+ flush
43
+ tmp
44
+ end
45
+
46
+ def_delegators :buffer, :append
47
+ end
48
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermal
4
+ module Db
5
+ # The charset is different than the codepage. It controls usage of
6
+ # specific ASCII-range code points such as dollar-sign ($).
7
+ class Charset
8
+ CODEPOINTS = [0x23, 0x24, 0x25, 0x2A, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x60, 0x7B, 0x7C, 0x7D, 0x7E].freeze
9
+
10
+ attr_reader :key, :charmap
11
+
12
+ def initialize(key, charmap)
13
+ @key = key
14
+ @charmap = Array(charmap)&.join&.freeze
15
+ end
16
+
17
+ def u_codepoints
18
+ return @u_codepoints if defined?(@u_codepoints)
19
+
20
+ @u_codepoints = @charmap.each_codepoint.to_a
21
+ end
22
+
23
+ def char?(char)
24
+ !!charmap&.include?(char)
25
+ end
26
+
27
+ def codepoint?(u_codepoint)
28
+ !!codepoints&.include?(u_codepoint)
29
+ end
30
+
31
+ def codepoint(u_codepoint)
32
+ CODEPOINTS[u_codepoints&.index(u_codepoint)]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermal
4
+ module Db
5
+ # Adds CJK character encoding lookup.
6
+ class CjkEncoding
7
+ attr_reader :key, :ruby
8
+
9
+ def initialize(key, ruby)
10
+ @key = key
11
+ @ruby = ruby
12
+ end
13
+
14
+ def codepoint(u_codepoint)
15
+ char(u_codepoint.chr('UTF-8'))
16
+ rescue RangeError
17
+ nil
18
+ end
19
+
20
+ def codepoint?(u_codepoint)
21
+ !!codepoint(u_codepoint)
22
+ end
23
+
24
+ def char(u_char)
25
+ return if u_char.ord <= 127
26
+
27
+ char = u_char.encode(@ruby)
28
+ char unless skip?(char)
29
+ rescue EncodingError
30
+ nil
31
+ end
32
+
33
+ def char?(u_char)
34
+ !!char(u_char)
35
+ end
36
+
37
+ private
38
+
39
+ def skip?(char)
40
+ # GB18030 has a 4-byte mapping into Unicode which should be skipped.
41
+ # Refer to: https://en.wikipedia.org/wiki/GB_18030#Mapping
42
+ @ruby == 'GB18030' && char&.bytesize == 4
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermal
4
+ module Db
5
+ module Data
6
+ extend self
7
+
8
+ MUTEX = Mutex.new
9
+ DATA_PATH = 'data/db.yml'
10
+
11
+ def data
12
+ MUTEX.synchronize do
13
+ @data ||= begin
14
+ data = load_data
15
+ normalize_data!(data)
16
+ ::Thermal::Util.deep_freeze!(data)
17
+ data
18
+ end
19
+ end
20
+ end
21
+
22
+ def available_devices
23
+ data['devices']
24
+ end
25
+
26
+ def available_encodings
27
+ data['encodings']
28
+ end
29
+
30
+ def available_charsets
31
+ data['charsets']
32
+ end
33
+
34
+ def device(device)
35
+ return unless device
36
+
37
+ data.dig('devices', device.to_s)
38
+ end
39
+
40
+ def encoding(encoding)
41
+ return unless encoding
42
+
43
+ data.dig('encodings', encoding.to_s)
44
+ end
45
+
46
+ def charset(charset)
47
+ return unless charset
48
+
49
+ data.dig('charsets', 'escpos', charset.to_i)
50
+ end
51
+
52
+ def reload
53
+ MUTEX.synchronize do
54
+ @data = nil
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def data_path
61
+ ::Thermal.gem_root.join(DATA_PATH).to_s
62
+ end
63
+
64
+ def load_data
65
+ YAML.safe_load_file(data_path, aliases: true)
66
+ end
67
+
68
+ def normalize_data!(data)
69
+ data.dig('charsets', 'escpos').transform_keys!(&:to_i)
70
+ device_int_keys = %w[codepages colors fonts]
71
+ data['devices'].each_value do |d|
72
+ device_int_keys.each { |k| d[k]&.transform_keys!(&:to_i) }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermal
4
+ module Db
5
+ class Device
6
+ DEFAULT_COL_WIDTH = 42
7
+
8
+ attr_reader :key
9
+
10
+ def initialize(key, data)
11
+ @key = key
12
+ @data = data
13
+
14
+ # eager load cache
15
+ codepage_index
16
+ charset_index
17
+ end
18
+
19
+ def name
20
+ @data['name']&.to_s
21
+ end
22
+
23
+ def format
24
+ @data['format']&.to_s
25
+ end
26
+
27
+ def supports?(feature)
28
+ !!@data.dig('features', feature)
29
+ end
30
+
31
+ # TODO: this needs to be dynamic in the printer
32
+ def col_width
33
+ @col_width ||= @data.dig('fonts', 0, 'columns') || DEFAULT_COL_WIDTH
34
+ end
35
+
36
+ def find_encoding(u_codepoint)
37
+ if (codepage = codepage_index[u_codepoint])
38
+ [:codepage, codepage].freeze
39
+ elsif (charset = charset_index[u_codepoint]) && charset > 0
40
+ [:charset, charset].freeze
41
+ end
42
+ end
43
+
44
+ # def find_codepage(encoding)
45
+ # encoding = encoding&.to_s
46
+ # device.codepages&.invert&.[](encoding) || 0
47
+ # end
48
+
49
+ def codepages
50
+ @codepages ||= begin
51
+ codepages = @data['codepages'] || ::Thermal::Db::DEFAULT_CODEPAGES
52
+ codepages.each_with_object({}) do |(k, v), h|
53
+ encoding = ::Thermal::Db.encoding(v)
54
+ h[k] = encoding if encoding
55
+ end.freeze
56
+ end
57
+ end
58
+
59
+ def charsets
60
+ @charsets ||= begin
61
+ charsets = @data['charsets'] || ::Thermal::Db::DEFAULT_CHARSETS
62
+ ::Thermal::Util.index_with(charsets) { |i| ::Thermal::Db.charset(i) }.freeze
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def codepage_index
69
+ @codepage_index ||= codepages.map { |k, v| ::Thermal::Util.index_with(v.u_codepoints, k) }
70
+ .reverse.inject(&:merge).freeze
71
+ end
72
+
73
+ def charset_index
74
+ @charset_index ||= charsets.values.map! { |c| ::Thermal::Util.index_with(c.u_codepoints, c.key) }
75
+ .reverse.inject(&:merge).freeze
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermal
4
+ module Db
5
+ class Encoding
6
+ RANGE = (128..255)
7
+
8
+ attr_reader :key, :charmap
9
+
10
+ def initialize(key, charmap)
11
+ @key = key
12
+ @charmap = Array(charmap)&.join&.freeze
13
+ raise 'Invalid charmap' unless @charmap.size == 128
14
+ end
15
+
16
+ def u_codepoints
17
+ return @u_codepoints if defined?(@u_codepoints)
18
+
19
+ @u_codepoints = charmap.each_codepoint.to_a
20
+ end
21
+
22
+ def char?(char)
23
+ !!charmap&.include?(char)
24
+ end
25
+
26
+ def codepoint?(u_codepoint)
27
+ !!u_codepoints&.include?(u_codepoint)
28
+ end
29
+
30
+ def codepoint(u_codepoint)
31
+ u_codepoints&.index(u_codepoint)&.+(128)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermal
4
+ module Db
5
+ module Loader
6
+ extend self
7
+
8
+ MUTEX = Mutex.new
9
+
10
+ def device(device)
11
+ device = device&.to_s
12
+ cached(:devices, device) do
13
+ cfg = data.device(device)
14
+ ::Thermal::Db::Device.new(device, cfg) if cfg
15
+ end
16
+ end
17
+
18
+ def encoding(encoding)
19
+ encoding = encoding&.to_s
20
+ cached(:encodings, encoding) do
21
+ charmap = data.encoding(encoding)&.[]('charmap')
22
+ ::Thermal::Db::Encoding.new(encoding, charmap) if charmap
23
+ end
24
+ end
25
+
26
+ def charset(charset)
27
+ charset = charset&.to_i
28
+ cached(:charsets, charset) do
29
+ charmap = data.charset(charset)&.[]('charmap')
30
+ ::Thermal::Db::Charset.new(charset, charmap) if charmap
31
+ end
32
+ end
33
+
34
+ def cjk_encoding(cjk)
35
+ cjk = cjk&.to_s
36
+ cached(:cjk_encodings, cjk) do
37
+ ruby = data.encoding(cjk)&.[]('ruby')
38
+ ::Thermal::Db::CjkEncoding.new(cjk, ruby) if ruby
39
+ end
40
+ end
41
+
42
+ def reload
43
+ MUTEX.synchronize do
44
+ @cache = nil
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def cached(key, value)
51
+ cache[key.to_s][value] ||= yield
52
+ end
53
+
54
+ def cache
55
+ MUTEX.synchronize do
56
+ @cache ||= Hash.new { |h, k| h[k] = {} }
57
+ end
58
+ end
59
+
60
+ def data
61
+ ::Thermal::Db::Data
62
+ end
63
+ end
64
+ end
65
+ end
data/lib/thermal/db.rb ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermal
4
+ module Db
5
+ extend self
6
+ extend Forwardable
7
+
8
+ DEFAULT_DEVICE = 'escpos_generic'
9
+ DEFAULT_CODEPAGES = { 0 => 'cp437' }.freeze
10
+ DEFAULT_CHARSETS = [0].freeze
11
+
12
+ def_delegators ::Thermal::Db::Data,
13
+ :data,
14
+ :available_devices,
15
+ :available_encodings,
16
+ :available_charsets
17
+
18
+ def_delegators ::Thermal::Db::Loader,
19
+ :encoding,
20
+ :charset,
21
+ :cjk_encoding
22
+
23
+ def device(device)
24
+ loader = ::Thermal::Db::Loader
25
+ loader.device(device) || loader.device(DEFAULT_DEVICE)
26
+ end
27
+
28
+ def find_cjk_encoding(locale)
29
+ return unless locale
30
+
31
+ case locale.to_s.downcase.tr('_', '-')
32
+ when /\Aja(-.+)?\z/, 'jp'
33
+ 'shift_jis'
34
+ when /\Ako(-.+)?\z/, 'kr'
35
+ 'ksc5601'
36
+ when /\A(zh)?-?(hant(-.+)?|tw|hk|mo)\z/
37
+ 'big5'
38
+ when /\Azh(-.+)?\z/, 'zhhans', 'cn', 'sg', 'my'
39
+ 'gb18030'
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermal
4
+ module Dsl
5
+ def self.included(base)
6
+ base.class_eval do
7
+ def thermal_print(...)
8
+ printer.print(...)
9
+ end
10
+
11
+ private
12
+
13
+ ::Thermal::Printer::PRINT_METHODS.each do |meth|
14
+ define_method(meth) do |*args, **kwargs, &block|
15
+ raise 'Must define #printer method or set @printer variable' unless printer
16
+
17
+ printer.send(meth, *args, **kwargs, &block)
18
+ end
19
+ private(meth)
20
+ end
21
+
22
+ def printer
23
+ @printer
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermal
4
+ module Escpos
5
+ # Inspired by mike42/escpos-php
6
+ # https://github.com/mike42/escpos-php/blob/development/src/Mike42/Escpos/PrintBuffers/EscposPrintBuffer.php
7
+ class Buffer < ::Thermal::ByteBuffer
8
+ extend Forwardable
9
+
10
+ def initialize(profile)
11
+ super()
12
+ @profile = profile
13
+ @codepage = 0
14
+ @charset = 0
15
+ @cjk = false
16
+ init_buffer!
17
+ end
18
+
19
+ def_delegators :@profile,
20
+ :find_encoding,
21
+ :cjk_encoding,
22
+ :codepages,
23
+ :charsets,
24
+ :codepoint_cjk_skip?,
25
+ :codepoint_cjk_force?
26
+
27
+ def current_encoding
28
+ @cjk ? cjk_encoding : codepages[@codepage]
29
+ end
30
+
31
+ def current_charset
32
+ charsets[@charset]
33
+ end
34
+
35
+ def write_text(text, replace: nil, no_cjk: false)
36
+ text = ::Thermal::Util.normalize_utf8(text, replace: replace)
37
+ text&.each_codepoint do |u_codepoint|
38
+ write_u_codepoint(u_codepoint, replace: replace, no_cjk: no_cjk)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def write_u_codepoint(u_codepoint, replace: nil, no_cjk: false, fallback: false)
45
+ replace ||= ::Thermal.replace_char
46
+
47
+ if ascii?(u_codepoint)
48
+ reset_charset!(u_codepoint)
49
+ write(u_codepoint)
50
+ elsif current_encoding &&
51
+ (!@cjk || (!no_cjk && !codepoint_cjk_skip?(u_codepoint))) &&
52
+ (@cjk || !codepoint_cjk_force?(u_codepoint)) &&
53
+ (codepoint = current_encoding.codepoint(u_codepoint))
54
+ write(codepoint)
55
+ else
56
+ location, value = find_encoding(u_codepoint, no_cjk: no_cjk)
57
+ case location
58
+ when :codepage
59
+ set_codepage(value)
60
+ codepoint = current_encoding.codepoint(u_codepoint)
61
+ # TODO: move this to encoding class
62
+ raise_missing_codepoint!(u_codepoint, current_encoding) unless codepoint
63
+ write(codepoint)
64
+ when :charset
65
+ set_charset(value)
66
+ codepoint = current_charset.codepoint(u_codepoint)
67
+ # TODO: move this to encoding class
68
+ raise_missing_codepoint!(u_codepoint, current_encoding) unless codepoint
69
+ write(codepoint)
70
+ when :cjk
71
+ set_cjk(true)
72
+ char = current_encoding.codepoint(u_codepoint)
73
+ # TODO: move this to encoding class
74
+ raise_missing_codepoint!(u_codepoint, current_encoding) unless char
75
+ write(char)
76
+ else
77
+ write_u_codepoint(replace.ord, replace: ' ', fallback: true) unless fallback || replace.empty?
78
+ end
79
+ end
80
+ end
81
+
82
+ def init_buffer!
83
+ sequence(::Escpos::HW_INIT)
84
+
85
+ # CJK is on after HW_INIT for Chinese models.
86
+ # To ensure consistency, we explicitly set it off.
87
+ # https://reference.epson-biz.com/modules/ref_escpos/index.php?content_id=175
88
+ sequence(Cmd::SET_CJK_OFF) if cjk_supported?
89
+ end
90
+
91
+ def ascii?(codepoint, extended: false)
92
+ (codepoint == 10) ||
93
+ (codepoint >= 32 && codepoint <= 126) ||
94
+ (extended && codepoint >= 128 && codepoint <= 255)
95
+ end
96
+
97
+ def set_charset(charset) # rubocop:disable Naming/AccessorMethodName
98
+ return if @charset == charset
99
+
100
+ sequence([0x1b, 0x52] + [charset])
101
+ @charset = charset
102
+ end
103
+
104
+ def reset_charset!(u_codepoint)
105
+ return unless ::Thermal::Db::Charset::CODEPOINTS.include?(u_codepoint)
106
+
107
+ set_charset(0)
108
+ end
109
+
110
+ def set_codepage(codepage) # rubocop:disable Naming/AccessorMethodName
111
+ set_cjk(false)
112
+ return if @codepage == codepage
113
+
114
+ sequence(::Escpos::CP_SET + [codepage])
115
+ @codepage = codepage
116
+ end
117
+
118
+ def set_cjk(enabled) # rubocop:disable Naming/AccessorMethodName
119
+ enabled ? set_cjk_on : set_cjk_off
120
+ end
121
+
122
+ def set_cjk_on
123
+ return if !cjk_supported? || @cjk
124
+
125
+ sequence(cjk_on_command)
126
+ @cjk = true
127
+ end
128
+
129
+ def set_cjk_off
130
+ return if !cjk_supported? || !@cjk
131
+
132
+ sequence(cjk_off_command)
133
+ @cjk = false
134
+ end
135
+
136
+ def cjk_supported?
137
+ !!cjk_encoding
138
+ end
139
+
140
+ def shift_jis?
141
+ cjk_encoding&.ruby == 'Shift_JIS'
142
+ end
143
+
144
+ def cjk_on_command
145
+ if shift_jis?
146
+ Cmd::SET_JIS_MODE + [1]
147
+ else
148
+ Cmd::SET_CJK_ON
149
+ end
150
+ end
151
+
152
+ def cjk_off_command
153
+ if shift_jis?
154
+ Cmd::SET_JIS_MODE + [0]
155
+ else
156
+ Cmd::SET_CJK_OFF
157
+ end
158
+ end
159
+
160
+ # TODO: move this to encoding
161
+ def raise_missing_codepoint!(u_codepoint, encoding)
162
+ klass = encoding.class.name.split('::').last
163
+ raise "Codepoint U#{u_codepoint.to_s(16)} not found in #{klass} #{encoding.name}"
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thermal
4
+ module Escpos
5
+ module Cmd
6
+ SET_JIS_MODE = [0x1C, 0x43].freeze
7
+ SET_CJK_ON = [0x1C, 0x26].freeze
8
+ SET_CJK_OFF = [0x1C, 0x2E].freeze
9
+ end
10
+ end
11
+ end