diff options
Diffstat (limited to 'lib/pubcode/code128.py')
-rw-r--r-- | lib/pubcode/code128.py | 348 |
1 files changed, 348 insertions, 0 deletions
diff --git a/lib/pubcode/code128.py b/lib/pubcode/code128.py new file mode 100644 index 0000000..1c37f37 --- /dev/null +++ b/lib/pubcode/code128.py @@ -0,0 +1,348 @@ +# -*- coding: utf-8 -*- +# MIT Licensed, Copyright (c) 2015 Ari Koivula +# https://github.com/Venti-/pubcode +from __future__ import absolute_import, division, print_function, unicode_literals +from builtins import * # Use Python3-like builtins for Python2. +import base64 +import io +try: + from PIL import Image +except ImportError: + # PIL is needed only for creating images of the barcode. Set Image to None to signify that PIL is missing. + Image = None + + +class Code128(object): + class Error(Exception): + pass + + class CharsetError(Error): + pass + + class CharsetLengthError(Error): + pass + + class IncompatibleCharsetError(Error): + pass + + class MissingDependencyError(Error): + pass + + class UnknownFormatError(Error): + pass + + # List of bar and space weights, indexed by symbol character values (0-105), and the STOP character (106). + # The first weights is a bar and then it alternates. + _val2bars = [ + '212222', '222122', '222221', '121223', '121322', '131222', '122213', '122312', '132212', '221213', + '221312', '231212', '112232', '122132', '122231', '113222', '123122', '123221', '223211', '221132', + '221231', '213212', '223112', '312131', '311222', '321122', '321221', '312212', '322112', '322211', + '212123', '212321', '232121', '111323', '131123', '131321', '112313', '132113', '132311', '211313', + '231113', '231311', '112133', '112331', '132131', '113123', '113321', '133121', '313121', '211331', + '231131', '213113', '213311', '213131', '311123', '311321', '331121', '312113', '312311', '332111', + '314111', '221411', '431111', '111224', '111422', '121124', '121421', '141122', '141221', '112214', + '112412', '122114', '122411', '142112', '142211', '241211', '221114', '413111', '241112', '134111', + '111242', '121142', '121241', '114212', '124112', '124211', '411212', '421112', '421211', '212141', + '214121', '412121', '111143', '111341', '131141', '114113', '114311', '411113', '411311', '113141', + '114131', '311141', '411131', '211412', '211214', '211232', '2331112' + ] + + class Special(object): + """These are special characters used by the Code128 encoding.""" + START_A = '[Start Code A]' + START_B = '[Start Code B]' + START_C = '[Start Code C]' + CODE_A = '[Code A]' + CODE_B = '[Code B]' + CODE_C = '[Code C]' + SHIFT_A = '[Shift A]' + SHIFT_B = '[Shift B]' + FNC_1 = '[FNC 1]' + FNC_2 = '[FNC 2]' + FNC_3 = '[FNC 3]' + FNC_4 = '[FNC 4]' + STOP = '[Stop]' + + _start_codes = {'A': Special.START_A, 'B': Special.START_B, 'C': Special.START_C} + _char_codes = {'A': Special.CODE_A, 'B': Special.CODE_B, 'C': Special.CODE_C} + + # Lists mapping symbol values to characters in each character set. This defines the alphabet and Code128._sym2val + # is derived from this structure. + _val2sym = { + # Code Set A includes ordinals 0 through 95 and 7 special characters. The ordinals include digits, + # upper case characters, punctuation and control characters. + 'A': + [chr(x) for x in range(32, 95 + 1)] + + [chr(x) for x in range(0, 31 + 1)] + + [ + Special.FNC_3, Special.FNC_2, Special.SHIFT_B, Special.CODE_C, + Special.CODE_B, Special.FNC_4, Special.FNC_1, + Special.START_A, Special.START_B, Special.START_C, Special.STOP + ], + # Code Set B includes ordinals 32 through 127 and 7 special characters. The ordinals include digits, + # upper and lover case characters and punctuation. + 'B': + [chr(x) for x in range(32, 127 + 1)] + + [ + Special.FNC_3, Special.FNC_2, Special.SHIFT_A, Special.CODE_C, + Special.FNC_4, Special.CODE_A, Special.FNC_1, + Special.START_A, Special.START_B, Special.START_C, Special.STOP + ], + # Code Set C includes all pairs of 2 digits and 3 special characters. + 'C': + ['%02d' % (x,) for x in range(0, 99 + 1)] + + [ + Special.CODE_B, Special.CODE_A, Special.FNC_1, + Special.START_A, Special.START_B, Special.START_C, Special.STOP + ], + } + + # Dicts mapping characters to symbol values in each character set. + _sym2val = { + 'A': {char: val for val, char in enumerate(_val2sym['A'])}, + 'B': {char: val for val, char in enumerate(_val2sym['B'])}, + 'C': {char: val for val, char in enumerate(_val2sym['C'])}, + } + + # How large the quiet zone is on either side of the barcode, when quiet zone is used. + quiet_zone = 10 + + def __init__(self, data, charset=None): + """Initialize a barcode with data as described by the character sets in charset. + + :param data: The data to be encoded. + :param charset: A single character set (A, B or C), an iterable with a character set for each symbol or None. + - If a single character set is chosen, all characters will be encoded with that set, except for + incompatible characters which will be coded with one of the other character sets. + - If a sequence of character sets are given, incompatible characters will result in + Code128.IncompatibleCharsetError. Wrong size of the charset sequence in relation to data, + will result in Code128.CharsetLengthError. + - If None is given, the character set will be chosen as to minimize the length of the barcode. + """ + self._validate_charset(data, charset) + + if charset in ('A', 'B'): + charset *= len(data) + elif charset in ('C',): + charset *= (len(data) // 2) + if len(data) % 2 == 1: + # If there are an odd number of characters for charset C, encode the last character with charset B. + charset += 'B' + + self.data = data + self.symbol_values = self._encode(data, charset) + + def width(self, add_quiet_zone=False): + """Return the barcodes width in modules for a given data and character set combination. + + :param add_quiet_zone: Whether quiet zone should be included in the width. + + :return: Width of barcode in modules, which for images translates to pixels. + """ + quiet_zone = self.quiet_zone if add_quiet_zone else 0 + return len(self.modules) + 2 * quiet_zone + + @staticmethod + def _validate_charset(data, charset): + """"Validate that the charset is correct and throw an error if it isn't.""" + if len(charset) > 1: + charset_data_length = 0 + for symbol_charset in charset: + if symbol_charset not in ('A', 'B', 'C'): + raise Code128.CharsetError + charset_data_length += 2 if symbol_charset is 'C' else 1 + if charset_data_length != len(data): + raise Code128.CharsetLengthError + elif len(charset) == 1: + if charset not in ('A', 'B', 'C'): + raise Code128.CharsetError + elif charset is not None: + raise Code128.CharsetError + + @classmethod + def _encode(cls, data, charsets): + """Encode the data using the character sets in charsets. + + :param data: Data to be encoded. + :param charsets: Sequence of charsets that are used to encode the barcode. + Must be the exact amount of symbols needed to encode the data. + :return: List of the symbol values representing the barcode. + """ + result = [] + + charset = charsets[0] + start_symbol = cls._start_codes[charset] + result.append(cls._sym2val[charset][start_symbol]) + + cur = 0 + prev_charset = charsets[0] + for symbol_num in range(len(charsets)): + charset = charsets[symbol_num] + + if charset is not prev_charset: + # Handle a special case of there being a single A in middle of two B's or the other way around, where + # using a single shift character is more efficient than using two character set switches. + next_charset = charsets[symbol_num + 1] if symbol_num + 1 < len(charsets) else None + if charset == 'A' and prev_charset == next_charset == 'B': + result.append(cls._sym2val[prev_charset][cls.Special.SHIFT_A]) + elif charset == 'B' and prev_charset == next_charset == 'A': + result.append(cls._sym2val[prev_charset][cls.Special.SHIFT_B]) + else: + # This is the normal case. + charset_symbol = cls._char_codes[charset] + result.append(cls._sym2val[prev_charset][charset_symbol]) + prev_charset = charset + + nxt = cur + (2 if charset == 'C' else 1) + symbol = data[cur:nxt] + cur = nxt + result.append(cls._sym2val[charset][symbol]) + + result.append(cls._calc_checksum(result)) + result.append(cls._sym2val[charset][cls.Special.STOP]) + + return result + + @property + def symbols(self): + """List of the coded symbols as strings, with special characters included.""" + def _iter_symbols(symbol_values): + # The initial charset doesn't matter, as the start codes have the same symbol values in all charsets. + charset = 'A' + + shift_charset = None + for symbol_value in symbol_values: + if shift_charset: + symbol = self._val2sym[shift_charset][symbol_value] + shift_charset = None + else: + symbol = self._val2sym[charset][symbol_value] + + if symbol in (self.Special.START_A, self.Special.CODE_A): + charset = 'A' + elif symbol in (self.Special.START_B, self.Special.CODE_B): + charset = 'B' + elif symbol in (self.Special.START_C, self.Special.CODE_C): + charset = 'C' + elif symbol in (self.Special.SHIFT_A,): + shift_charset = 'A' + elif symbol in (self.Special.SHIFT_B,): + shift_charset = 'B' + + yield symbol + + return list(_iter_symbols(self.symbol_values)) + + @property + def bars(self): + """A string of the bar and space weights of the barcode. Starting with a bar and alternating. + + >>> barcode = Code128("Hello!", charset='B') + >>> barcode.bars + '2112142311131122142211142211141341112221221212412331112' + + :rtype: string + """ + return ''.join(map((lambda val: self._val2bars[val]), self.symbol_values)) + + @property + def modules(self): + """A list of the modules, with 0 representing a bar and 1 representing a space. + + >>> barcode = Code128("Hello!", charset='B') + >>> barcode.modules # doctest: +ELLIPSIS + [0, 0, 1, 0, 1, 1, 0, 1, ..., 0, 0, 0, 1, 0, 1, 0, 0] + + :rtype: list[int] + """ + def _iterate_modules(bars): + is_bar = True + for char in map(int, bars): + while char > 0: + char -= 1 + yield 0 if is_bar else 1 + is_bar = not is_bar + + return list(_iterate_modules(self.bars)) + + @staticmethod + def _calc_checksum(values): + """Calculate the symbol check character.""" + checksum = values[0] + for index, value in enumerate(values): + checksum += index * value + return checksum % 103 + + def image(self, height=1, module_width=1, add_quiet_zone=True): + """Get the barcode as PIL.Image. + + By default the image is one pixel high and the number of modules pixels wide, with 10 empty modules added to + each side to act as the quiet zone. The size can be modified by setting height and module_width, but if used in + a web page it might be a good idea to do the scaling on client side. + + :param height: Height of the image in number of pixels. + :param module_width: A multiplier for the width. + :param add_quiet_zone: Whether to add 10 empty modules to each side of the barcode. + + :rtype: PIL.Image + :return: A monochromatic image containing the barcode as black bars on white background. + """ + if Image is None: + raise Code128.MissingDependencyError("PIL module is required to use image method.") + + modules = list(self.modules) + if add_quiet_zone: + # Add ten space modules to each side of the barcode. + modules = [1] * self.quiet_zone + modules + [1] * self.quiet_zone + width = len(modules) + + img = Image.new(mode='1', size=(width, 1)) + img.putdata(modules) + + if height == 1 and module_width == 1: + return img + else: + new_size = (width * module_width, height) + return img.resize(new_size, resample=Image.NEAREST) + + def data_url(self, image_format='png', add_quiet_zone=True): + """Get a data URL representing the barcode. + + >>> barcode = Code128('Hello!', charset='B') + >>> barcode.data_url() # doctest: +ELLIPSIS + 'data:image/png;base64,...' + + :param image_format: Either 'png' or 'bmp'. + :param add_quiet_zone: Add a 10 white pixels on either side of the barcode. + + :raises: Code128.UnknownFormatError + :raises: Code128.MissingDependencyError + + :rtype: str + :returns: A data URL with the barcode as an image. + """ + memory_file = io.BytesIO() + pil_image = self.image(add_quiet_zone=add_quiet_zone) + + # Using BMP can often result in smaller data URLs than PNG, but it isn't as widely supported by browsers as PNG. + # GIFs result in data URLs 10 times bigger than PNG or BMP, possibly due to lack of support for monochrome GIFs + # in Pillow, so they shouldn't be used. + if image_format == 'png': + # Unfortunately there is no way to avoid adding the zlib headers. + # Using compress_level=0 sometimes results in a slightly bigger data size (by a few bytes), but there + # doesn't appear to be a difference between levels 9 and 1, so let's just use 1. + pil_image.save(memory_file, format='png', compress_level=1) + elif image_format == 'bmp': + pil_image.save(memory_file, format='bmp') + else: + raise Code128.UnknownFormatError('Only png and bmp are supported.') + + # Encode the data in the BytesIO object and convert the result into unicode. + base64_image = base64.b64encode(memory_file.getvalue()).decode('ascii') + + data_url = 'data:image/{format};base64,{base64_data}'.format( + format=image_format, + base64_data=base64_image + ) + + return data_url |