paperback 0.0.2
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/.gitignore +5 -0
- data/.rspec +3 -0
- data/.rubocop-disables.yml +133 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +24 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +676 -0
- data/README.md +38 -0
- data/Rakefile +15 -0
- data/bin/paperback +76 -0
- data/lib/paperback.rb +32 -0
- data/lib/paperback/cli.rb +12 -0
- data/lib/paperback/document.rb +174 -0
- data/lib/paperback/preparer.rb +206 -0
- data/lib/paperback/version.rb +5 -0
- data/paperback.gemspec +38 -0
- data/spec/spec_helper.rb +88 -0
- data/spec/unit/paperback_spec.rb +5 -0
- metadata +208 -0
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Paperback
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/paperback)
|
4
|
+
[](https://travis-ci.org/ab/paperback)
|
5
|
+
[](https://codeclimate.com/github/ab/paperback)
|
6
|
+
[](http://www.rubydoc.info/github/ab/paperback/master)
|
7
|
+
|
8
|
+
*Paperback* is a library that facilitates the creation of paper offline backups
|
9
|
+
of small amounts of important data, such as encryption keys.
|
10
|
+
|
11
|
+
It is designed to be used for long-term paper storage. Arbitrary data to be
|
12
|
+
backed up is encoded using QR codes and
|
13
|
+
[sixword](https://github.com/ab/sixword) English text.
|
14
|
+
|
15
|
+
By default, the backup data is GPG-encrypted with a symmetric passphrase to
|
16
|
+
avoid exposing data to the printer (or scanner, assuming you cover the
|
17
|
+
passphrase when scanning).
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Typical usage will be through the `paperback` executable.
|
22
|
+
|
23
|
+
```sh
|
24
|
+
# Back up the content in data.key
|
25
|
+
paperback data.key out.pdf
|
26
|
+
```
|
27
|
+
|
28
|
+
### More complex patterns
|
29
|
+
|
30
|
+
See the [YARD documentation](http://www.rubydoc.info/github/ab/paperback/master).
|
31
|
+
|
32
|
+
## Contributing
|
33
|
+
|
34
|
+
1. Fork it
|
35
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
36
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
37
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
38
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
task :default do
|
6
|
+
sh 'rake -T'
|
7
|
+
end
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new(:spec)
|
10
|
+
|
11
|
+
def alias_task(alias_task, original)
|
12
|
+
desc "Alias for rake #{original}"
|
13
|
+
task alias_task, Rake.application[original].arg_names => original
|
14
|
+
end
|
15
|
+
alias_task(:test, :spec)
|
data/bin/paperback
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$VERBOSE = true
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require_relative '../lib/paperback'
|
6
|
+
|
7
|
+
BaseName = File.basename($0)
|
8
|
+
|
9
|
+
def main
|
10
|
+
options = {}
|
11
|
+
|
12
|
+
optparse = OptionParser.new do |opts|
|
13
|
+
opts.banner = <<-EOM
|
14
|
+
usage: #{BaseName} [OPTION]... INPUT OUT_PDF
|
15
|
+
|
16
|
+
Create a printable PDF backup of INPUT file and write to OUT_PDF. The saved PDF
|
17
|
+
file will be suitable for printing. By default, the paper backup will contain
|
18
|
+
the input data encoded as a QR code for ease of scanning and as sixword-encoded
|
19
|
+
English text for more natural paper printing.
|
20
|
+
|
21
|
+
In order to prevent the input content from being exposed to the printer, by
|
22
|
+
default, the input content will be encrypted with a symmetric passphrase that
|
23
|
+
is echoed to the terminal. There will be a placeholder in the PDF where you can
|
24
|
+
manually add the passphrase by hand and pen.
|
25
|
+
|
26
|
+
For example:
|
27
|
+
|
28
|
+
# Create a backup of secret.key in backup.pdf
|
29
|
+
#{BaseName} secret.key backup.pdf
|
30
|
+
|
31
|
+
Options:
|
32
|
+
EOM
|
33
|
+
|
34
|
+
opts.on('-h', '--help', 'Display this message', ' ') do
|
35
|
+
STDERR.puts opts, ''
|
36
|
+
exit 0
|
37
|
+
end
|
38
|
+
opts.on('--version', 'Print version number', ' ') do
|
39
|
+
puts 'paperback ' + Paperback::VERSION
|
40
|
+
exit 0
|
41
|
+
end
|
42
|
+
opts.on('-v', '--verbose', 'Be more verbose', ' ') do
|
43
|
+
Paperback.log_level -= 1
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
|
48
|
+
opts.on('--no-encrypt', 'Skip encryption of input') do |val|
|
49
|
+
options[:encrypt] = val
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on('-c', '--comment TEXT', 'Add TEXT comment to output') do |val|
|
53
|
+
options[:comment] = val
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on('--passphrase-out FILE',
|
57
|
+
'Write generated passphrase to FILE') do |val|
|
58
|
+
options[:passphrase_file] = val
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
optparse.parse!
|
63
|
+
|
64
|
+
case ARGV.length
|
65
|
+
when 2
|
66
|
+
options[:input] = ARGV.fetch(0)
|
67
|
+
options[:output] = ARGV.fetch(1)
|
68
|
+
else
|
69
|
+
STDERR.puts optparse, ''
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
|
73
|
+
Paperback::CLI.create_backup(**options)
|
74
|
+
end
|
75
|
+
|
76
|
+
main
|
data/lib/paperback.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
# Paperback is a library for creating paper backups of sensitive data.
|
4
|
+
module Paperback
|
5
|
+
def self.log
|
6
|
+
return @log if @log
|
7
|
+
@log = Logger.new(STDERR)
|
8
|
+
@log.progname = self.name
|
9
|
+
@log.level = log_level
|
10
|
+
@log
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.class_log(klass, stream=STDERR)
|
14
|
+
log = Logger.new(stream)
|
15
|
+
log.progname = klass.name
|
16
|
+
log.level = log_level
|
17
|
+
log
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.log_level
|
21
|
+
@log_level ||= Logger::INFO
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.log_level=(val)
|
25
|
+
@log_level = val
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
require_relative 'paperback/version'
|
30
|
+
require_relative 'paperback/cli'
|
31
|
+
require_relative 'paperback/document'
|
32
|
+
require_relative 'paperback/preparer'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Paperback
|
2
|
+
module CLI
|
3
|
+
def self.create_backup(input:, output:, encrypt: true, qr_base64: true,
|
4
|
+
qr_level: :l, comment: nil, passphrase_file: nil)
|
5
|
+
prep = Paperback::Preparer.new(filename: input, encrypt: encrypt,
|
6
|
+
qr_base64: qr_base64, qr_level: qr_level,
|
7
|
+
passphrase_file: passphrase_file,
|
8
|
+
comment: comment)
|
9
|
+
prep.render(output_filename: output)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'prawn'
|
2
|
+
|
3
|
+
# Main class for creating and rendering PDFs
|
4
|
+
module Paperback; class Document
|
5
|
+
attr_reader :pdf, :debug
|
6
|
+
|
7
|
+
def initialize(debug: false)
|
8
|
+
log.debug('Document#initialize')
|
9
|
+
@debug = debug
|
10
|
+
@pdf = Prawn::Document.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def log
|
14
|
+
@log ||= Paperback.class_log(self.class)
|
15
|
+
end
|
16
|
+
|
17
|
+
def render(output_file:, draw_opts:)
|
18
|
+
log.info('Rendering PDF')
|
19
|
+
|
20
|
+
# Create all the PDF content
|
21
|
+
draw_paperback(**draw_opts)
|
22
|
+
|
23
|
+
# Render to output file
|
24
|
+
log.info("Writing PDF to #{output_file.inspect}")
|
25
|
+
pdf.render_file(output_file)
|
26
|
+
end
|
27
|
+
|
28
|
+
# High level method to draw the paperback content on the pdf document
|
29
|
+
def draw_paperback(qr_code:, sixword_lines:, sixword_bytes:,
|
30
|
+
labels:, passphrase_sha: nil, passphrase_len: nil)
|
31
|
+
unless qr_code.is_a?(RQRCode::QRCode)
|
32
|
+
raise ArgumentError.new('qr_code must be RQRCode::QRCode')
|
33
|
+
end
|
34
|
+
|
35
|
+
# Header & QR code page
|
36
|
+
pdf.font('Times-Roman')
|
37
|
+
|
38
|
+
debug_draw_axes
|
39
|
+
|
40
|
+
draw_header(labels: labels, passphrase_sha: passphrase_sha,
|
41
|
+
passphrase_len: passphrase_len)
|
42
|
+
|
43
|
+
add_newline
|
44
|
+
|
45
|
+
draw_qr_code(qr_modules: qr_code.modules)
|
46
|
+
|
47
|
+
pdf.stroke_color '000000'
|
48
|
+
pdf.fill_color '000000'
|
49
|
+
|
50
|
+
# Sixword page
|
51
|
+
|
52
|
+
pdf.start_new_page
|
53
|
+
|
54
|
+
draw_sixword(lines: sixword_lines, sixword_bytes: sixword_bytes)
|
55
|
+
|
56
|
+
pdf.number_pages('<page> of <total>', align: :right,
|
57
|
+
at: [pdf.bounds.right - 100, -2])
|
58
|
+
end
|
59
|
+
|
60
|
+
# If in debug mode, draw axes on the page to assist with layout
|
61
|
+
def debug_draw_axes
|
62
|
+
return unless debug
|
63
|
+
pdf.float { pdf.stroke_axis }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Move cursor down by one line
|
67
|
+
def add_newline
|
68
|
+
pdf.move_down(pdf.font_size)
|
69
|
+
end
|
70
|
+
|
71
|
+
def draw_header(labels:, passphrase_sha:, passphrase_len:,
|
72
|
+
repo_url: 'https://github.com/ab/paperback')
|
73
|
+
|
74
|
+
intro = [
|
75
|
+
"This is a paper backup produced by `paperback`. ",
|
76
|
+
"<u><link href='#{repo_url}'>#{repo_url}</link></u>",
|
77
|
+
].join
|
78
|
+
pdf.text(intro, inline_format: true)
|
79
|
+
add_newline
|
80
|
+
|
81
|
+
label_pad = labels.keys.map(&:length).max + 1
|
82
|
+
|
83
|
+
unless passphrase_sha && passphrase_len
|
84
|
+
labels['Encrypted'] = 'no'
|
85
|
+
end
|
86
|
+
|
87
|
+
pdf.font('Courier') do
|
88
|
+
labels.each_pair do |k, v|
|
89
|
+
pdf.text("#{(k + ':').ljust(label_pad)} #{v}")
|
90
|
+
end
|
91
|
+
|
92
|
+
if passphrase_sha
|
93
|
+
pdf.text("SHA256(passphrase)[0...16]: #{passphrase_sha}")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
add_newline
|
98
|
+
|
99
|
+
if passphrase_len
|
100
|
+
pdf.font('Helvetica') do
|
101
|
+
pdf.font_size(12.8) do
|
102
|
+
pdf.text('Passphrase: ' + '_ ' * passphrase_len)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
pdf.move_down(8)
|
107
|
+
pdf.indent(72) do
|
108
|
+
pdf.text('Be sure to cover the passphrase when scanning the QR code!')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# @param [Array<String>] lines An array of sixword sentences to print
|
114
|
+
# @param [Integer] columns The number of text columns on the page
|
115
|
+
# @param [Integer] hunks_per_row The number of 6-word sentences per line
|
116
|
+
# @param [Integer] sixword_bytes Bytesize of the sixword encoded data
|
117
|
+
def draw_sixword(lines:, sixword_bytes:, columns: 3, hunks_per_row: 1)
|
118
|
+
debug_draw_axes
|
119
|
+
|
120
|
+
numbered = lines.each_slice(hunks_per_row).each_with_index.map { |row, i|
|
121
|
+
"#{i * hunks_per_row + 1}: #{row.map(&:strip).join('. ')}"
|
122
|
+
}
|
123
|
+
|
124
|
+
header = [
|
125
|
+
"This sixword text encodes #{sixword_bytes} bytes in #{lines.length}",
|
126
|
+
" six-word sentences.",
|
127
|
+
" Decode with `sixword -d`"
|
128
|
+
].join
|
129
|
+
|
130
|
+
pdf.font('Times-Roman') do
|
131
|
+
pdf.text(header)
|
132
|
+
add_newline
|
133
|
+
end
|
134
|
+
|
135
|
+
pdf.column_box([0, pdf.cursor], columns: columns, width: pdf.bounds.width) do
|
136
|
+
pdf.font('Times-Roman') do
|
137
|
+
pdf.font_size(11) do
|
138
|
+
pdf.text(numbered.join("\n"))
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def draw_qr_code(qr_modules:)
|
145
|
+
qr_height = pdf.cursor # entire rest of page
|
146
|
+
qr_width = pdf.bounds.width # entire page width
|
147
|
+
|
148
|
+
# number of modules plus 2 for quiet area
|
149
|
+
qr_code_size = qr_modules.length + 2
|
150
|
+
|
151
|
+
pixel_height = qr_height / qr_code_size
|
152
|
+
pixel_width = qr_width / qr_code_size
|
153
|
+
|
154
|
+
pdf.bounding_box([0, pdf.cursor], width: qr_width, height: qr_height) do
|
155
|
+
if debug
|
156
|
+
pdf.stroke_color('888888')
|
157
|
+
pdf.stroke_bounds
|
158
|
+
end
|
159
|
+
|
160
|
+
qr_modules.each do |row|
|
161
|
+
pdf.move_down(pixel_height)
|
162
|
+
|
163
|
+
row.each_with_index do |pixel_val, col_i|
|
164
|
+
pdf.stroke do
|
165
|
+
pdf.stroke_color(pixel_val ? '000000' : 'ffffff')
|
166
|
+
pdf.fill_color(pixel_val ? '000000' : 'ffffff')
|
167
|
+
xy = [(col_i + 1) * pixel_width, pdf.cursor]
|
168
|
+
pdf.fill_and_stroke_rectangle(xy, pixel_width, pixel_height)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end; end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'digest/sha2'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
require 'rqrcode'
|
8
|
+
require 'sixword'
|
9
|
+
require 'subprocess'
|
10
|
+
|
11
|
+
module Paperback
|
12
|
+
# Class wrapping functions to prepare data for paperback storage, including
|
13
|
+
# QR code and sixword encoding.
|
14
|
+
class Preparer
|
15
|
+
attr_reader :data
|
16
|
+
attr_reader :labels
|
17
|
+
attr_reader :qr_base64
|
18
|
+
attr_reader :encrypt
|
19
|
+
attr_reader :passphrase_file
|
20
|
+
|
21
|
+
def initialize(filename:, encrypt: true, qr_base64: false, qr_level: nil,
|
22
|
+
comment: nil, passphrase_file: nil)
|
23
|
+
|
24
|
+
log.debug('Preparer#initialize')
|
25
|
+
|
26
|
+
log.info("Reading #{filename.inspect}")
|
27
|
+
plain_data = File.read(filename)
|
28
|
+
|
29
|
+
log.debug("Read #{plain_data.bytesize} bytes")
|
30
|
+
|
31
|
+
@encrypt = encrypt
|
32
|
+
|
33
|
+
if encrypt
|
34
|
+
@data = self.class.gpg_encrypt(filename: filename, password: passphrase)
|
35
|
+
else
|
36
|
+
@data = plain_data
|
37
|
+
end
|
38
|
+
@sha256 = Digest::SHA256.hexdigest(plain_data)
|
39
|
+
|
40
|
+
@qr_base64 = qr_base64
|
41
|
+
@qr_level = qr_level
|
42
|
+
|
43
|
+
@passphrase_file = passphrase_file
|
44
|
+
|
45
|
+
@labels = {}
|
46
|
+
@labels['Filename'] = filename
|
47
|
+
@labels['Backed up'] = Time.now.to_s
|
48
|
+
|
49
|
+
stat = File.stat(filename)
|
50
|
+
@labels['Mtime'] = stat.mtime
|
51
|
+
@labels['Bytes'] = plain_data.bytesize
|
52
|
+
@labels['Comment'] = comment if comment
|
53
|
+
|
54
|
+
@labels['SHA256'] = Digest::SHA256.hexdigest(plain_data)
|
55
|
+
|
56
|
+
@document = Paperback::Document.new
|
57
|
+
end
|
58
|
+
|
59
|
+
def log
|
60
|
+
@log ||= Paperback.class_log(self.class)
|
61
|
+
end
|
62
|
+
def self.log
|
63
|
+
@log ||= Paperback.class_log(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
def render(output_filename:)
|
67
|
+
log.debug('Preparer#render')
|
68
|
+
|
69
|
+
opts = {
|
70
|
+
labels: labels,
|
71
|
+
qr_code: qr_code,
|
72
|
+
sixword_lines: sixword_lines,
|
73
|
+
sixword_bytes: data.bytesize,
|
74
|
+
}
|
75
|
+
|
76
|
+
if encrypt
|
77
|
+
opts[:passphrase_sha] = self.class.truncated_sha256(passphrase)
|
78
|
+
opts[:passphrase_len] = passphrase.length
|
79
|
+
if passphrase_file
|
80
|
+
File.open(passphrase_file, File::CREAT|File::EXCL|File::WRONLY,
|
81
|
+
0400) do |f|
|
82
|
+
f.write(passphrase)
|
83
|
+
end
|
84
|
+
log.info("Wrote passphrase to #{passphrase_file.inspect}")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
@document.render(output_file: output_filename, draw_opts: opts)
|
89
|
+
|
90
|
+
log.info('Render complete')
|
91
|
+
|
92
|
+
if encrypt && !passphrase_file
|
93
|
+
puts "Passphrase: #{passphrase}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def passphrase
|
98
|
+
raise "Can't have passphrase without encrypt" unless encrypt
|
99
|
+
@passphrase ||= self.class.random_passphrase
|
100
|
+
end
|
101
|
+
|
102
|
+
PassChars = [*'a'..'z', *'A'..'Z', *'0'..'9'].freeze
|
103
|
+
|
104
|
+
def self.random_passphrase(entropy_bits: 256, char_set: PassChars)
|
105
|
+
chars_needed = (entropy_bits / Math.log2(char_set.length)).ceil
|
106
|
+
(0...chars_needed).map {
|
107
|
+
PassChars.fetch(SecureRandom.random_number(char_set.length))
|
108
|
+
}.join
|
109
|
+
end
|
110
|
+
|
111
|
+
# Compute a truncated SHA256 digest
|
112
|
+
def self.truncated_sha256(content)
|
113
|
+
Digest::SHA256.hexdigest(content)[0...16]
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.gpg_encrypt(filename:, password:)
|
117
|
+
cmd = %w[
|
118
|
+
gpg -c -o - --batch --cipher-algo aes256 --passphrase-fd 0 --
|
119
|
+
] + [filename]
|
120
|
+
out = nil
|
121
|
+
|
122
|
+
log.debug('+ ' + cmd.join(' '))
|
123
|
+
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
|
124
|
+
stdout: Subprocess::PIPE) do |p|
|
125
|
+
out, _err = p.communicate(password)
|
126
|
+
end
|
127
|
+
|
128
|
+
out
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.gpg_ascii_enarmor(data, strip_comments: true)
|
132
|
+
cmd = %w[gpg --batch --enarmor]
|
133
|
+
out = nil
|
134
|
+
|
135
|
+
log.debug('+ ' + cmd.join(' '))
|
136
|
+
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
|
137
|
+
stdout: Subprocess::PIPE) do |p|
|
138
|
+
out, _err = p.communicate(data)
|
139
|
+
end
|
140
|
+
|
141
|
+
if strip_comments
|
142
|
+
out = out.each_line.select { |l| !l.start_with?('Comment: ') }.join
|
143
|
+
end
|
144
|
+
|
145
|
+
out
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.gpg_ascii_dearmor(data)
|
149
|
+
cmd = %w[gpg --batch --dearmor]
|
150
|
+
out = nil
|
151
|
+
|
152
|
+
log.debug('+ ' + cmd.join(' '))
|
153
|
+
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
|
154
|
+
stdout: Subprocess::PIPE) do |p|
|
155
|
+
out, _err = p.communicate(data)
|
156
|
+
end
|
157
|
+
|
158
|
+
out
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def qr_code
|
164
|
+
@qr_code ||= qr_code!
|
165
|
+
end
|
166
|
+
|
167
|
+
def qr_code!
|
168
|
+
log.info('Generating QR code')
|
169
|
+
|
170
|
+
# Base64 encode data prior to QR encoding as requested
|
171
|
+
if qr_base64
|
172
|
+
if encrypt
|
173
|
+
# If data is already GPG encrypted, use GPG's base64 armor
|
174
|
+
input = self.class.gpg_ascii_enarmor(data)
|
175
|
+
else
|
176
|
+
# Otherwise do plain Base64
|
177
|
+
input = Base64.encode64(data)
|
178
|
+
end
|
179
|
+
else
|
180
|
+
input = data
|
181
|
+
end
|
182
|
+
|
183
|
+
# If QR level not specified, pick highest level of redundancy possible
|
184
|
+
# given the size of the input, up to Q (25% redundancy)
|
185
|
+
unless @qr_level
|
186
|
+
if input.bytesize <= 1663
|
187
|
+
@qr_level = :q
|
188
|
+
elsif input.bytesize <= 2331
|
189
|
+
@qr_level = :m
|
190
|
+
else
|
191
|
+
@qr_level = :l
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
log.debug("qr_level: #{@qr_level.inspect}")
|
196
|
+
RQRCode::QRCode.new(input, level: @qr_level)
|
197
|
+
end
|
198
|
+
|
199
|
+
def sixword_lines
|
200
|
+
log.info('Encoding with Sixword')
|
201
|
+
@sixword_lines ||=
|
202
|
+
Sixword.pad_encode_to_sentences(data).map(&:downcase)
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
end
|