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.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -68
  3. data/README.md +775 -78
  4. data/examples/accessible_report.rb +133 -0
  5. data/examples/article.rb +93 -0
  6. data/examples/assets/bay.png +0 -0
  7. data/examples/assets/dunes.png +0 -0
  8. data/examples/assets/hills.png +0 -0
  9. data/examples/assets/logo.png +0 -0
  10. data/examples/assets/ridge.png +0 -0
  11. data/examples/assets/sea.png +0 -0
  12. data/examples/assets/stone.png +0 -0
  13. data/examples/assets/wall.png +0 -0
  14. data/examples/certificate.rb +104 -0
  15. data/examples/contract.rb +159 -0
  16. data/examples/e_invoice.rb +161 -0
  17. data/examples/flyer.rb +336 -0
  18. data/examples/form.rb +113 -0
  19. data/examples/invoice.rb +163 -0
  20. data/examples/letter.rb +121 -0
  21. data/examples/menu.rb +133 -0
  22. data/examples/multilingual_notice.rb +121 -0
  23. data/examples/newsletter.rb +108 -0
  24. data/examples/packing_slip.rb +120 -0
  25. data/examples/postcard.rb +83 -0
  26. data/examples/price_list.rb +107 -0
  27. data/examples/receipt.rb +176 -0
  28. data/examples/report.rb +294 -0
  29. data/examples/resume.rb +121 -0
  30. data/examples/shaping/extraction_matrix/pdfium.py +8 -0
  31. data/examples/shaping/extraction_matrix/pdfjs.mjs +18 -0
  32. data/examples/shaping/extraction_matrix/pdfkit.swift +6 -0
  33. data/examples/shaping/extraction_matrix.rb +208 -0
  34. data/examples/shaping/harfbuzz_shaper.rb +132 -0
  35. data/examples/shaping/rtl_letter.rb +106 -0
  36. data/examples/shipping_label.rb +93 -0
  37. data/lib/stationery/barcode/code128.rb +96 -0
  38. data/lib/stationery/barcode/data_matrix.rb +111 -0
  39. data/lib/stationery/barcode/data_matrix_placement.rb +118 -0
  40. data/lib/stationery/barcode/ean13.rb +66 -0
  41. data/lib/stationery/barcode/qr.rb +120 -0
  42. data/lib/stationery/barcode/qr_matrix.rb +189 -0
  43. data/lib/stationery/barcode/reed_solomon.rb +61 -0
  44. data/lib/stationery/barcode.rb +33 -0
  45. data/lib/stationery/builder.rb +12 -2
  46. data/lib/stationery/canvas/glyphs.rb +40 -0
  47. data/lib/stationery/canvas/interface.rb +169 -0
  48. data/lib/stationery/canvas/marking.rb +32 -3
  49. data/lib/stationery/canvas/path.rb +32 -70
  50. data/lib/stationery/canvas/surfacing.rb +67 -0
  51. data/lib/stationery/canvas/text.rb +4 -25
  52. data/lib/stationery/canvas.rb +24 -75
  53. data/lib/stationery/cli/examples.rb +66 -0
  54. data/lib/stationery/cli/inspect.rb +65 -0
  55. data/lib/stationery/cli/layout_text.rb +132 -0
  56. data/lib/stationery/cli/pictures.rb +50 -0
  57. data/lib/stationery/cli/render.rb +33 -6
  58. data/lib/stationery/cli/skill.rb +166 -0
  59. data/lib/stationery/cli.rb +5 -0
  60. data/lib/stationery/color.rb +27 -13
  61. data/lib/stationery/component.rb +1 -0
  62. data/lib/stationery/css/stylesheet.rb +9 -1
  63. data/lib/stationery/document.rb +135 -27
  64. data/lib/stationery/elements/forms.rb +2 -1
  65. data/lib/stationery/elements/lists.rb +6 -2
  66. data/lib/stationery/elements.rb +43 -7
  67. data/lib/stationery/examples.rb +22 -0
  68. data/lib/stationery/fonts/cff.rb +46 -0
  69. data/lib/stationery/fonts/charstring.rb +297 -0
  70. data/lib/stationery/fonts/fallback.rb +22 -1
  71. data/lib/stationery/fonts/font.rb +58 -3
  72. data/lib/stationery/fonts/font_book.rb +7 -3
  73. data/lib/stationery/fonts/glyf_outline.rb +230 -0
  74. data/lib/stationery/fonts/glyph_run.rb +37 -10
  75. data/lib/stationery/fonts/outline.rb +102 -0
  76. data/lib/stationery/fonts/shaped_run.rb +22 -4
  77. data/lib/stationery/fonts/true_type.rb +7 -0
  78. data/lib/stationery/forms/typeface.rb +2 -4
  79. data/lib/stationery/html/css.rb +31 -6
  80. data/lib/stationery/html/tree_builder/blocks.rb +7 -3
  81. data/lib/stationery/images/images.rb +10 -0
  82. data/lib/stationery/images/jpeg/bit_reader.rb +92 -0
  83. data/lib/stationery/images/jpeg/colors.rb +133 -0
  84. data/lib/stationery/images/jpeg/decoder.rb +253 -0
  85. data/lib/stationery/images/jpeg/huffman.rb +59 -0
  86. data/lib/stationery/images/jpeg/idct.rb +150 -0
  87. data/lib/stationery/images/jpeg/progressive.rb +128 -0
  88. data/lib/stationery/images/jpeg/reduced_idct.rb +106 -0
  89. data/lib/stationery/images/jpeg/scan.rb +116 -0
  90. data/lib/stationery/images/jpeg/upsampler.rb +81 -0
  91. data/lib/stationery/images/jpeg.rb +59 -1
  92. data/lib/stationery/images/png.rb +10 -7
  93. data/lib/stationery/images/resampled.rb +10 -2
  94. data/lib/stationery/images/webp.rb +6 -5
  95. data/lib/stationery/layout/barcode.rb +75 -0
  96. data/lib/stationery/layout/box/wrapping.rb +56 -0
  97. data/lib/stationery/layout/box.rb +41 -15
  98. data/lib/stationery/layout/columns/leveller.rb +65 -0
  99. data/lib/stationery/layout/columns/pour.rb +7 -0
  100. data/lib/stationery/layout/columns.rb +32 -15
  101. data/lib/stationery/layout/field.rb +3 -2
  102. data/lib/stationery/layout/floated.rb +12 -1
  103. data/lib/stationery/layout/flow/floating.rb +23 -5
  104. data/lib/stationery/layout/flow/placement.rb +16 -12
  105. data/lib/stationery/layout/flow/splitter.rb +56 -10
  106. data/lib/stationery/layout/flow.rb +1 -0
  107. data/lib/stationery/layout/image.rb +2 -2
  108. data/lib/stationery/layout/list_item.rb +41 -11
  109. data/lib/stationery/layout/mark.rb +2 -0
  110. data/lib/stationery/layout/node.rb +16 -2
  111. data/lib/stationery/layout/opening.rb +61 -0
  112. data/lib/stationery/layout/paginator.rb +24 -5
  113. data/lib/stationery/layout/svg.rb +1 -1
  114. data/lib/stationery/layout/table/cell.rb +39 -10
  115. data/lib/stationery/layout/table/row_splitter.rb +6 -2
  116. data/lib/stationery/layout/table/selection.rb +9 -3
  117. data/lib/stationery/layout/table/stream.rb +123 -0
  118. data/lib/stationery/layout/table/widths.rb +26 -0
  119. data/lib/stationery/layout/table.rb +151 -35
  120. data/lib/stationery/layout/table_of_contents/entry.rb +3 -3
  121. data/lib/stationery/layout/table_of_contents.rb +1 -1
  122. data/lib/stationery/layout/text.rb +22 -2
  123. data/lib/stationery/minitest.rb +11 -3
  124. data/lib/stationery/monochrome/bitmap.rb +78 -0
  125. data/lib/stationery/monochrome/canvas.rb +19 -0
  126. data/lib/stationery/monochrome/dither.rb +74 -0
  127. data/lib/stationery/monochrome/grid.rb +131 -0
  128. data/lib/stationery/monochrome/painting.rb +140 -0
  129. data/lib/stationery/monochrome/rules.rb +141 -0
  130. data/lib/stationery/monochrome.rb +87 -0
  131. data/lib/stationery/page/format.rb +84 -0
  132. data/lib/stationery/page.rb +11 -7
  133. data/lib/stationery/page_templates.rb +13 -10
  134. data/lib/stationery/path.rb +138 -0
  135. data/lib/stationery/pdf/assembler.rb +14 -3
  136. data/lib/stationery/pdf/canvases.rb +51 -0
  137. data/lib/stationery/pdf/conformance.rb +43 -13
  138. data/lib/stationery/pdf/print_hints.rb +137 -0
  139. data/lib/stationery/pdf/serializer.rb +41 -0
  140. data/lib/stationery/raster/adjust.rb +67 -0
  141. data/lib/stationery/raster/canvas.rb +108 -0
  142. data/lib/stationery/raster/canvases.rb +49 -0
  143. data/lib/stationery/raster/flattener.rb +75 -0
  144. data/lib/stationery/raster/glyphs.rb +81 -0
  145. data/lib/stationery/raster/painter.rb +184 -0
  146. data/lib/stationery/raster/picture.rb +69 -0
  147. data/lib/stationery/raster/png.rb +32 -0
  148. data/lib/stationery/raster/render.rb +134 -0
  149. data/lib/stationery/raster/scanner.rb +164 -0
  150. data/lib/stationery/raster/shading.rb +77 -0
  151. data/lib/stationery/raster/spans.rb +66 -0
  152. data/lib/stationery/raster/stroker.rb +208 -0
  153. data/lib/stationery/raster/surface.rb +70 -0
  154. data/lib/stationery/raster.rb +44 -0
  155. data/lib/stationery/rich/nodes.rb +4 -4
  156. data/lib/stationery/rich/renderer/boxes.rb +37 -16
  157. data/lib/stationery/rich/renderer.rb +3 -2
  158. data/lib/stationery/skill/SKILL.md.erb +244 -0
  159. data/lib/stationery/skill/recipes/flyer.md.erb +76 -0
  160. data/lib/stationery/skill/recipes/form.md.erb +78 -0
  161. data/lib/stationery/skill/recipes/invoice.md.erb +142 -0
  162. data/lib/stationery/skill/recipes/label.md.erb +116 -0
  163. data/lib/stationery/skill/recipes/receipt.md.erb +79 -0
  164. data/lib/stationery/skill/recipes/report.md.erb +113 -0
  165. data/lib/stationery/skill/reference/conformance.md.erb +45 -0
  166. data/lib/stationery/skill/reference/elements.md.erb +19 -0
  167. data/lib/stationery/skill/reference/fonts_and_images.md.erb +7 -0
  168. data/lib/stationery/skill/reference/forms.md.erb +5 -0
  169. data/lib/stationery/skill/reference/html_and_markdown.md.erb +5 -0
  170. data/lib/stationery/skill/reference/pages.md.erb +15 -0
  171. data/lib/stationery/skill/reference/printing.md.erb +33 -0
  172. data/lib/stationery/skill/reference/testing.md.erb +33 -0
  173. data/lib/stationery/skill.rb +101 -0
  174. data/lib/stationery/structure.rb +7 -6
  175. data/lib/stationery/svg/gradient.rb +12 -0
  176. data/lib/stationery/svg/painter.rb +1 -1
  177. data/lib/stationery/tagging/element.rb +9 -2
  178. data/lib/stationery/tagging/tree.rb +98 -2
  179. data/lib/stationery/testing/field_page.rb +89 -0
  180. data/lib/stationery/testing/inspector.rb +80 -2
  181. data/lib/stationery/testing/layout.rb +166 -0
  182. data/lib/stationery/testing/marked_text.rb +124 -18
  183. data/lib/stationery/testing/matchers.rb +63 -14
  184. data/lib/stationery/testing/page_reader.rb +146 -0
  185. data/lib/stationery/text/exclusions.rb +11 -0
  186. data/lib/stationery/text/paragraph.rb +13 -8
  187. data/lib/stationery/text/wrapper/one_word.rb +33 -0
  188. data/lib/stationery/text/wrapper.rb +62 -27
  189. data/lib/stationery/units.rb +45 -0
  190. data/lib/stationery/version.rb +1 -1
  191. data/lib/stationery/warnings.rb +67 -7
  192. data/lib/stationery/zpl/native.rb +51 -0
  193. data/lib/stationery/zpl/render.rb +56 -0
  194. data/lib/stationery/zpl.rb +88 -0
  195. data/lib/stationery.rb +48 -0
  196. 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