Repository: ihabunek/pdf417-py

Issue:
Python3 isn't supported
Hi

I found support declaration in setup.py.

But it is impossible to encode something like b'\x01' 

Comments:

Patch:
diff --git a/.travis.yml b/.travis.yml
index a05962b..c5b0492 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,13 +3,15 @@ language: python
 python:
   - "2.6"
   - "2.7"
-  - "3.2"
   - "3.3"
   - "3.4"
   - "3.5"
+  - "3.6"
   - "nightly"
 
 install:
+  # pillow 4 doesn't support py2.6
+  - pip install "pillow<4.0.0"
   - pip install -e .
 
 script: py.test
diff --git a/README.rst b/README.rst
index 98cbb6d..a67bea8 100644
--- a/README.rst
+++ b/README.rst
@@ -59,6 +59,19 @@ Usage overview:
     svg = render_svg(codes)  # ElementTree object
     svg.write("barcode.svg")
 
+
+Supports strings (unicode in py2) and byte arrays (str in py2):
+
+.. code-block:: python
+
+    # These two inputs encode to the same code words
+    encode(u"love 💔")
+    encode(b"love \xf0\x9f\x92\x94")
+
+    # Default encoding is UTF-8, but you can specify your own
+    encode(u"love 💔", encoding="utf-8")
+
+
 Encoding data
 -------------
 
diff --git a/pdf417gen/compaction.py b/pdf417gen/compaction.py
index ba401a4..05bde6a 100644
--- a/pdf417gen/compaction.py
+++ b/pdf417gen/compaction.py
@@ -13,7 +13,8 @@ def compact_numbers(data):
     Rate compaction: 2.9 bytes per code word
     """
     def compact_chunk(chunk):
-        value = int("1" + chunk)
+        number = "".join([chr(x) for x in chunk])
+        value = int("1" + number)
         return to_base(value, 900)
 
     compacted_chunks = [compact_chunk(chunk) for chunk in chunks(data, size=44)]
@@ -31,7 +32,7 @@ def compact_text_interim(data):
 
     def get_submode(char):
         if char not in CHARACTERS_LOOKUP:
-            raise ValueError("Cannot encode char: " + char)
+            raise ValueError("Cannot encode char: {}".format(char))
 
         submodes = CHARACTERS_LOOKUP[char].keys()
 
@@ -41,7 +42,7 @@ def compact_text_interim(data):
             if submode in submodes:
                 return submode
 
-        raise ValueError("Cannot encode char: " + char)
+        raise ValueError("Cannot encode char: {}".format(char))
 
     # By default, encoding starts with uppercase submode
     submode = Submode.UPPER
@@ -103,7 +104,8 @@ def compact_bytes(data):
 
         The chunk is encoded to 5 code words by changing the base from 256 to 900.
         """
-        digits = [ord(i) for i in chunk]
+
+        digits = [i for i in chunk]
         return switch_base(digits, 256, 900)
 
     def compact_incomplete_chunk(chunk):
