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
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
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