PBM2BB

The Braille Blazer has a limited and poorly documented, but quite functional graphics mode. Although some existing programs use this mode, the only ones I have found to date are (a) proprietary and (b) Windows-based. So, I decided to write my own.

Background

Using the Blazer's graphics mode is very simple: just flip over the platen. This removes all spacing between braille cells, resulting in a continuous, embossed block (90 dots per line, ~0.09" apart). (The Blazer actually produces two (45-dot) line segments, separated by slightly more than the normal inter-dot spacing. So, the resulting image has a slight gap along the vertical center line.)

Although this approach means that graphics and text cannot be interspersed, it also avoids the complexity of using a special data format for graphics. So, all we need to do is generate lines of braille cells that have the desired dot patterns. By default, the Blazer uses Braille ASCII and 6-dot cells. This is fine as an initial target, but handling other encoding variants should not be difficult.

The Netpbm format family defines several image formats. Portable bitmap format (PBM) supports a single bit per pixel, so it is a good match for a braille embosser. Because many image processing tools can export PBM, there should be no problem editing or generating input images.

Approach

Unicode defines a set of braille patterns, where each cell is represented by an 8-bit byte. Each dot has a corresponding bit value (e.g., the upper right dot has value 0x08). The arrangement is a bit odd, but it has the benefit of clustering the 6-dot patterns in a 64-code range.

Using a small lookup table, we can select the cell and dot that map to a given row and column in the input image. Once we have a matrix of braille patterns, we can map them to the corresponding Braille ASCII codes. Although there is no simple rule that defines which character has which pattern, we can use a 64-position lookup table.

Usage

At present, pbm2bb runs as a Unix "filter", reading from STDIN and writing to STDOUT. Here's how I use it:

$ pbm2ub < sample.pbm | tee sample.ba
... Braille ASCII, for inspection ...
$ lp -d blazer sample.ba

Ruby Code

Here is the current Ruby code for pbm2bb:

#!/usr/bin/env ruby
#...

  # Map rows and columns to Unicode Braille bit values.
  @rc2ub  = [ [ 0x01, 0x08 ], [ 0x02, 0x10 ],
              [ 0x04, 0x20 ], [ 0x40, 0x80 ] ]

  # Map (6-dot) Unicode Braille to Braille ASCII.
  @ub2ab  = [ ' ' ] + %w{
       A  1  B  '  K  2  L  @  C  I  F  /  M  S  P
    "  E  3  H  9  O  6  R  ^  D  J  G  >  N  T  Q
    ,  *  5  <  -  U  8  V  .  %  [  $  +  X  !  &
    ;  :  4  \\ 0  Z  7  (  _  ?  W  ]  #  Y  )  = }
  # 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F

  def main()
    read_header()
    read_image()
    set_bits()
    dump_mat()
  end

  def dump_mat()
  #
  # Convert @ub_mat rows to Braille ASCII strings, then print.

    0.upto(@ub_rows-1) do |row|
      ba_chars = []
      0.upto(@ub_cols-1) do |col|
        ub_char   = @ub_mat[row][col]
        ba_char   = @ub2ab[ub_char]
        ba_chars.push(ba_char)
      end
      puts ba_chars.join.strip
    end
  end

  def read_header()
  #
  # Read (and check) the header information.
  # Set @ub_cols and @ub_rows; create @ub_mat.

    magic = readline().chomp
    unless (magic == 'P1')
      fmt = "Unexpected magic number (%s).\n"
      STDERR.printf(fmt, magic)
      exit(1)
    end

    dim_line = '#'
    while (dim_line =~ /^\s*#/) do
      dim_line = readline().chomp
    end
    dims     = dim_line.split
    @bm_cols, @bm_rows = dims[0].to_i, dims[1].to_i
    @ub_cols = (@bm_cols + 1) / 2
    @ub_rows = (@bm_rows + 2) / 3

    if (@ub_cols < 1 or @ub_rows < 1)
      fmt = "Error: @ub_cols (%d) or @ub_rows (%d) < 1.\n"
      STDERR.printf(fmt, @ub_cols, @ub_rows)
      exit(1)
    end
    @ub_mat = Array.new(@ub_rows) { Array.new(@ub_cols, 0) }
  end

  def read_image()
  #
  # Read and tidy up the raster image.
  # Set @pixels to an array of pixels.

    @pixels = readlines().each do |line|
      line.chomp!             # remove line termination
      line.gsub!(/#.*$/, '')  # remove trailing comments
      line.gsub!(/\s*/,  '')  # remove all whitespace
    end.join.chars            # concatenate the lines
    
    length = @pixels.length
    if (length != @bm_cols * @bm_rows)
      fmt = "Error: length (%d) != @bm_cols (%d) * @bm_rows (%d).\n"
      STDERR.printf(fmt, length, @bm_cols, @bm_rows)
      exit(1)
    end
  end

  def set_bit(pbm_row, pbm_col)
  #
  # Set a bit in @ub_mat.

    lim_col = [ 90, pbm_col ].min
    col_pos = lim_col / 2; row_pos = pbm_row / 3
    col_off = lim_col % 2; row_off = pbm_row % 3
    dot_val = @rc2ub[row_off][col_off]
    @ub_mat[row_pos][col_pos] |= dot_val
  end

  def set_bits() #T
  #
  # Set bits in @ub_mat.

    0.upto(@bm_rows-1) do |row|
      0.upto(@bm_cols-1) do |col|
        set_bit(row, col) if (@pixels.shift == '1')
      end
    end
  end

  main

##EOF


This wiki page is maintained by Rich Morin, an independent consultant specializing in software design, development, and documentation. Please feel free to email comments, inquiries, suggestions, etc!

Topic revision: r4 - 18 Feb 2016, RichMorin
This site is powered by Foswiki Copyright © by the contributing authors. All material on this wiki is the property of the contributing authors.
Foswiki version v2.1.6, Release Foswiki-2.1.6, Plugin API version 2.4
Ideas, requests, problems regarding CFCL Wiki? Send us email