Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
for attempt in 1 2 3; do
if sudo timeout 90 apt-get update; then
sudo timeout 90 apt-get install --yes \
-o Dir::Cache::Archives="$HOME/.cache/apt-archives" mupdf-tools poppler-utils qpdf && break
-o Dir::Cache::Archives="$HOME/.cache/apt-archives" mupdf-tools poppler-utils qpdf zbar-tools && break
else
sudo dpkg --install ~/.cache/apt-archives/*.deb && break
fi
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ files. `lib/prawn*.rb` and `lib/pdf` answer Prawn's requires; the code lives in
```sh
bundle install && BUNDLE_GEMFILE=oracle/Gemfile bundle install # Prawn runs from its own bundle
bundle exec rake corpus:fetch # Prawn's manual and data, into corpus/cache
bundle exec rake test # needs: brew install mupdf-tools poppler qpdf
bundle exec rake test # needs: brew install mupdf-tools poppler qpdf zbar
ORACLE_CANDIDATE=prawn FUZZ_CASES=0 bundle exec rake test # Prawn against itself: proves the oracle
FUZZ_SEED=42 FUZZ_CASES=500 bundle exec ruby -Itest test/fuzz_test.rb
INPUT_FUZZ_SEED=42 INPUT_FUZZ_CASES=400 bundle exec ruby -Itest test/input_fuzz_test.rb
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ Prawn::Document.new(background_pdf: "letterhead.pdf") do
end
```

QR codes in pure Ruby, drawn as vectors: one path and one fill per code, read back by zbar in CI:

```ruby
qr_code "https://example.com/pay/INV-2026-0001", at: [400, 700], size: 80, level: :m
```

Loading font and SVG support once, before a server such as Puma or Unicorn forks its workers, so they share it in
memory:

Expand Down Expand Up @@ -214,8 +220,8 @@ its proof.
## Development

Prawn runs from its own bundle, `oracle/Gemfile`, so the oracle can compare the two. The tests need `mutool`,
`pdftoppm` and `qpdf` (`brew install mupdf-tools poppler qpdf`), and Prawn's manual and sample data, which
`rake corpus:fetch` downloads into `corpus/cache`. Without that cache, the comparison tests are skipped.
`pdftoppm`, `qpdf` and `zbarimg` (`brew install mupdf-tools poppler qpdf zbar`), and Prawn's manual and sample data,
which `rake corpus:fetch` downloads into `corpus/cache`. Without that cache, the comparison tests are skipped.

```sh
bundle install && BUNDLE_GEMFILE=oracle/Gemfile bundle install
Expand Down
10 changes: 10 additions & 0 deletions docs/guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ <h1 class="page-title">Guide</h1>
<li><a href="#svg">SVG</a></li>
<li><a href="#fonts">Fonts</a></li>
<li><a href="#images">Images</a></li>
<li><a href="#barcodes">QR codes</a></li>
<li><a href="#pdf-a">PDF/A and Factur-X</a></li>
<li><a href="#output">Output</a></li>
<li><a href="#servers">Servers that fork</a></li>
Expand Down Expand Up @@ -233,6 +234,15 @@ <h3 id="pdf-pages">Pages of other PDFs</h3>
</div>
</section>

<section id="barcodes" aria-labelledby="barcodes-title">
<h2 id="barcodes-title">QR codes</h2>
<div class="prose">
<p>A QR code is drawn as vector rectangles, one path and one fill, so it stays sharp at any size and a sheet of labels stays small. The encoder is pure Ruby, with no dependency: every version and level of error correction, numeric, alphanumeric and byte modes, and UTF-8 text announced to scanners.</p>
<pre><code class="language-ruby"><span class="n">qr_code</span> <span class="s2">"https://example.com/pay/INV-2026-0001"</span><span class="p">,</span> <span class="ss">at: </span><span class="p">[</span><span class="mi">400</span><span class="p">,</span> <span class="mi">700</span><span class="p">],</span> <span class="ss">size: </span><span class="mi">80</span><span class="p">,</span> <span class="ss">level: :m</span></code></pre>
<p><code>size</code> is the width of the whole symbol, its quiet zone of four modules included; <code>module_size</code> gives the width of one module instead (2 points unless told). <code>level</code> is <code>:l</code>, <code>:m</code> (the default), <code>:q</code> or <code>:h</code>, and the smallest version that holds the text at that level is chosen; text too long for any raises <code>ArgumentError</code>. Without <code>at</code>, the code is placed as an image is, with <code>position</code> and <code>vposition</code>. <code>quiet_zone: false</code> leaves the margin to the layout, and <code>color</code> sets the colour of the modules. In Gamba’s own API, <code>qr_code</code> is a box, with <code>size</code> or <code>module_size</code>, <code>level</code>, <code>quiet_zone</code>, <code>color</code> and <code>align</code>.</p>
</div>
</section>

<section id="pdf-a" aria-labelledby="pdf-a-title">
<h2 id="pdf-a-title">PDF/A and Factur-X</h2>
<div class="prose">
Expand Down
3 changes: 2 additions & 1 deletion lib/gamba.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def verify_options(accepted, actual)
require_relative "gamba/images"
require_relative "gamba/images/png_decoder"
require_relative "gamba/images/image"
require_relative "gamba/barcodes"
require_relative "gamba/repeater"
require_relative "gamba/grid"
require_relative "gamba/outline"
Expand Down Expand Up @@ -121,7 +122,7 @@ def self.combine(sources, page_size: nil, orientation: nil)
# Loads what Gamba otherwise loads on first use, SVG support included: call it before a server forks its
# workers, so they share that memory instead of each loading its own copy.
def self.eager_load!
[TrueType::File, TrueType::Subset, Prawn::Fonts::TTF, Combine, ImportedPage, PDF::Core::Reader, Document]
[TrueType::File, TrueType::Subset, Prawn::Fonts::TTF, Combine, ImportedPage, Barcodes::QRCode, PDF::Core::Reader, Document]
require "prawn-svg"
end
end
Expand Down
68 changes: 68 additions & 0 deletions lib/gamba/barcodes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

module Gamba
# Symbologies encoded in pure Ruby, as grids of modules or runs of bars that Prawn::Barcodes draws.
module Barcodes
autoload :QRCode, ::File.expand_path("barcodes/qr_code", __dir__)
end
end

module Prawn
# Codes drawn as vector rectangles, one path and one fill per symbol: sharp at any size, and a sheet of labels
# stays small.
module Barcodes
QR_OPTIONS = %i[at position vposition size module_size level quiet_zone color].freeze
# The light margin scanners need around a QR code, in modules (ISO/IEC 18004 §6.3.8).
QR_QUIET_ZONE_MODULES = 4
DEFAULT_MODULE_SIZE = 2

# A QR code of the text, from the top left corner at `at`, or placed as an image is: size is its width, quiet
# zone included, or module_size the width of one module. level: :l, :m, :q or :h. Returns the symbol.
def qr_code(text, options = {})
Prawn.verify_options(QR_OPTIONS, options)
symbol = Gamba::Barcodes::QRCode.new(text, level: options.fetch(:level, :m))
draw_qr_code(symbol, options)
symbol
end

# A symbol already encoded, with the options of qr_code but level.
def draw_qr_code(symbol, options)
quiet_zone = options.fetch(:quiet_zone, true) ? QR_QUIET_ZONE_MODULES : 0
modules = symbol.size + (2 * quiet_zone)
module_size = options[:size] ? options[:size] / modules.to_f : options.fetch(:module_size, DEFAULT_MODULE_SIZE)
width = modules * module_size
if options[:at]
left, top = map_to_absolute(options[:at])
else
left, top = image_position(width, width, options.dup)
move_text_position(width)
end
save_graphics_state do
fill_color(options.fetch(:color, "000000"))
add_content("#{module_path(symbol, left + (quiet_zone * module_size), top - (quiet_zone * module_size), module_size)}f")
end
end

private

# Dark modules side by side on a row are one rectangle of the path.
def module_path(symbol, left, top, module_size)
path = +""
symbol.size.times do |y|
x = 0
while x < symbol.size
unless symbol.dark?(x, y)
x += 1
next
end

run = x
run += 1 while run < symbol.size && symbol.dark?(run, y)
path << PDF::Core.real_params([left + (x * module_size), top - ((y + 1) * module_size), (run - x) * module_size, module_size]) << " re\n"
x = run
end
end
path
end
end
end
Loading
Loading