Goban

A fast and efficient QR Code encoder/decoder library written purely in Crystal. It is significantly faster (4-10×) and uses fewer heap allocations (-95%) compared to the other implementation in Crystal (spider-gazelle/qr-code), and it supports wider QR Code standard features such as Kanji mode encoding. It also supports generating Micro QR Code and rMQR Code symbols.

The implementation aims compliance with following standards:

The name comes from the board game Go, which inspired the QR Code inventor to come up with a fast and accurate canvas barcode to read. 碁盤(Goban) literally means Go board in Japanese.

"QR Code" is a registered trademark of Denso Wave Incorporated. https://www.qrcode.com/en/patent.html

Benchmarks

vs spider-gazelle/qr-code:

text = ARGV[0]
Benchmark.ips do |x|
  x.report("goban") { Goban::QR.encode_string(text, Goban::ECC::Level::High) }
  x.report("qr-code") { QRCode.new(text, level: :h) }
end
❯ ./bin/qr_test "Hello World\!"
  goban  30.76k ( 32.51µs) (± 0.21%)  7.6kB/op        fastest
qr-code   4.59k (217.64µs) (± 0.64%)  149kB/op   6.69× slower
❯ ./bin/qr_test "こんにちは"
  goban  30.99k ( 32.26µs) (± 0.31%)  7.56kB/op        fastest
qr-code   3.08k (324.90µs) (± 0.59%)   198kB/op  10.07× slower

It goes even faster with a larger data and multithreading:

❯ shards build --release -Dpreview_mt
Dependencies are satisfied
Building: qr_test

❯ CRYSTAL_WORKERS=4 ./bin/qr_test "A fast and efficient QR Code encoder/decoder library written purely in Crystal. It is significantly faster (4-10×) and uses fewer heap allocations (-95%) compared to the other implementation in Crystal (spider-gazelle/qr-code), and it supports wider QR Code standard features such as Kanji mode encoding. It also supports generating Micro QR Code and rMQR Code symbols."
  goban   2.45k (408.47µs) (± 6.31%)  95.3kB/op        fastest
qr-code 168.07  (  5.95ms) (± 2.17%)  2.55MB/op  14.57× slower

❯ CRYSTAL_WORKERS=8 ./bin/qr_test "A fast and efficient QR Code encoder/decoder library written purely in Crystal. It is significantly faster (4-10×) and uses fewer heap allocations (-95%) compared to the other implementation in Crystal (spider-gazelle/qr-code), and it supports wider QR Code standard features such as Kanji mode encoding. It also supports generating Micro QR Code and rMQR Code symbols."
  goban   3.41k (292.99µs) (± 2.03%)  95.3kB/op        fastest
qr-code 173.34  (  5.77ms) (± 1.81%)  2.55MB/op  19.69× slower

vs woodruffw/qrencode.cr (Crystal bindings to libqrencode):

text = ARGV[0]
Benchmark.ips do |x|
  x.report("goban") { Goban::QR.encode_string(text, Goban::ECC::Level::High) }
  x.report("qrencode") { QRencode::QRcode.new(text, level: QRencode::ECLevel::HIGH) }
end
❯ ./bin/qr_test "Hello World\!"
   goban  30.31k ( 32.99µs) (± 0.20%)  7.6kB/op   1.46× slower
qrencode  44.18k ( 22.63µs) (± 0.28%)   112B/op        fastest
❯ ./bin/qr_test "こんにちは"
   goban  31.45k ( 31.80µs) (± 0.22%)  7.56kB/op   1.03× slower
qrencode  32.42k ( 30.84µs) (± 0.52%)    112B/op        fastest

* The heap allocation value reported for qrencode.cr is not accurate as it doesn't include memory allocations happened on the C library.

When compared to the C library, Goban is usually a bit slower, but it should be noted that Goban uses a more advanced algorithm for text segmentation and is more likely to produce a smaller QR Code symbol as a result.

Features

| QR Code Type | Encoding | Decoding | | ------------- | :------: | :------: | | QR Code* | ✓ | ✓ | | Micro QR Code | ✓ | ✓ | | rMQR Code | ✓ | ✓ |

* QR Code Model 1 will not be supported as it is considered obsolete.

Roadmap

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      goban:
        github: soya-daizu/goban
  2. Run shards install

Usage

A simple example to generate a QR Code for the given string and output to the console:

require "goban"

qr = Goban::QR.encode_string("Hello World!", Goban::ECC::Level::Low)
qr.print_to_console
# => ██████████████  ████    ██  ██████████████
#    ██          ██    ██    ██  ██          ██
#    ██  ██████  ██  ██  ██  ██  ██  ██████  ██
#    ██  ██████  ██  ██    ██    ██  ██████  ██
#    ██  ██████  ██  ██████      ██  ██████  ██
#    ██          ██              ██          ██
#    ██████████████  ██  ██  ██  ██████████████
#                      ████
#    ████████    ██  ██  ██    ██    ██████  ██
#    ██████████    ██  ██    ██████████████  ██
#        ██    ████    ██    ████  ██      ████
#    ████  ██      ██████    ██    ██  ██  ██
#    ████  ██████████  ██████  ████          ██
#                    ████████    ████    ██  ██
#    ██████████████      ██    ████████
#    ██          ██    ██████████  ██  ████
#    ██  ██████  ██    ██  ██          ██████
#    ██  ██████  ██  ██  ██  ██  ██    ██████
#    ██  ██████  ██  ██████  ██    ██    ██
#    ██          ██  ██    ██  ████████      ██
#    ██████████████  ██    ██  ██████    ██

