class PDF417

Constants

CODE_WORD

codeword bits, by cluster

REED_SOLO

reed solomon error correcting factors

START_CODE

start and stop codes

STOP_CODE
TEXT_JUMP

used to jump between text submodes

TEXT_MODE

text submodes

Public Class Methods

new(str=nil, tall:2, wbyh:3, pads:2) click to toggle source
# File lib/pdf-417.rb, line 12
def initialize(str=nil, tall:2, wbyh:3, pads:2)
  @tall = tall # row height in module units
  @wbyh = wbyh # barcode's width/height
  @pads = pads # padding around barcode

  @str = str # string to encode
  @cws = nil # codeword array
  @bar = nil # barcode pattern
end
to_png(file, str) click to toggle source
# File lib/pdf-417.rb, line 187
def self.to_png(file, str)
  if out = new(str).generate.to_png()
    File.write(file, out)
  end
end

Public Instance Methods

encode(str=nil) click to toggle source
# File lib/pdf-417.rb, line 22
def encode(str=nil)
  str = str ? (@str = str) : @str
  all = str.split('')
  max = all.size - 1
  ary = TEXT_MODE[cur = nxt = 0]
  out = []

  # map to text submodes
  all.each_with_index do |chr, pos|
    unless val = ary.index(ord = chr.ord)
      if nxt = TEXT_MODE.probe {|row, nxt| nxt if (nxt != cur) && (val = row.index(ord)) }
        if (nxt == 3 || (nxt == 0 && cur == 1)) && (pos == max || ary.index(all[pos + 1].ord))
          out.push(nxt == 3 ? 29 : 27) # only shift modes for the next character
        else
          out.concat(TEXT_JUMP["#{cur}#{nxt}"]) # jump to new mode
          ary = TEXT_MODE[cur = nxt]
        end
      end
    end
    out.push val
  end
  out.push(29) unless out.size.even?

  # map to codewords
  @cws = out.each_slice(2).map {|a,b| a * 30 + b }
end
generate() click to toggle source
# File lib/pdf-417.rb, line 71
def generate

  # convert content to codewords
  cws = encode(@str)
  cnt = cws.size; raise "ERROR: Max codeword count is 925, you have #{cnt}" if cnt > 925
  ecl = get_ecl(cnt)
  err = 2 << ecl
  nce = cnt + err + 1

  # find optimal columns using the quadratic equation [-b ± √(b²-4ac)] / 2a
  col = ((Math.sqrt(69 * 69 + 4 * 17 * nce * @tall * @wbyh) - 69) / (2 * 17)).round
  col = col < 1 ? 1 : 30 unless col.between?(1, 30)
  row = (nce / col.to_f).ceil
  tot = col * row
  unless row.between?(3, 90)
    row = row < 3 ? 3 : 90
    col = (tot / row.to_f).ceil # why not (nce / col.to_f).ceil ??
    tot = col * row
  end
  if tot > 928 # adjust dimensions to fit
    col, row = (@wbyh - (17.0 * 29 / 32)).abs < (@wbyh - (17.0 * 16 / 58)).abs ? [29, 32] : [16, 58]
    tot = col * row # 928
  end

  # calculate padding and prepend the symbol length descriptor
  (pad = tot - nce) > 0 and cws.concat([900] * pad)
  cws.unshift(tot - err) # same as (cnt + pad + 1) ?

  # append error correction codewords
  ecw = get_ecc(cws, ecl)
  cws.concat(ecw)

  # create barcode array
  bar = []
  cid = 0
  ind = 0
  pos = 0

  # side bars
  lbar = '0' * @pads + START_CODE
  rbar = STOP_CODE + '0' * @pads

  row.times do |r|
    key = 30 * (r / 3)

    # left side
    out = lbar + ('%17b' % CODE_WORD[cid][case cid
      when 0 then key + (row - 1) / 3
      when 1 then key + (row - 1) % 3 + (ecl * 3)
      when 2 then key + (col - 1)
    end])

    # data portion
    col.times do |c|
      out << ('%17b' % CODE_WORD[cid][cws[pos]])
      pos += 1
    end

    # right side
    out += ('%17b' % CODE_WORD[cid][case cid
      when 0 then key + (col - 1)
      when 1 then key + (row - 1) / 3
      when 2 then key + (row - 1) % 3 + (ecl * 3)
    end]) + rbar

    # add a row
    @tall.times { bar << out }

    # next cluster
    cid = (cid += 1) % 3
  end

  # top and bottom quiet zones
  zone = '0' * (col * 17 + 69 + 2 * @pads) # pad, start, lri, cols, rri, stop, pad
  @pads.times { bar.unshift zone } # top quiet zone
  @pads.times { bar.push    zone } # bottom quiet zone

  # stash barcode
  @bar = bar
  self
end
get_ecc(cws, ecl) click to toggle source
# File lib/pdf-417.rb, line 59
def get_ecc(cws, ecl)
  ecc = REED_SOLO[ecl]
  max = (2 << ecl) - 1
  ecw = [0] * (max + 1)
  cws.each do |val|
    key = (val + ecw[max]) % 929
    max.downto(0) {|pos| ecw[pos] = ((pos == 0 ? 0 : ecw[pos - 1]) + (929 - (key * ecc[pos]) % 929)) % 929 }
  end
  ecw.map! {|val| val == 0 ? val : 929 - val}
  ecw.reverse
end
get_ecl(cnt) click to toggle source
# File lib/pdf-417.rb, line 49
def get_ecl(cnt)
  case
    when cnt <  41 then 2
    when cnt < 161 then 3
    when cnt < 321 then 4
    when cnt < 864 then 5
  else                  6
  end
end
to_png(opts = {}) click to toggle source
# File lib/pdf-417.rb, line 153
def to_png(opts = {})
  ary = @bar or raise "no barcode available"

  require "chunky_png" unless defined?(ChunkyPNG)

  opts[:x_scale] ||=  1
  opts[:y_scale] ||=  1 # 3
  opts[:margin ] ||= 10

  full_width  = (ary.first.size * opts[:x_scale]) + (opts[:margin] * 2)
  full_height = (ary.size       * opts[:y_scale]) + (opts[:margin] * 2)

  canvas = ChunkyPNG::Image.new(full_width, full_height, ChunkyPNG::Color::WHITE)

  x, y = opts[:margin], opts[:margin]
  dots = ary.map {|l| l.split('').map {|c| c == '1' }}
  dots.each do |line|
    line.each do |bar|
      if bar
        x.upto(x + (opts[:x_scale] - 1)) {|xx|
          y.upto(y + (opts[:y_scale] - 1)) {|yy|
            canvas[xx,yy] = ChunkyPNG::Color::BLACK
          }
        }
      end
      x += opts[:x_scale]
    end
    y += opts[:y_scale]
    x = opts[:margin]
  end

  canvas.to_datastream.to_s
end