module Ascii85

Ascii85 is an implementation of Adobe’s binary-to-text encoding of the same name in pure Ruby.

See www.adobe.com/products/postscript/pdfs/PLRM.pdf page 131 and en.wikipedia.org/wiki/Ascii85 for more information about the format.

Author

Johannes Holzfuß (johannes@holzfuss.name)

License

Distributed under the MIT License (see LICENSE file)

Constants

VERSION

Public Class Methods

decode(str) click to toggle source

Searches through str and decodes the first Ascii85-String found.

decode expects an Ascii85-encoded String enclosed in <~ and ~> — it will ignore all characters outside these markers. The returned strings are always encoded as ASCII-8BIT.

Ascii85.decode("<~;KZGo~>")
=> "Ruby"

Ascii85.decode("Foo<~;KZGo~>Bar<~;KZGo~>Baz")
=> "Ruby"

Ascii85.decode("No markers")
=> ""

decode will raise Ascii85::DecodingError when malformed input is encountered.

# File lib/ascii85.rb, line 122
def self.decode(str)
  input = str.to_s

  opening_delim = '<~'
  closing_delim = '~>'

  # Make sure the delimiter strings have the correct encoding.
  #
  # Although I don't think it likely, this may raise encoding
  # errors if an especially exotic input encoding is introduced.
  # As of Ruby 1.9.2 all non-dummy encodings work fine though.
  #
  if opening_delim.respond_to?(:encode)
    opening_delim = opening_delim.encode(input.encoding)
    closing_delim = closing_delim.encode(input.encoding)
  end

  # Get the positions of the opening/closing delimiters. If there is
  # no pair of opening/closing delimiters, return the empty string.
  (start_pos = input.index(opening_delim))                or return ''
  (end_pos   = input.index(closing_delim, start_pos + 2)) or return ''

  # Get the string inside the delimiter-pair
  input = input[(start_pos + 2)...end_pos]

  # Decode
  word   = 0
  count  = 0
  result = []

  input.each_byte do |c|
    case c.chr
    when " ", "\t", "\r", "\n", "\f", "\0"
      # Ignore whitespace
      next

    when 'z'
      if count == 0
        # Expand z to 0-word
        result << 0
      else
        raise(Ascii85::DecodingError, "Found 'z' inside Ascii85 5-tuple")
      end

    when '!'..'u'
      # Decode 5 characters into a 4-byte word
      word  += (c - 33) * 85**(4 - count)
      count += 1

      if count == 5

        if word > 0xffffffff
          raise(Ascii85::DecodingError,
                "Invalid Ascii85 5-tuple (#{word} >= 2**32)")
        end

        result << word

        word  = 0
        count = 0
      end

    else
      raise(Ascii85::DecodingError,
            "Illegal character inside Ascii85: #{c.chr.dump}")
    end
  end

  # Convert result into a String
  result = result.pack('N*')

  if count > 0
    # Finish last, partially decoded 32-bit-word

    if count == 1
      raise(Ascii85::DecodingError,
            "Last 5-tuple consists of single character")
    end

    count -= 1
    word  += 85**(4 - count)

    result << ((word >> 24) & 255).chr if count >= 1
    result << ((word >> 16) & 255).chr if count >= 2
    result << ((word >>  8) & 255).chr if count == 3
  end

  return result
end
encode(str, wrap_lines = 80) click to toggle source

Encodes the bytes of the given String as Ascii85.

If wrap_lines evaluates to false, the output will be returned as a single long line. Otherwise encode formats the output into lines of length wrap_lines (minimum is 2).

Ascii85.encode("Ruby")
=> <~;KZGo~>

Ascii85.encode("Supercalifragilisticexpialidocious", 15)
=> <~;g!%jEarNoBkD
   BoB5)0rF*),+AU&
   0.@;KXgDe!L"F`R
   ~>

Ascii85.encode("Supercalifragilisticexpialidocious", false)
=> <~;g!%jEarNoBkDBoB5)0rF*),+AU&0.@;KXgDe!L"F`R~>
# File lib/ascii85.rb, line 39
def self.encode(str, wrap_lines = 80)
  to_encode = str.to_s
  return '' if to_encode.empty?

  # Deal with multi-byte encodings
  if to_encode.respond_to?(:bytesize)
    input_size = to_encode.bytesize
  else
    input_size = to_encode.size
  end

  # Compute number of \0s to pad the message with (0..3)
  padding_length = (-input_size) % 4

  # Extract big-endian integers
  tuples = (to_encode + ("\0" * padding_length)).unpack('N*')

  # Encode
  tuples.map! do |tuple|
    if tuple == 0
      'z'
    else
      tmp = String.new
      5.times do
        tmp << ((tuple % 85) + 33).chr
        tuple /= 85
      end
      tmp.reverse
    end
  end

  # We can't use the z-abbreviation if we're going to cut off padding
  if (padding_length > 0) and (tuples.last == 'z')
    tuples[-1] = '!!!!!'
  end

  # Cut off the padding
  tuples[-1] = tuples[-1][0..(4 - padding_length)]

  # If we don't need to wrap the lines, add delimiters and return
  if (!wrap_lines)
    return '<~' + tuples.join + '~>'
  end

  # Otherwise we wrap the lines
  line_length = [2, wrap_lines.to_i].max

  wrapped = []
  to_wrap = '<~' + tuples.join

  0.step(to_wrap.length, line_length) do |index|
    wrapped << to_wrap.slice(index, line_length)
  end

  # Add end-marker – on a new line if necessary
  if (wrapped.last.length + 2) > line_length
    wrapped << '~>'
  else
    wrapped[-1] << '~>'
  end

  return wrapped.join("\n")
end