Goban::ECC::Level represents the ECC (Error Correction Coding) level to use when encoding the data. The available options are:

| Level | Error Correction Capability | | -------- | --------------------------- | | Low | Approx 7% | | Medium | Approx 15% | | Quartile | Approx 25% | | High | Approx 30% |

The default ECC level is Medium. Use Low if you want your QR Code to be as compact as possible, or increase the level to Quartile or High if you want it to be more resistant to damage.

Higher ECC levels are especially capable of interpolating a large chunk of loss in the symbol such as by tears and stains. Typically, it is not necessary to set the ECC level high for display purposes on the screen.

Using exporters to generate a PNG and SVG image

To generate a PNG image, add stumpy_png as a dependency in your shard.yml, and require "goban/exporters/png" to use Goban::PNGExporter:

require "goban/exporters/png"

qr = Goban::QR.encode_string("Hello World!")
puts "Exporting with targeted size: 500"
size = Goban::PNGExporter.export(qr, "output.png", 500)
puts "Actual QR Code size: #{size}"

Goban::SVGExporter requires no external dependency and can be used like below:

require "goban/exporters/svg"

qr = Goban::QR.encode_string("Hello World!")
# Get SVG string
puts Goban::SVGExporter.svg_string(qr, 4)
# or export as a file
Goban::SVGExporter.export(qr, "test.svg")

Alternatively, you can write your own export logic by iterating over the canvas of the QR Code object.

qr = Goban::QR.encode_string("Hello World!")
qr.canvas.each_row do |row, y|
  row.each do |mod, x|
    # mod is each module (pixel or dot in other words) included in the symbol
    # and the value is either 0 (= light) or 1 (= dark)
  end
end

About the encoding modes and the text segmentation

The Goban::QR.encode_string method under the hood encodes a string to an optimized sequence of text segments where each segment is encoded in one of the following encoding modes:

| Mode | Supported Characters | | ------------ | --------------------------- | | Numeric | 0-9 | | Alphanumeric | 0-9 A-Z \s $ % * + - . / : | | Byte | Any UTF-8 characters | | Kanji | Any Shift-JIS characters |

The Byte mode supports the widest range of characters but it is inefficient and produces longer data bits, meaning that when comparing the two QR Code symbols, one encoded entirely in the Byte mode and the other encoded in the appropriate mode for each character*, the former one can be more challenging to scan and decode than the other given that both symbols are printed in the same size.

* Because each text segment includes additional header bits to indicate its encoding mode, simply encoding each character in the supported mode that has the smallest character set may not always produce the most optimal segments. Goban addresses this by using the technique of dynamic programming.

Finding out the optimal segmentation requires some processing, so if you are generating thousands of QR Codes with all the same limited sets of characters, you may want to hard-code the text segments and apply the characters to those to generate the QR Codes.

This can be done by using the Goban::QR.encode_segments method, which is the lower-level method used by the Goban::QR.encode_string method.

segments = [
  Goban::Segment.kanji("こんにち"),
  Goban::Segment.byte("wa"),
  Goban::Segment.kanji("、世界!"),
  Goban::Segment.alphanumeric(" 123"),
]
# Note that when using this method, you have to manually assign the version (= size) of the QR Code.
qr = Goban::QR.encode_segments(segments, Goban::ECC::Level::Low, 2)

The optimal segments and version to hard-code can be figured out by manually executing the Goban::QR.determine_version_and_segments method.

Generating Micro QR Codes

Micro QR Codes can be generated just like regular QR Codes using the Goban::MQR.encode_string or Goban::MQR.encode_segments methods.

mqr = Goban::MQR.encode_string("Hello World!", Goban::ECC::Level::Low)
mqr.print_to_console
# => ██████████████  ██  ██  ██  ██  ██
#    ██          ██  ██        ██
#    ██  ██████  ██    ██    ████
#    ██  ██████  ██  ██      ██████  ██
#    ██  ██████  ██      ██  ██████  ██
#    ██          ██  ██        ██  ██
#    ██████████████  ██████    ██    ██
#                            ██████  ██
#    ██    ██  ██████  ████  ██      ██
#      ██████████            ██
#    ████    ████████  ██████████  ██
#      ██      ██  ████    ████
#    ████████  ██  ██  ████  ██████  ██
#              ██████████████████
#    ██████      ████████        ██
#        ██      ██  ██████  ████
#    ██████  ██    ██  ████  ██      ██

You can learn more about the text segments and encoding modes above.

Note that Micro QR Code has strong limitations in the data capacity, supported encoding modes, and error correction capabilities.