@@ -111,7 +113,7 @@ def compact_bytes(data):
 
         The chunk is encoded to 6 code words leaving the base unchanged.
         """
-        return [ord(i) for i in chunk]
+        return [i for i in chunk]
 
     compacted_chunks = [compact_chunk(chunk) for chunk in chunks(data, size=6)]
 
@@ -151,7 +153,7 @@ def compact(data):
 
         # Default compaction mode is Text (does not require an initial switch code)
         function = compact_text
-        chunk = ""
+        chunk = []
 
         for char in data:
             new_function = get_optimal_compactor_fn(char)
@@ -159,9 +161,9 @@ def compact(data):
                 if chunk:
                     yield chunk, function
 
-                chunk = ""
+                chunk = []
                 function = new_function
-            chunk += char
+            chunk.append(char)
 
         if chunk:
             yield chunk, function
@@ -171,7 +173,7 @@ def compact(data):
 
 
 def get_optimal_compactor_fn(char):
-    if 48 <= ord(char) <= 57:
+    if 48 <= char <= 57:
         return compact_numbers
 
     if char in CHARACTERS_LOOKUP:
diff --git a/pdf417gen/data.py b/pdf417gen/data.py
index 2cf3211..fb59703 100644
--- a/pdf417gen/data.py
+++ b/pdf417gen/data.py
@@ -5,103 +5,103 @@ MIXED = 'MIXED'
 PUNCT = 'PUNCT'
 
 CHARACTERS_LOOKUP = {
-    "'": {PUNCT: 28},
-    ' ': {UPPER: 26, LOWER: 26, MIXED: 26},
-    '!': {PUNCT: 10},
-    '#': {MIXED: 15},
-    '$': {MIXED: 18, PUNCT: 18},
-    '%': {MIXED: 21},
-    '&': {MIXED: 10},
-    '(': {PUNCT: 23},
-    ')': {PUNCT: 24},
-    '*': {MIXED: 22, PUNCT: 22},
-    '+': {MIXED: 20},
-    ',': {MIXED: 13, PUNCT: 13},
-    '-': {MIXED: 16, PUNCT: 16},
-    '.': {MIXED: 17, PUNCT: 17},
-    '/': {MIXED: 19, PUNCT: 19},
-    '0': {MIXED: 0},
-    '1': {MIXED: 1},
-    '2': {MIXED: 2},
-    '3': {MIXED: 3},
-    '4': {MIXED: 4},
-    '5': {MIXED: 5},
-    '6': {MIXED: 6},
-    '7': {MIXED: 7},
-    '8': {MIXED: 8},
-    '9': {MIXED: 9},
-    ':': {MIXED: 14, PUNCT: 14},
-    ';': {PUNCT: 0},
-    '<': {PUNCT: 1},
-    '=': {MIXED: 23},
-    '>': {PUNCT: 2},
-    '?': {PUNCT: 25},
-    '@': {PUNCT: 3},
-    '[': {PUNCT: 4},
-    '\\': {PUNCT: 5},
-    '\n': {PUNCT: 15},
-    '\r': {MIXED: 11, PUNCT: 11},
-    '\t': {MIXED: 12, PUNCT: 12},
-    ']': {PUNCT: 6},
-    '^': {MIXED: 24},
-    '_': {PUNCT: 7},
-    '`': {PUNCT: 8},
-    'a': {LOWER: 0},
-    'A': {UPPER: 0},
-    'b': {LOWER: 1},
-    'B': {UPPER: 1},
-    'c': {LOWER: 2},
-    'C': {UPPER: 2},
-    'd': {LOWER: 3},
-    'D': {UPPER: 3},
-    'e': {LOWER: 4},
-    'E': {UPPER: 4},
-    'f': {LOWER: 5},
-    'F': {UPPER: 5},
-    'g': {LOWER: 6, PUNCT: 20},
-    'G': {UPPER: 6},
-    'h': {LOWER: 7},
-    'H': {UPPER: 7},
-    'i': {LOWER: 8},
-    'I': {UPPER: 8},
-    'j': {LOWER: 9},
-    'J': {UPPER: 9},
-    'k': {LOWER: 10},
-    'K': {UPPER: 10},
-    'l': {LOWER: 11},
-    'L': {UPPER: 11},
-    'm': {LOWER: 12},
-    'M': {UPPER: 12},
-    'n': {LOWER: 13},
-    'N': {UPPER: 13},
-    'o': {LOWER: 14},
-    'O': {UPPER: 14},
-    'p': {LOWER: 15},
-    'P': {UPPER: 15},
-    'q': {LOWER: 16},
-    'Q': {UPPER: 16},
-    'r': {LOWER: 17},
-    'R': {UPPER: 17},
-    's': {LOWER: 18},
-    'S': {UPPER: 18},
-    't': {LOWER: 19},
-    'T': {UPPER: 19},
-    'u': {LOWER: 20},
-    'U': {UPPER: 20},
-    'v': {LOWER: 21},
-    'V': {UPPER: 21},
-    'w': {LOWER: 22},
-    'W': {UPPER: 22},
-    'x': {LOWER: 23},
-    'X': {UPPER: 23},
-    'y': {LOWER: 24},
-    'Y': {UPPER: 24},
-    'z': {LOWER: 25},
-    'Z': {UPPER: 25},
-    '{': {PUNCT: 26},
-    '|': {PUNCT: 21},
-    '}': {PUNCT: 27},
-    '~': {PUNCT: 9},
+    9:   {MIXED: 12, PUNCT: 12},               # \t
+    10:  {PUNCT: 15},                          # \n
+    13:  {MIXED: 11, PUNCT: 11},               # \r
+    32:  {UPPER: 26, LOWER: 26, MIXED: 26},    # SPACE
+    33:  {PUNCT: 10},                          # !
+    35:  {MIXED: 15},                          # #
+    36:  {MIXED: 18, PUNCT: 18},               # $
+    37:  {MIXED: 21},                          # %
+    38:  {MIXED: 10},                          # &
+    39:  {PUNCT: 28},                          # '
+    40:  {PUNCT: 23},                          # (
+    41:  {PUNCT: 24},                          # )
+    42:  {MIXED: 22, PUNCT: 22},               # *
+    43:  {MIXED: 20},                          # +
+    44:  {MIXED: 13, PUNCT: 13},               # ,
+    45:  {MIXED: 16, PUNCT: 16},               # -
+    46:  {MIXED: 17, PUNCT: 17},               # .
+    47:  {MIXED: 19, PUNCT: 19},               # /
+    48:  {MIXED: 0},                           # 0
+    49:  {MIXED: 1},                           # 1
+    50:  {MIXED: 2},                           # 2
+    51:  {MIXED: 3},                           # 3
+    52:  {MIXED: 4},                           # 4
+    53:  {MIXED: 5},                           # 5
+    54:  {MIXED: 6},                           # 6
+    55:  {MIXED: 7},                           # 7
+    56:  {MIXED: 8},                           # 8
+    57:  {MIXED: 9},                           # 9
+    58:  {MIXED: 14, PUNCT: 14},               # :
+    59:  {PUNCT: 0},                           # ;
+    60:  {PUNCT: 1},                           # <
+    61:  {MIXED: 23},                          # =
+    62:  {PUNCT: 2},                           # >
+    63:  {PUNCT: 25},                          # ?
+    64:  {PUNCT: 3},                           # @
+    65:  {UPPER: 0},                           # A
+    66:  {UPPER: 1},                           # B
+    67:  {UPPER: 2},                           # C
+    68:  {UPPER: 3},                           # D
+    69:  {UPPER: 4},                           # E
+    70:  {UPPER: 5},                           # F
+    71:  {UPPER: 6},                           # G
+    72:  {UPPER: 7},                           # H
+    73:  {UPPER: 8},                           # I
+    74:  {UPPER: 9},                           # J
+    75:  {UPPER: 10},                          # K
+    76:  {UPPER: 11},                          # L
+    77:  {UPPER: 12},                          # M
+    78:  {UPPER: 13},                          # N
+    79:  {UPPER: 14},                          # O
+    80:  {UPPER: 15},                          # P
+    81:  {UPPER: 16},                          # Q
+    82:  {UPPER: 17},                          # R
+    83:  {UPPER: 18},                          # S
+    84:  {UPPER: 19},                          # T
+    85:  {UPPER: 20},                          # U
+    86:  {UPPER: 21},                          # V
+    87:  {UPPER: 22},                          # W
+    88:  {UPPER: 23},                          # X
+    89:  {UPPER: 24},                          # Y
+    90:  {UPPER: 25},                          # Z
+    91:  {PUNCT: 4},                           # [
+    92:  {PUNCT: 5},                           # \
+    93:  {PUNCT: 6},                           # ]
+    94:  {MIXED: 24},                          # ^
+    95:  {PUNCT: 7},                           # _
+    96:  {PUNCT: 8},                           # `
+    97:  {LOWER: 0},                           # a
+    98:  {LOWER: 1},                           # b
+    99:  {LOWER: 2},                           # c
+    100: {LOWER: 3},                           # d
+    101: {LOWER: 4},                           # e
+    102: {LOWER: 5},                           # f
+    103: {LOWER: 6, PUNCT: 20},                # g
+    104: {LOWER: 7},                           # h
+    105: {LOWER: 8},                           # i
+    106: {LOWER: 9},                           # j
+    107: {LOWER: 10},                          # k
+    108: {LOWER: 11},                          # l
+    109: {LOWER: 12},                          # m
+    110: {LOWER: 13},                          # n
+    111: {LOWER: 14},                          # o
+    112: {LOWER: 15},                          # p
+    113: {LOWER: 16},                          # q
+    114: {LOWER: 17},                          # r
+    115: {LOWER: 18},                          # s
+    116: {LOWER: 19},                          # t
+    117: {LOWER: 20},                          # u
+    118: {LOWER: 21},                          # v
+    119: {LOWER: 22},                          # w
+    120: {LOWER: 23},                          # x
+    121: {LOWER: 24},                          # y
+    122: {LOWER: 25},                          # z
+    123: {PUNCT: 26},                          # {
+    124: {PUNCT: 21},                          # |
+    125: {PUNCT: 27},                          # }
+    126: {PUNCT: 9},                           # ~
 }
 
 # Switch codes between submodes
diff --git a/pdf417gen/encoding.py b/pdf417gen/encoding.py
index 7e7b05b..23f9451 100644
--- a/pdf417gen/encoding.py
+++ b/pdf417gen/encoding.py
@@ -2,10 +2,10 @@ from __future__ import division
 
 import math
 
-from .codes import map_code_word
-from .compaction import compact
-from .error_correction import compute_error_correction_code_words
-from .util import chunks
+from pdf417gen.codes import map_code_word
+from pdf417gen.compaction import compact
+from pdf417gen.error_correction import compute_error_correction_code_words
+from pdf417gen.util import chunks, to_bytes
 
 START_CHARACTER = 0x1fea8
 STOP_CHARACTER = 0x3fa29
@@ -19,8 +19,11 @@ MAX_CODE_WORDS = 928
 MIN_ROWS = 3
 MAX_ROWS = 90
 
+# Encoding to use when given a string and encoding is not specified
+DEFAULT_ENCODING = 'utf-8'
 
-def encode(data, columns=6, security_level=2):
+
+def encode(data, columns=6, security_level=2, encoding=DEFAULT_ENCODING):
     if columns < 1 or columns > 30:
         raise ValueError("'columns' must be between 1 and 30. Given: %r" % columns)
 
@@ -29,8 +32,11 @@ def encode(data, columns=6, security_level=2):
 
     num_cols = columns  # Nomenclature
 
+    # Prepare input
+    data_bytes = to_bytes(data, encoding)
+
     # Convert data to code words and split into rows
-    code_words = encode_high(data, num_cols, security_level)
+    code_words = encode_high(data_bytes, num_cols, security_level)
     rows = list(chunks(code_words, num_cols))
 
     return list(encode_rows(rows, num_cols, security_level))
diff --git a/pdf417gen/util.py b/pdf417gen/util.py
index 6e8d648..2d60d28 100644
--- a/pdf417gen/util.py
+++ b/pdf417gen/util.py
@@ -1,8 +1,10 @@
 from __future__ import division
 
+from builtins import bytes, str
+
 
 def from_base(digits, base):
-    return sum([v * (base ** (len(digits) - k - 1)) for k, v in enumerate(digits)])
+    return sum(v * (base ** (len(digits) - k - 1)) for k, v in enumerate(digits))
 
 
 def to_base(value, base):
@@ -23,3 +25,13 @@ def chunks(data, size):
     """Generator which chunks data into 6 bytes batches"""
     for i in range(0, len(data), size):
         yield data[i:i+size]
+
+
+def to_bytes(input, encoding='utf-8'):
+    if isinstance(input, bytes):
+        return bytes(input)
+
+    if isinstance(input, str):
+        return bytes(input, encoding)
+
+    raise ValueError("Invalid input, expected string or bytes")
diff --git a/setup.py b/setup.py
index 5c0f1f0..df9c459 100644
--- a/setup.py
+++ b/setup.py
@@ -24,9 +24,6 @@ setup(
         'Programming Language :: Python :: 2.6',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.0',
-        'Programming Language :: Python :: 3.1',
-        'Programming Language :: Python :: 3.2',
         'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',


Test patch:
diff --git a/tests/test_compaction.py b/tests/test_compaction.py
index f9036d0..e3db65c 100644
--- a/tests/test_compaction.py
+++ b/tests/test_compaction.py
@@ -1,58 +1,56 @@
-from pdf417gen.compaction import compact, compact_bytes, compact_numbers, compact_text, compact_text_interim
-
-
-# def test_byte_encoder_can_encode():
-#     enc = ByteEncoder()
-#     # Can encode any byte from 0 to 255
-#     for x in range(0, 255):
-#         assert enc.can_encode(chr(x))
-
+from pdf417gen.encoding import to_bytes
+from pdf417gen.compaction import (
+    compact, compact_bytes, compact_numbers, compact_text, compact_text_interim)
 
 
 def test_byte_compactor():
-    assert list(compact_bytes("alcool")) == [163, 238, 432, 766, 244]
-    assert list(compact_bytes("alcoolique")) == [163, 238, 432, 766, 244, 105, 113, 117, 101]
-
+    def do_compact(str):
+        return list(compact_bytes(to_bytes(str)))
 
-# def test_text_encoder_can_encode():
-#     enc = TextEncoder()
-#     # Can encode ASCII 9, 10, 13, 32, 33 and 35-126
-#     # TODO: should be able to encode double quote (ASCII 34), but doesn't currently
-#     for x in range(0, 255):
-#         can_encode = x in [9, 10, 13, 32, 33] or x in range(35, 127)
-#         assert enc.can_encode(chr(x)) == can_encode
+    assert do_compact("alcool") == [163, 238, 432, 766, 244]
+    assert do_compact("alcoolique") == [163, 238, 432, 766, 244, 105, 113, 117, 101]
 
 
 def test_text_compactor_interim():
+    def do_compact(str):
+        return list(compact_text_interim(to_bytes(str)))
+
     # Upper transitions
-    assert compact_text_interim("Ff") == [5, 27, 5]
-    assert compact_text_interim("F#") == [5, 28, 15]
-    assert compact_text_interim("F!") == [5, 28, 25, 10]
+    assert do_compact("Ff") == [5, 27, 5]
+    assert do_compact("F#") == [5, 28, 15]
+    assert do_compact("F!") == [5, 28, 25, 10]
 
     # Lower transitions
-    assert compact_text_interim("fF") == [27, 5, 28, 28, 5]
-    assert compact_text_interim("f#") == [27, 5, 28, 15]
-    assert compact_text_interim("f!") == [27, 5, 28, 25, 10]
+    assert do_compact("fF") == [27, 5, 28, 28, 5]
+    assert do_compact("f#") == [27, 5, 28, 15]
+    assert do_compact("f!") == [27, 5, 28, 25, 10]
 
 
 def test_text_compactor():
-    assert compact_text("Super ") == [567, 615, 137, 809]
-    assert compact_text("Super !") == [567, 615, 137, 808, 760]
+    def do_compact(str):
+        return list(compact_text(to_bytes(str)))
+
+    assert do_compact("Super ") == [567, 615, 137, 809]
+    assert do_compact("Super !") == [567, 615, 137, 808, 760]
 
 
 def test_numbers_compactor():
-    assert list(compact_numbers("01234")) == [112, 434]
+    numbers = [ord(x) for x in "01234"]
+    assert list(compact_numbers(numbers)) == [112, 434]
 
 
 def test_compact():
+    def do_compact(str):
+        return list(compact(to_bytes(str)))
+
     # When starting with text, the first code word does not need to be the switch
-    assert list(compact("ABC123")) == [1, 89, 902, 1, 223]
+    assert do_compact("ABC123") == [1, 89, 902, 1, 223]
 
     # When starting with numbers, we do need to switch
-    assert list(compact("123ABC")) == [902, 1, 223, 900, 1, 89]
+    assert do_compact("123ABC") == [902, 1, 223, 900, 1, 89]
 
     # Also with bytes
-    assert list(compact("\x0B")) == [901, 11]
+    assert do_compact(b"\x0B") == [901, 11]
 
     # Alternate bytes switch code when number of bytes is divisble by 6
-    assert list(compact("\x0B\x0B\x0B\x0B\x0B\x0B")) == [924, 18, 455, 694, 754, 291]
+    assert do_compact(b"\x0B\x0B\x0B\x0B\x0B\x0B") == [924, 18, 455, 694, 754, 291]
diff --git a/tests/test_encode.py b/tests/test_encode.py
index 6d2b5d1..756f99b 100644
--- a/tests/test_encode.py
+++ b/tests/test_encode.py
@@ -1,4 +1,6 @@
-from pdf417gen.encoding import encode, encode_high
+# -*- coding: utf-8 -*-
+
+from pdf417gen.encoding import encode, encode_high, to_bytes
 
 TEST_DATA = '\n'.join([
     'HRVHUB30',
@@ -34,7 +36,7 @@ def test_encode_high():
         570, 805, 26, 30, 536, 314, 104, 634, 865, 479, 900, 713, 846, 93, 59,
         313, 515, 294, 844]
 
-    assert encode_high(TEST_DATA, 6, 2) == expected
+    assert encode_high(to_bytes(TEST_DATA), 6, 2) == expected
 
 
 def test_encode_low():
@@ -69,3 +71,17 @@ def test_encode_low():
     ]
 
     assert list(encode(TEST_DATA, 6, 2)) == expected
+
+
+def test_encode_unicode():
+    # These two should encode to the same string
+    uc = u"love 💔"
+    by = b"love \xf0\x9f\x92\x94"
+
+    expected = [
+        [130728, 120256, 108592, 115526, 126604, 103616, 66594, 126094, 128318, 260649],
+        [130728, 125456, 83916, 107396, 83872, 97968, 77702, 98676, 128352, 260649],
+        [130728, 86496, 128114, 90190, 98038, 72124, 72814, 81040, 86256, 260649]]
+
+    assert encode(uc) == expected
+    assert encode(by) == expected