stationery 0.11.0 → 0.12.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 +4 -4
- data/CHANGELOG.md +54 -68
- data/README.md +775 -78
- data/examples/accessible_report.rb +133 -0
- data/examples/article.rb +93 -0
- data/examples/assets/bay.png +0 -0
- data/examples/assets/dunes.png +0 -0
- data/examples/assets/hills.png +0 -0
- data/examples/assets/logo.png +0 -0
- data/examples/assets/ridge.png +0 -0
- data/examples/assets/sea.png +0 -0
- data/examples/assets/stone.png +0 -0
- data/examples/assets/wall.png +0 -0
- data/examples/certificate.rb +104 -0
- data/examples/contract.rb +159 -0
- data/examples/e_invoice.rb +161 -0
- data/examples/flyer.rb +336 -0
- data/examples/form.rb +113 -0
- data/examples/invoice.rb +163 -0
- data/examples/letter.rb +121 -0
- data/examples/menu.rb +133 -0
- data/examples/multilingual_notice.rb +121 -0
- data/examples/newsletter.rb +108 -0
- data/examples/packing_slip.rb +120 -0
- data/examples/postcard.rb +83 -0
- data/examples/price_list.rb +107 -0
- data/examples/receipt.rb +176 -0
- data/examples/report.rb +294 -0
- data/examples/resume.rb +121 -0
- data/examples/shaping/extraction_matrix/pdfium.py +8 -0
- data/examples/shaping/extraction_matrix/pdfjs.mjs +18 -0
- data/examples/shaping/extraction_matrix/pdfkit.swift +6 -0
- data/examples/shaping/extraction_matrix.rb +208 -0
- data/examples/shaping/harfbuzz_shaper.rb +132 -0
- data/examples/shaping/rtl_letter.rb +106 -0
- data/examples/shipping_label.rb +93 -0
- data/lib/stationery/barcode/code128.rb +96 -0
- data/lib/stationery/barcode/data_matrix.rb +111 -0
- data/lib/stationery/barcode/data_matrix_placement.rb +118 -0
- data/lib/stationery/barcode/ean13.rb +66 -0
- data/lib/stationery/barcode/qr.rb +120 -0
- data/lib/stationery/barcode/qr_matrix.rb +189 -0
- data/lib/stationery/barcode/reed_solomon.rb +61 -0
- data/lib/stationery/barcode.rb +33 -0
- data/lib/stationery/builder.rb +12 -2
- data/lib/stationery/canvas/glyphs.rb +40 -0
- data/lib/stationery/canvas/interface.rb +169 -0
- data/lib/stationery/canvas/marking.rb +32 -3
- data/lib/stationery/canvas/path.rb +32 -70
- data/lib/stationery/canvas/surfacing.rb +67 -0
- data/lib/stationery/canvas/text.rb +4 -25
- data/lib/stationery/canvas.rb +24 -75
- data/lib/stationery/cli/examples.rb +66 -0
- data/lib/stationery/cli/inspect.rb +65 -0
- data/lib/stationery/cli/layout_text.rb +132 -0
- data/lib/stationery/cli/pictures.rb +50 -0
- data/lib/stationery/cli/render.rb +33 -6
- data/lib/stationery/cli/skill.rb +166 -0
- data/lib/stationery/cli.rb +5 -0
- data/lib/stationery/color.rb +27 -13
- data/lib/stationery/component.rb +1 -0
- data/lib/stationery/css/stylesheet.rb +9 -1
- data/lib/stationery/document.rb +135 -27
- data/lib/stationery/elements/forms.rb +2 -1
- data/lib/stationery/elements/lists.rb +6 -2
- data/lib/stationery/elements.rb +43 -7
- data/lib/stationery/examples.rb +22 -0
- data/lib/stationery/fonts/cff.rb +46 -0
- data/lib/stationery/fonts/charstring.rb +297 -0
- data/lib/stationery/fonts/fallback.rb +22 -1
- data/lib/stationery/fonts/font.rb +58 -3
- data/lib/stationery/fonts/font_book.rb +7 -3
- data/lib/stationery/fonts/glyf_outline.rb +230 -0
- data/lib/stationery/fonts/glyph_run.rb +37 -10
- data/lib/stationery/fonts/outline.rb +102 -0
- data/lib/stationery/fonts/shaped_run.rb +22 -4
- data/lib/stationery/fonts/true_type.rb +7 -0
- data/lib/stationery/forms/typeface.rb +2 -4
- data/lib/stationery/html/css.rb +31 -6
- data/lib/stationery/html/tree_builder/blocks.rb +7 -3
- data/lib/stationery/images/images.rb +10 -0
- data/lib/stationery/images/jpeg/bit_reader.rb +92 -0
- data/lib/stationery/images/jpeg/colors.rb +133 -0
- data/lib/stationery/images/jpeg/decoder.rb +253 -0
- data/lib/stationery/images/jpeg/huffman.rb +59 -0
- data/lib/stationery/images/jpeg/idct.rb +150 -0
- data/lib/stationery/images/jpeg/progressive.rb +128 -0
- data/lib/stationery/images/jpeg/reduced_idct.rb +106 -0
- data/lib/stationery/images/jpeg/scan.rb +116 -0
- data/lib/stationery/images/jpeg/upsampler.rb +81 -0
- data/lib/stationery/images/jpeg.rb +59 -1
- data/lib/stationery/images/png.rb +10 -7
- data/lib/stationery/images/resampled.rb +10 -2
- data/lib/stationery/images/webp.rb +6 -5
- data/lib/stationery/layout/barcode.rb +75 -0
- data/lib/stationery/layout/box/wrapping.rb +56 -0
- data/lib/stationery/layout/box.rb +41 -15
- data/lib/stationery/layout/columns/leveller.rb +65 -0
- data/lib/stationery/layout/columns/pour.rb +7 -0
- data/lib/stationery/layout/columns.rb +32 -15
- data/lib/stationery/layout/field.rb +3 -2
- data/lib/stationery/layout/floated.rb +12 -1
- data/lib/stationery/layout/flow/floating.rb +23 -5
- data/lib/stationery/layout/flow/placement.rb +16 -12
- data/lib/stationery/layout/flow/splitter.rb +56 -10
- data/lib/stationery/layout/flow.rb +1 -0
- data/lib/stationery/layout/image.rb +2 -2
- data/lib/stationery/layout/list_item.rb +41 -11
- data/lib/stationery/layout/mark.rb +2 -0
- data/lib/stationery/layout/node.rb +16 -2
- data/lib/stationery/layout/opening.rb +61 -0
- data/lib/stationery/layout/paginator.rb +24 -5
- data/lib/stationery/layout/svg.rb +1 -1
- data/lib/stationery/layout/table/cell.rb +39 -10
- data/lib/stationery/layout/table/row_splitter.rb +6 -2
- data/lib/stationery/layout/table/selection.rb +9 -3
- data/lib/stationery/layout/table/stream.rb +123 -0
- data/lib/stationery/layout/table/widths.rb +26 -0
- data/lib/stationery/layout/table.rb +151 -35
- data/lib/stationery/layout/table_of_contents/entry.rb +3 -3
- data/lib/stationery/layout/table_of_contents.rb +1 -1
- data/lib/stationery/layout/text.rb +22 -2
- data/lib/stationery/minitest.rb +11 -3
- data/lib/stationery/monochrome/bitmap.rb +78 -0
- data/lib/stationery/monochrome/canvas.rb +19 -0
- data/lib/stationery/monochrome/dither.rb +74 -0
- data/lib/stationery/monochrome/grid.rb +131 -0
- data/lib/stationery/monochrome/painting.rb +140 -0
- data/lib/stationery/monochrome/rules.rb +141 -0
- data/lib/stationery/monochrome.rb +87 -0
- data/lib/stationery/page/format.rb +84 -0
- data/lib/stationery/page.rb +11 -7
- data/lib/stationery/page_templates.rb +13 -10
- data/lib/stationery/path.rb +138 -0
- data/lib/stationery/pdf/assembler.rb +14 -3
- data/lib/stationery/pdf/canvases.rb +51 -0
- data/lib/stationery/pdf/conformance.rb +43 -13
- data/lib/stationery/pdf/print_hints.rb +137 -0
- data/lib/stationery/pdf/serializer.rb +41 -0
- data/lib/stationery/raster/adjust.rb +67 -0
- data/lib/stationery/raster/canvas.rb +108 -0
- data/lib/stationery/raster/canvases.rb +49 -0
- data/lib/stationery/raster/flattener.rb +75 -0
- data/lib/stationery/raster/glyphs.rb +81 -0
- data/lib/stationery/raster/painter.rb +184 -0
- data/lib/stationery/raster/picture.rb +69 -0
- data/lib/stationery/raster/png.rb +32 -0
- data/lib/stationery/raster/render.rb +134 -0
- data/lib/stationery/raster/scanner.rb +164 -0
- data/lib/stationery/raster/shading.rb +77 -0
- data/lib/stationery/raster/spans.rb +66 -0
- data/lib/stationery/raster/stroker.rb +208 -0
- data/lib/stationery/raster/surface.rb +70 -0
- data/lib/stationery/raster.rb +44 -0
- data/lib/stationery/rich/nodes.rb +4 -4
- data/lib/stationery/rich/renderer/boxes.rb +37 -16
- data/lib/stationery/rich/renderer.rb +3 -2
- data/lib/stationery/skill/SKILL.md.erb +244 -0
- data/lib/stationery/skill/recipes/flyer.md.erb +76 -0
- data/lib/stationery/skill/recipes/form.md.erb +78 -0
- data/lib/stationery/skill/recipes/invoice.md.erb +142 -0
- data/lib/stationery/skill/recipes/label.md.erb +116 -0
- data/lib/stationery/skill/recipes/receipt.md.erb +79 -0
- data/lib/stationery/skill/recipes/report.md.erb +113 -0
- data/lib/stationery/skill/reference/conformance.md.erb +45 -0
- data/lib/stationery/skill/reference/elements.md.erb +19 -0
- data/lib/stationery/skill/reference/fonts_and_images.md.erb +7 -0
- data/lib/stationery/skill/reference/forms.md.erb +5 -0
- data/lib/stationery/skill/reference/html_and_markdown.md.erb +5 -0
- data/lib/stationery/skill/reference/pages.md.erb +15 -0
- data/lib/stationery/skill/reference/printing.md.erb +33 -0
- data/lib/stationery/skill/reference/testing.md.erb +33 -0
- data/lib/stationery/skill.rb +101 -0
- data/lib/stationery/structure.rb +7 -6
- data/lib/stationery/svg/gradient.rb +12 -0
- data/lib/stationery/svg/painter.rb +1 -1
- data/lib/stationery/tagging/element.rb +9 -2
- data/lib/stationery/tagging/tree.rb +98 -2
- data/lib/stationery/testing/field_page.rb +89 -0
- data/lib/stationery/testing/inspector.rb +80 -2
- data/lib/stationery/testing/layout.rb +166 -0
- data/lib/stationery/testing/marked_text.rb +124 -18
- data/lib/stationery/testing/matchers.rb +63 -14
- data/lib/stationery/testing/page_reader.rb +146 -0
- data/lib/stationery/text/exclusions.rb +11 -0
- data/lib/stationery/text/paragraph.rb +13 -8
- data/lib/stationery/text/wrapper/one_word.rb +33 -0
- data/lib/stationery/text/wrapper.rb +62 -27
- data/lib/stationery/units.rb +45 -0
- data/lib/stationery/version.rb +1 -1
- data/lib/stationery/warnings.rb +67 -7
- data/lib/stationery/zpl/native.rb +51 -0
- data/lib/stationery/zpl/render.rb +56 -0
- data/lib/stationery/zpl.rb +88 -0
- data/lib/stationery.rb +48 -0
- metadata +118 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A 4 × 6 in shipping label for a thermal label printer, in black and white
|
|
4
|
+
# only, with a Code 128 and a QR code, written as a PDF and as ZPL whose
|
|
5
|
+
# barcodes the printer draws itself (`native: true`). Run it to write
|
|
6
|
+
# examples/shipping_label.pdf and examples/shipping_label.zpl:
|
|
7
|
+
#
|
|
8
|
+
# ruby -Ilib examples/shipping_label.rb
|
|
9
|
+
# ruby -Ilib exe/stationery render examples/shipping_label.rb --zpl --dpi 203
|
|
10
|
+
require "stationery"
|
|
11
|
+
|
|
12
|
+
class ExampleShippingLabel < Stationery::Document
|
|
13
|
+
page size: "4in x 6in", margin: mm(4)
|
|
14
|
+
max_pages 1 # one label: what does not fit warns, and to_zpl writes no second label
|
|
15
|
+
monochrome dpi: 203
|
|
16
|
+
default_text size: 10
|
|
17
|
+
metadata title: "Shipping label", creator: "stationery example"
|
|
18
|
+
|
|
19
|
+
def self.preview
|
|
20
|
+
new(parcel: "SX 0042 7719 03", service: "EXPRESS", weight: "2.4 kg", route: "R-07",
|
|
21
|
+
from: ["Northwind Supplies", "Unit 5, Harbour Road", "40115 Gothenburg", "Sweden"],
|
|
22
|
+
to: ["Ada Example", "Example Workshop Ltd", "12 Sample Street", "3011 Testville", "Netherlands"])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(parcel:, service:, weight:, route:, from:, to:)
|
|
26
|
+
super()
|
|
27
|
+
@parcel = parcel
|
|
28
|
+
@service = service
|
|
29
|
+
@weight = weight
|
|
30
|
+
@route = route
|
|
31
|
+
@from = from
|
|
32
|
+
@to = to
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def view_template
|
|
36
|
+
row(gap: 8) do
|
|
37
|
+
column(width: 0.62) { address("FROM", @from, size: 9) }
|
|
38
|
+
column { service }
|
|
39
|
+
end
|
|
40
|
+
spacer 8
|
|
41
|
+
rule height: 2
|
|
42
|
+
spacer 10
|
|
43
|
+
address("SHIP TO", @to, size: 14)
|
|
44
|
+
spacer 12
|
|
45
|
+
rule height: 2
|
|
46
|
+
spacer 10
|
|
47
|
+
row(gap: 8, align: :center) do
|
|
48
|
+
column(width: 0.36) { detail("ROUTE", @route) }
|
|
49
|
+
column(width: 0.36) { detail("WEIGHT", @weight) }
|
|
50
|
+
column { barcode "https://track.example/#{code}", type: :qr, module_size: 1.4, align: :right, native: true }
|
|
51
|
+
end
|
|
52
|
+
spacer 10
|
|
53
|
+
box(border: { width: 3 }, padding: 10) do
|
|
54
|
+
text "PARCEL", size: 8, weight: :bold
|
|
55
|
+
spacer 6
|
|
56
|
+
barcode code, module_size: 1.5, height: 44, align: :center, native: true
|
|
57
|
+
spacer 4
|
|
58
|
+
text @parcel, size: 14, weight: :bold, letter_spacing: 1, align: :center
|
|
59
|
+
end
|
|
60
|
+
spacer 10
|
|
61
|
+
text "Keep dry · This side up", size: 9, align: :center
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def code = @parcel.delete(" ")
|
|
67
|
+
|
|
68
|
+
def address(label, lines, size:)
|
|
69
|
+
text label, size: 8, weight: :bold, letter_spacing: 0.6
|
|
70
|
+
spacer 3
|
|
71
|
+
text "<b>#{lines.first}</b>\n#{lines.drop(1).join("\n")}", size:, markup: true, leading: 1.5
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def service
|
|
75
|
+
box(background: "#000000", padding: [10, 6]) do
|
|
76
|
+
text @service, size: 16, weight: :bold, color: "#FFFFFF", align: :center
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def detail(label, value)
|
|
81
|
+
text label, size: 8, weight: :bold, letter_spacing: 0.6
|
|
82
|
+
spacer 2
|
|
83
|
+
text value, size: 20, weight: :bold
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if $PROGRAM_NAME == __FILE__
|
|
88
|
+
label = ExampleShippingLabel.preview
|
|
89
|
+
label.to_pdf(File.expand_path("shipping_label.pdf", __dir__))
|
|
90
|
+
label.to_zpl(File.expand_path("shipping_label.zpl", __dir__))
|
|
91
|
+
warn label.warnings.map(&:message) if label.warnings.any?
|
|
92
|
+
puts "wrote examples/shipping_label.pdf and examples/shipping_label.zpl"
|
|
93
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Barcode
|
|
5
|
+
# Code 128 (ISO/IEC 15417) of printable ASCII (" " to "~"): code set B,
|
|
6
|
+
# switching to code set C (two digits a symbol) for a run of at least six
|
|
7
|
+
# digits, or four at either end. Bars and spaces are 1 to 4 modules wide;
|
|
8
|
+
# the symbol is 11 modules a character, 13 for the stop, with a quiet
|
|
9
|
+
# zone of 10 modules on either side.
|
|
10
|
+
class Code128
|
|
11
|
+
PATTERNS = %w[
|
|
12
|
+
212222 222122 222221 121223 121322 131222 122213 122312 132212 221213 221312 231212 112232 122132 122231
|
|
13
|
+
113222 123122 123221 223211 221132 221231 213212 223112 312131 311222 321122 321221 312212 322112 322211
|
|
14
|
+
212123 212321 232121 111323 131123 131321 112313 132113 132311 211313 231113 231311 112133 112331 132131
|
|
15
|
+
113123 113321 133121 313121 211331 231131 213113 213311 213131 311123 311321 331121 312113 312311 332111
|
|
16
|
+
314111 221411 431111 111224 111422 121124 121421 141122 141221 112214 112412 122114 122411 142112 142211
|
|
17
|
+
241211 221114 413111 241112 134111 111242 121142 121241 114212 124112 124211 411212 421112 421211 212141
|
|
18
|
+
214121 412121 111143 111341 131141 114113 114311 411113 411311 113141 114131 311141 411131 211412 211214
|
|
19
|
+
211232 2331112
|
|
20
|
+
].freeze
|
|
21
|
+
START_B = 104
|
|
22
|
+
START_C = 105
|
|
23
|
+
CODE_B = 100
|
|
24
|
+
CODE_C = 99
|
|
25
|
+
STOP = 106
|
|
26
|
+
QUIET = 10
|
|
27
|
+
# How ZPL's ^BC (mode N) is told to start or change code set.
|
|
28
|
+
ZPL_SET = { START_B => ">:", START_C => ">;", CODE_B => ">6", CODE_C => ">5" }.freeze
|
|
29
|
+
|
|
30
|
+
attr_reader :data, :codes
|
|
31
|
+
|
|
32
|
+
def initialize(data)
|
|
33
|
+
@data = data.to_s
|
|
34
|
+
raise ArgumentError, "a Code 128 barcode needs data" if @data.empty?
|
|
35
|
+
|
|
36
|
+
bad = @data.each_char.find { |char| !char.ord.between?(32, 126) }
|
|
37
|
+
raise ArgumentError, "Code 128 takes printable ASCII (\" \" to \"~\"), not #{bad.inspect}" if bad
|
|
38
|
+
|
|
39
|
+
@parts = parts
|
|
40
|
+
@codes = checked(@parts.flat_map { |set, text| [set, *values(set, text)] })
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def linear? = true
|
|
44
|
+
def native? = true
|
|
45
|
+
# Dots ZPL draws the symbol below its ^FO.
|
|
46
|
+
def zpl_top = 0
|
|
47
|
+
def quiet = QUIET
|
|
48
|
+
|
|
49
|
+
# Modules across, without the quiet zones.
|
|
50
|
+
def width = ((@codes.size - 1) * 11) + 13
|
|
51
|
+
|
|
52
|
+
# The bars, as [first module, modules wide].
|
|
53
|
+
def bars
|
|
54
|
+
x = 0
|
|
55
|
+
@codes.flat_map { |code| PATTERNS[code].chars.map(&:to_i) }.each_slice(2).filter_map do |bar, space|
|
|
56
|
+
[x, bar].tap { x += bar + space.to_i }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The ^BC command drawing the same symbol: its code sets named, so the
|
|
61
|
+
# printer does not choose others.
|
|
62
|
+
def zpl(module_dots:, height_dots:)
|
|
63
|
+
text = @parts.map { |set, part| ZPL_SET.fetch(set) + part.gsub(">", "><") }.join
|
|
64
|
+
"^BY#{module_dots}^BCN,#{height_dots},N,N,N,N#{Barcode.field(text)}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# The data as [set, text] runs, the first set being a start code.
|
|
70
|
+
def parts
|
|
71
|
+
runs = @data.scan(/\d+|\D+/)
|
|
72
|
+
pieces = runs.each_with_index.flat_map do |run, index|
|
|
73
|
+
edge = index.zero? || index == runs.size - 1
|
|
74
|
+
next [[CODE_B, run]] unless run.match?(/\A\d+\z/) && run.size >= (edge ? 4 : 6)
|
|
75
|
+
|
|
76
|
+
even = run.size.even? ? run : run[...-1]
|
|
77
|
+
[[CODE_C, even], *([[CODE_B, run[-1]]] if even.size < run.size)]
|
|
78
|
+
end
|
|
79
|
+
list = pieces.chunk_while { |a, b| a[0] == b[0] }.map { |group| [group[0][0], group.map(&:last).join] }
|
|
80
|
+
list[0][0] = list[0][0] == CODE_C ? START_C : START_B
|
|
81
|
+
list
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def values(set, text)
|
|
85
|
+
return text.scan(/\d\d/).map(&:to_i) if [START_C, CODE_C].include?(set)
|
|
86
|
+
|
|
87
|
+
text.each_char.map { |char| char.ord - 32 }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def checked(codes)
|
|
91
|
+
sum = codes.each_with_index.sum { |code, index| code * [index, 1].max }
|
|
92
|
+
[*codes, sum % 103, STOP]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Barcode
|
|
5
|
+
# A Data Matrix symbol (ISO/IEC 16022, ECC 200) of the data's bytes in
|
|
6
|
+
# ASCII encodation (a character a codeword, two digits in one, a byte
|
|
7
|
+
# above 127 after an upper shift), in the smallest of the 24 square
|
|
8
|
+
# sizes from 10 × 10 to 144 × 144 that holds it. The codewords are laid
|
|
9
|
+
# out by the placement of the standard's annex F, in data regions each
|
|
10
|
+
# framed by its finder (solid left and bottom) and clock track
|
|
11
|
+
# (alternating top and right). The quiet zone is 1 module.
|
|
12
|
+
class DataMatrix
|
|
13
|
+
# Symbol size => [data codewords, error correction codewords, data
|
|
14
|
+
# regions across, modules across a region, interleaved blocks].
|
|
15
|
+
SIZES = {
|
|
16
|
+
10 => [3, 5, 1, 8, 1], 12 => [5, 7, 1, 10, 1], 14 => [8, 10, 1, 12, 1], 16 => [12, 12, 1, 14, 1],
|
|
17
|
+
18 => [18, 14, 1, 16, 1], 20 => [22, 18, 1, 18, 1], 22 => [30, 20, 1, 20, 1], 24 => [36, 24, 1, 22, 1],
|
|
18
|
+
26 => [44, 28, 1, 24, 1], 32 => [62, 36, 2, 14, 1], 36 => [86, 42, 2, 16, 1], 40 => [114, 48, 2, 18, 1],
|
|
19
|
+
44 => [144, 56, 2, 20, 1], 48 => [174, 68, 2, 22, 1], 52 => [204, 84, 2, 24, 2], 64 => [280, 112, 4, 14, 2],
|
|
20
|
+
72 => [368, 144, 4, 16, 4], 80 => [456, 192, 4, 18, 4], 88 => [576, 224, 4, 20, 4],
|
|
21
|
+
96 => [696, 272, 4, 22, 4], 104 => [816, 336, 4, 24, 6], 120 => [1050, 408, 6, 18, 6],
|
|
22
|
+
132 => [1304, 496, 6, 20, 8], 144 => [1558, 620, 6, 22, 10]
|
|
23
|
+
}.freeze
|
|
24
|
+
PAD = 129
|
|
25
|
+
UPPER_SHIFT = 235
|
|
26
|
+
QUIET = 1
|
|
27
|
+
# The escape character ^BX is given: data holding it is left to the picture.
|
|
28
|
+
ZPL_ESCAPE = "_"
|
|
29
|
+
|
|
30
|
+
attr_reader :data, :size, :data_codewords, :codewords, :modules
|
|
31
|
+
|
|
32
|
+
# ASCII encodation of `data`'s bytes.
|
|
33
|
+
def self.codewords(data)
|
|
34
|
+
data.to_s.b.scan(/\d\d|./mn).flat_map do |chunk|
|
|
35
|
+
next [130 + chunk.to_i] if chunk.size == 2
|
|
36
|
+
|
|
37
|
+
byte = chunk.ord
|
|
38
|
+
byte < 128 ? [byte + 1] : [UPPER_SHIFT, byte - 127]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(data)
|
|
43
|
+
@data = data.to_s
|
|
44
|
+
raise ArgumentError, "a Data Matrix needs data" if @data.empty?
|
|
45
|
+
|
|
46
|
+
encoded = self.class.codewords(@data)
|
|
47
|
+
@size, (capacity, ecc, regions, region, blocks) = SIZES.find { |_, spec| encoded.size <= spec[0] }
|
|
48
|
+
raise ArgumentError, "#{encoded.size} codewords are more than a Data Matrix holds (1558)" unless @size
|
|
49
|
+
|
|
50
|
+
@data_codewords = padded(encoded, capacity)
|
|
51
|
+
@codewords = @data_codewords + check(@data_codewords, ecc, blocks)
|
|
52
|
+
@modules = frame(place(regions * region), region)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def linear? = false
|
|
56
|
+
def quiet = QUIET
|
|
57
|
+
def native? = !@data.include?(ZPL_ESCAPE)
|
|
58
|
+
def zpl_top = 0
|
|
59
|
+
|
|
60
|
+
# ^BX at this size, so the printer draws as many modules.
|
|
61
|
+
def zpl(module_dots:, height_dots: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
62
|
+
"^BXN,#{module_dots},200,#{@size},#{@size},6,#{ZPL_ESCAPE},1#{Barcode.field(@data)}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# The first pad is 129, the others scrambled by their position (the
|
|
68
|
+
# 253-state algorithm, annex B.1.2).
|
|
69
|
+
def padded(encoded, capacity)
|
|
70
|
+
result = encoded.dup
|
|
71
|
+
result << PAD if result.size < capacity
|
|
72
|
+
while result.size < capacity
|
|
73
|
+
value = PAD + ((149 * (result.size + 1)) % 253) + 1
|
|
74
|
+
result << (value > 254 ? value - 254 : value)
|
|
75
|
+
end
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The error correction of each block (the data taken a codeword to a
|
|
80
|
+
# block in turn), interleaved the same way.
|
|
81
|
+
def check(data, ecc, blocks)
|
|
82
|
+
per_block = ecc / blocks
|
|
83
|
+
checks = Array.new(blocks) do |block|
|
|
84
|
+
ReedSolomon::DATA_MATRIX.remainder(data.select.with_index { |_, i| i % blocks == block }, per_block)
|
|
85
|
+
end
|
|
86
|
+
(0...per_block).flat_map { |i| checks.map { |block| block[i] } }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The mapping matrix (the data regions without their frames) of
|
|
90
|
+
# `count` modules across, true for dark.
|
|
91
|
+
def place(count)
|
|
92
|
+
Placement.new(count, @codewords).grid
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def frame(grid, region)
|
|
96
|
+
step = region + 2
|
|
97
|
+
Array.new(@size) do |y|
|
|
98
|
+
Array.new(@size) do |x|
|
|
99
|
+
ry, dy = y.divmod(step)
|
|
100
|
+
rx, dx = x.divmod(step)
|
|
101
|
+
if dx.zero? || dy == step - 1 then true
|
|
102
|
+
elsif dy.zero? then dx.even?
|
|
103
|
+
elsif dx == step - 1 then dy.odd?
|
|
104
|
+
else grid[(ry * region) + dy - 1][(rx * region) + dx - 1]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Barcode
|
|
5
|
+
class DataMatrix
|
|
6
|
+
# The ECC 200 placement of ISO/IEC 16022 annex F: codewords laid on a
|
|
7
|
+
# mapping matrix of `count` × `count` modules along diagonal strokes,
|
|
8
|
+
# eight modules each (the "utah" shape), with the four corner shapes
|
|
9
|
+
# where a stroke meets the edge, and the fixed pattern in the corner
|
|
10
|
+
# left over.
|
|
11
|
+
class Placement
|
|
12
|
+
UTAH = [[-2, -2], [-2, -1], [-1, -2], [-1, -1], [-1, 0], [0, -2], [0, -1], [0, 0]].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :grid
|
|
15
|
+
|
|
16
|
+
def initialize(count, codewords)
|
|
17
|
+
@rows = @cols = count
|
|
18
|
+
@codewords = codewords
|
|
19
|
+
@grid = Array.new(count) { Array.new(count) }
|
|
20
|
+
place
|
|
21
|
+
@grid.each { |row| row.map! { |cell| cell == true } }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def place
|
|
27
|
+
chr = 0
|
|
28
|
+
row = 4
|
|
29
|
+
col = 0
|
|
30
|
+
loop do
|
|
31
|
+
chr = corners(row, col, chr)
|
|
32
|
+
loop do
|
|
33
|
+
chr = utah(row, col, chr) if row < @rows && col >= 0 && @grid[row][col].nil?
|
|
34
|
+
row -= 2
|
|
35
|
+
col += 2
|
|
36
|
+
break unless row >= 0 && col < @cols
|
|
37
|
+
end
|
|
38
|
+
row += 1
|
|
39
|
+
col += 3
|
|
40
|
+
loop do
|
|
41
|
+
chr = utah(row, col, chr) if row >= 0 && col < @cols && @grid[row][col].nil?
|
|
42
|
+
row += 2
|
|
43
|
+
col -= 2
|
|
44
|
+
break unless row < @rows && col >= 0
|
|
45
|
+
end
|
|
46
|
+
row += 3
|
|
47
|
+
col += 1
|
|
48
|
+
break unless row < @rows || col < @cols
|
|
49
|
+
end
|
|
50
|
+
fill_corner
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def corners(row, col, chr)
|
|
54
|
+
shape = col.zero? ? left_corner(row) : (corner4 if col == 2 && row == @rows + 4 && (@cols % 8).zero?)
|
|
55
|
+
return chr unless shape
|
|
56
|
+
|
|
57
|
+
shape.each_with_index { |(r, c), bit| put(r, c, chr, bit) }
|
|
58
|
+
chr + 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def left_corner(row)
|
|
62
|
+
if row == @rows then corner1
|
|
63
|
+
elsif row == @rows - 2 && (@cols % 4).positive? then corner2
|
|
64
|
+
elsif row == @rows - 2 && @cols % 8 == 4 then corner3
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def corner1
|
|
69
|
+
[[@rows - 1, 0], [@rows - 1, 1], [@rows - 1, 2], [0, @cols - 2], [0, @cols - 1], [1, @cols - 1],
|
|
70
|
+
[2, @cols - 1], [3, @cols - 1]]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def corner2
|
|
74
|
+
[[@rows - 3, 0], [@rows - 2, 0], [@rows - 1, 0], [0, @cols - 4], [0, @cols - 3], [0, @cols - 2],
|
|
75
|
+
[0, @cols - 1], [1, @cols - 1]]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def corner3
|
|
79
|
+
[[@rows - 3, 0], [@rows - 2, 0], [@rows - 1, 0], [0, @cols - 2], [0, @cols - 1], [1, @cols - 1],
|
|
80
|
+
[2, @cols - 1], [3, @cols - 1]]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def corner4
|
|
84
|
+
[[@rows - 1, 0], [@rows - 1, @cols - 1], [0, @cols - 3], [0, @cols - 2], [0, @cols - 1], [1, @cols - 3],
|
|
85
|
+
[1, @cols - 2], [1, @cols - 1]]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def utah(row, col, chr)
|
|
89
|
+
UTAH.each_with_index { |(dr, dc), bit| put(row + dr, col + dc, chr, bit) }
|
|
90
|
+
chr + 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Bit `bit` (0 the most significant) of codeword `chr` at (row, col),
|
|
94
|
+
# wrapped around the edges as the annex wraps them.
|
|
95
|
+
def put(row, col, chr, bit)
|
|
96
|
+
if row.negative?
|
|
97
|
+
row += @rows
|
|
98
|
+
col += 4 - ((@rows + 4) % 8)
|
|
99
|
+
end
|
|
100
|
+
if col.negative?
|
|
101
|
+
col += @cols
|
|
102
|
+
row += 4 - ((@cols + 4) % 8)
|
|
103
|
+
end
|
|
104
|
+
codeword = @codewords[chr] || 0
|
|
105
|
+
@grid[row][col] = codeword[7 - bit] == 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The bottom right corner, when no codeword reached it.
|
|
109
|
+
def fill_corner
|
|
110
|
+
return unless @grid[@rows - 1][@cols - 1].nil?
|
|
111
|
+
|
|
112
|
+
@grid[@rows - 1][@cols - 1] = @grid[@rows - 2][@cols - 2] = true
|
|
113
|
+
@grid[@rows - 1][@cols - 2] = @grid[@rows - 2][@cols - 1] = false
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Barcode
|
|
5
|
+
# EAN-13 (ISO/IEC 15420): twelve digits and the check digit, which is
|
|
6
|
+
# computed when twelve are given and checked when thirteen are. 95
|
|
7
|
+
# modules across between guard bars, with a quiet zone of 11 modules on
|
|
8
|
+
# the left and 7 on the right; the digits under it are not drawn (write
|
|
9
|
+
# them with `text` when they are wanted).
|
|
10
|
+
class EAN13
|
|
11
|
+
L = %w[0001101 0011001 0010011 0111101 0100011 0110001 0101111 0111011 0110111 0001011].freeze
|
|
12
|
+
R = L.map { |code| code.tr("01", "10") }.freeze
|
|
13
|
+
G = R.map(&:reverse).freeze
|
|
14
|
+
# Which of L and G the six digits on the left use, by the first digit.
|
|
15
|
+
PARITY = %w[LLLLLL LLGLGG LLGGLG LLGGGL LGLLGG LGGLLG LGGGLL LGLGLG LGLGGL LGGLGL].freeze
|
|
16
|
+
QUIET = 11
|
|
17
|
+
|
|
18
|
+
attr_reader :data
|
|
19
|
+
|
|
20
|
+
def self.check_digit(digits)
|
|
21
|
+
sum = digits.chars.each_with_index.sum { |digit, index| digit.to_i * (index.odd? ? 3 : 1) }
|
|
22
|
+
((10 - (sum % 10)) % 10).to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(data)
|
|
26
|
+
digits = data.to_s
|
|
27
|
+
raise ArgumentError, "EAN-13 takes 12 or 13 digits, not #{data.inspect}" unless digits.match?(/\A\d{12,13}\z/)
|
|
28
|
+
|
|
29
|
+
check = self.class.check_digit(digits[0, 12])
|
|
30
|
+
if digits.size == 13 && digits[12] != check
|
|
31
|
+
raise ArgumentError, "the check digit of #{digits} is #{check}, not #{digits[12]}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@data = digits[0, 12] + check
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def linear? = true
|
|
38
|
+
def native? = true
|
|
39
|
+
# Dots ZPL draws the symbol below its ^FO.
|
|
40
|
+
def zpl_top = 0
|
|
41
|
+
def quiet = QUIET
|
|
42
|
+
def width = 95
|
|
43
|
+
|
|
44
|
+
def bars
|
|
45
|
+
pattern.chars.each_with_index.chunk_while { |a, b| a[0] == b[0] }.filter_map do |run|
|
|
46
|
+
[run[0][1], run.size] if run[0][0] == "1"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# ^BE, which adds the check digit itself.
|
|
51
|
+
def zpl(module_dots:, height_dots:)
|
|
52
|
+
"^BY#{module_dots}^BEN,#{height_dots},N,N#{Barcode.field(@data[0, 12])}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def pattern
|
|
58
|
+
left = @data[1, 6].chars.each_with_index.map do |digit, index|
|
|
59
|
+
(PARITY[@data[0].to_i][index] == "L" ? L : G)[digit.to_i]
|
|
60
|
+
end
|
|
61
|
+
right = @data[7, 6].chars.map { |digit| R[digit.to_i] }
|
|
62
|
+
"101#{left.join}01010#{right.join}101"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stationery
|
|
4
|
+
module Barcode
|
|
5
|
+
# A QR Code (ISO/IEC 18004, model 2) of the data's bytes (UTF-8 for a
|
|
6
|
+
# String, marked as UTF-8 with an ECI when it is not ASCII), in byte
|
|
7
|
+
# mode, at the smallest version (1 to 40) that holds it at the error
|
|
8
|
+
# correction `level:` (:l, :m, :q or :h: 7, 15, 25 or 30 % of the
|
|
9
|
+
# symbol may be lost). The modules are laid out by QR::Matrix, which
|
|
10
|
+
# also picks the mask; the quiet zone is 4 modules.
|
|
11
|
+
class QR
|
|
12
|
+
LEVELS = %i[l m q h].freeze
|
|
13
|
+
# Error correction codewords a block, and blocks, by level and version.
|
|
14
|
+
ECC_PER_BLOCK = {
|
|
15
|
+
l: [7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30,
|
|
16
|
+
30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
17
|
+
m: [10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28,
|
|
18
|
+
28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
|
|
19
|
+
q: [13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28,
|
|
20
|
+
30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
21
|
+
h: [17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30,
|
|
22
|
+
30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]
|
|
23
|
+
}.freeze
|
|
24
|
+
BLOCKS = {
|
|
25
|
+
l: [1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18,
|
|
26
|
+
19, 19, 20, 21, 22, 24, 25],
|
|
27
|
+
m: [1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29,
|
|
28
|
+
31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
|
|
29
|
+
q: [1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38,
|
|
30
|
+
40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
|
|
31
|
+
h: [1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45,
|
|
32
|
+
48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81]
|
|
33
|
+
}.freeze
|
|
34
|
+
QUIET = 4
|
|
35
|
+
PADS = [0xEC, 0x11].freeze
|
|
36
|
+
# An ECI segment naming UTF-8 (assignment 26), before text that is not ASCII.
|
|
37
|
+
ECI_UTF8 = "011100011010"
|
|
38
|
+
|
|
39
|
+
attr_reader :data, :level, :version, :matrix
|
|
40
|
+
|
|
41
|
+
# The data codewords of `version` at `level`.
|
|
42
|
+
def self.capacity(version, level)
|
|
43
|
+
(Matrix.raw_modules(version) / 8) - (ECC_PER_BLOCK.fetch(level)[version - 1] * BLOCKS.fetch(level)[version - 1])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The bytes byte mode holds at `version` and `level`: 4 bits of mode
|
|
47
|
+
# and 8 or 16 of length come first, and 12 of ECI with `eci:`.
|
|
48
|
+
def self.bytes(version, level, eci: false)
|
|
49
|
+
((capacity(version, level) * 8) - 4 - (version < 10 ? 8 : 16) - (eci ? 12 : 0)) / 8
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def initialize(data, level: :m)
|
|
53
|
+
raise ArgumentError, "a QR code's level: is :l, :m, :q or :h, not #{level.inspect}" unless
|
|
54
|
+
LEVELS.include?(level)
|
|
55
|
+
|
|
56
|
+
@data = data.to_s
|
|
57
|
+
@level = level
|
|
58
|
+
bytes = @data.b
|
|
59
|
+
raise ArgumentError, "a QR code needs data" if bytes.empty?
|
|
60
|
+
|
|
61
|
+
@eci = !bytes.ascii_only? && @data.encoding == Encoding::UTF_8
|
|
62
|
+
@version = (1..40).find { |version| bytes.bytesize <= self.class.bytes(version, level, eci: @eci) } or
|
|
63
|
+
raise ArgumentError, "#{bytes.bytesize} bytes are more than a QR code holds at level #{level.inspect}"
|
|
64
|
+
@matrix = Matrix.new(@version, level, codewords(bytes))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def linear? = false
|
|
68
|
+
# Whether a printer's ^BQ draws the same data: not UTF-8 text, which
|
|
69
|
+
# this symbol marks with an ECI (26) that ^BQ has no field for.
|
|
70
|
+
def native? = !@eci
|
|
71
|
+
# Dots ^BQ draws the symbol below its ^FO (as Labelary renders it,
|
|
72
|
+
# at any magnification and resolution).
|
|
73
|
+
def zpl_top = 10
|
|
74
|
+
def quiet = QUIET
|
|
75
|
+
def size = @matrix.size
|
|
76
|
+
|
|
77
|
+
# The dark modules, row by row, true for dark.
|
|
78
|
+
def modules = @matrix.modules
|
|
79
|
+
|
|
80
|
+
# ^BQ in manual byte mode, so the printer encodes the bytes as they
|
|
81
|
+
# are, at this level and so this version (its mask may differ).
|
|
82
|
+
def zpl(module_dots:, height_dots: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
83
|
+
bytes = @data.b
|
|
84
|
+
"^BQN,2,#{module_dots}#{Barcode.field("#{@level.upcase}M,B#{format("%04d", bytes.bytesize)}#{bytes}")}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def codewords(bytes)
|
|
90
|
+
bits = [(@eci ? ECI_UTF8 : ""), "0100", bytes.bytesize.to_s(2).rjust(@version < 10 ? 8 : 16, "0"),
|
|
91
|
+
bytes.unpack1("B*")].join
|
|
92
|
+
capacity = self.class.capacity(@version, @level)
|
|
93
|
+
bits << ("0" * [4, (capacity * 8) - bits.size].min)
|
|
94
|
+
bits << ("0" * (-bits.size % 8))
|
|
95
|
+
data = [bits].pack("B*").bytes
|
|
96
|
+
data << PADS[(data.size - (bits.size / 8)) % 2] while data.size < capacity
|
|
97
|
+
interleave(data)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The data split into blocks, each followed by its error correction,
|
|
101
|
+
# read a codeword from each block in turn.
|
|
102
|
+
def interleave(data)
|
|
103
|
+
count = BLOCKS.fetch(@level)[@version - 1]
|
|
104
|
+
ecc = ECC_PER_BLOCK.fetch(@level)[@version - 1]
|
|
105
|
+
raw = Matrix.raw_modules(@version) / 8
|
|
106
|
+
short = count - (raw % count)
|
|
107
|
+
short_length = (raw / count) - ecc
|
|
108
|
+
offset = 0
|
|
109
|
+
blocks = Array.new(count) do |index|
|
|
110
|
+
length = short_length + (index < short ? 0 : 1)
|
|
111
|
+
block = data[offset, length]
|
|
112
|
+
offset += length
|
|
113
|
+
[block, ReedSolomon::QR.remainder(block, ecc)]
|
|
114
|
+
end
|
|
115
|
+
(0..short_length).flat_map { |i| blocks.filter_map { |block, _| block[i] } } +
|
|
116
|
+
(0...ecc).flat_map { |i| blocks.map { |_, check| check[i] } }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|