| Version | Supported ECC Level | Supported Modes | | ------- | --------------------------- | ---------------------------------- | | M1 | None (Error Detection Only) | Numeric | | M2 | Low, Medium | Numeric, Alphanumeric | | M3 | Low, Medium | Numeric, Alphanumeric, Byte, Kanji | | M4 | Low, Medium, Quartile | Numeric, Alphanumeric, Byte, Kanji |

Data capacity for each combination of the symbol version and ECC level can be found here.

Since the version M1 doesn't support error correction at all, the value passed as the ECC level will be ignored.

Generating rMQR Codes

Just like regular QR Codes and Micro QR Codes, rMQR Codes can also be generated using the Goban::RMQR.encode_string and Goban::RMQR::encode_segments methods.

# Note that rMQR Code only supports Medium and High ECC Level
rmqr = Goban::RMQR.encode_string("Hello World!", Goban::ECC::Level::Medium)
puts rmqr.version.value
# => R11x43
rmqr.print_to_console
# => ██████████████  ██  ██  ██  ██  ██  ██  ██████  ██  ██  ██  ██  ██  ██  ██  ██  ██████
#    ██          ██          ██    ████████  ██  ██████████  ██████████  ██    ██        ██
#    ██  ██████  ██    ██  ██████        ██████████    ██    ████  ██  ████  ██    ████  ██
#    ██  ██████  ██  ██      ██    ██        ██            ████    ██████      ██      ██
#    ██  ██████  ██    ██  ██████    ████  ██  ████  ████    ██      ████      ████  ██████
#    ██          ██      ██  ██████                ████  ██  ██████    ██  ██    ██  ██
#    ██████████████  ██        ██  ██          ████████    ██████████████        ██████████
#                        ██  ██    ██████████    ████████      ████  ██    ████████      ██
#    ████    ██    ██      ██████    ████    ████████  ████    ██    ██    ████████  ██  ██
#    ██    ████    ██  ██    ██      ████    ██  ██  ██████  ██████  ██████      ██      ██
#    ██████  ██  ██  ██  ██  ██  ██  ██  ██  ██████  ██  ██  ██  ██  ██  ██  ██  ██████████

However, unlike regular QR Codes and Micro QR Codes, rMQR Codes has different sizes in width and height, which means that there can be multiple versions that are optimal in terms of capacity. rMQR Code versions are represented in the format of R{height}x{width} with the following available combinations.

| | 27 | 43 | 59 | 77 | 99 | 139 | | --- | :-: | :-: | :-: | :-: | :-: | :-: | | R7 | - | ✓ | ✓ | ✓ | ✓ | ✓ | | R9 | - | ✓ | ✓ | ✓ | ✓ | ✓ | | R11 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | R13 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | R15 | - | ✓ | ✓ | ✓ | ✓ | ✓ | | R17 | - | ✓ | ✓ | ✓ | ✓ | ✓ |

SizingStrategy is used to prioritize one version than the other based on whether you want the symbol to be smaller in total area, width, or height. By default, it tries to balance the width and height, keeping the total area as small as possible.

For example, if you want to encode the same text but prioritizing smaller height rather than area:

rmqr = Goban::RMQR.encode_string("Hello World!", Goban::ECC::Level::Medium, Goban::RMQR::SizingStrategy::MinimizeHeight)
puts rmqr.version.value
# => R7x77
rmqr.print_to_console
# => ██████████████  ██  ██  ██  ██  ██  ██  ██  ██  ██████  ██  ██  ██  ██  ██  ██  ██  ██  ██  ██  ██  ██████  ██  ██  ██  ██  ██  ██  ██  ██  ██  ██  ██████
#    ██          ██  ██    ████████    ██████        ██  ██          ██  ████  ████  ██  ████        ██  ██  ████    ██        ██  ██    ██████          ██  ██
#    ██  ██████  ██    ██████████      ██████    ██  ██████      ██████    ██████████████      ████  ████████████  ██    ████  ██  ██    ██  ██    ████████████
#    ██  ██████  ██  ██████  ██  ██    ████        ████  ██  ██████████  ████  ██  ██  ██████  ██████    ██    ██████      ██        ████  ██████    ██      ██
#    ██  ██████  ██  ████    ██          ████    ██████████          ██  ██      ██  ██████  ██        ████████    ██  ██  ██          ████████  ██████  ██  ██
#    ██          ██  ██████  ██████  ██████    ████████  ██    ██  ██        ████  ██  ██    ██  ██  ██  ██  ██      ████████  ██  ██  ████    ████  ██      ██
#    ██████████████  ██  ██  ██  ██  ██  ██  ██  ██  ██████  ██  ██  ██  ██  ██  ██  ██  ██  ██  ██  ██  ██████  ██  ██  ██  ██  ██  ██  ██  ██  ██  ██████████

API Documentations

The API docs for the current master branch are available from the link below:

API docs

You might want to first look at the Goban::QR or one of the exporters (Goban::PNGExporter and Goban::SVGExporters) to understand how to use this library.

Contributing

  1. Fork it (https://github.com/soya-daizu/goban/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

Credits