byceps/byceps

View on GitHub
byceps/services/ticketing/barcode_service.py

Summary

Maintainability
A
0 mins
Test Coverage
F
27%
"""
byceps.services.ticketing.barcode_service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Render Code 128 (set B) barcodes as SVG images.

This implementation only supports code set B.

:Copyright: 2014-2024 Jochen Kupperschmidt
:License: Revised BSD (see `LICENSE` file for details)
"""

from jinja2 import Template


# As seen on https://en.wikipedia.org/wiki/Code_128#Bar_code_widths
#
# (Value, 128B,      Widths)
VALUES_CHARS_WIDTHS = [
    (0, ' ', '212222'),
    (1, '!', '222122'),
    (2, '"', '222221'),
    (3, '#', '121223'),
    (4, '$', '121322'),
    (5, '%', '131222'),
    (6, '&', '122213'),
    (7, "'", '122312'),
    (8, '(', '132212'),
    (9, ')', '221213'),
    (10, '*', '221312'),
    (11, '+', '231212'),
    (12, ',', '112232'),
    (13, '-', '122132'),
    (14, '.', '122231'),
    (15, '/', '113222'),
    (16, '0', '123122'),
    (17, '1', '123221'),
    (18, '2', '223211'),
    (19, '3', '221132'),
    (20, '4', '221231'),
    (21, '5', '213212'),
    (22, '6', '223112'),
    (23, '7', '312131'),
    (24, '8', '311222'),
    (25, '9', '321122'),
    (26, ':', '321221'),
    (27, ';', '312212'),
    (28, '<', '322112'),
    (29, '=', '322211'),
    (30, '>', '212123'),
    (31, '?', '212321'),
    (32, '@', '232121'),
    (33, 'A', '111323'),
    (34, 'B', '131123'),
    (35, 'C', '131321'),
    (36, 'D', '112313'),
    (37, 'E', '132113'),
    (38, 'F', '132311'),
    (39, 'G', '211313'),
    (40, 'H', '231113'),
    (41, 'I', '231311'),
    (42, 'J', '112133'),
    (43, 'K', '112331'),
    (44, 'L', '132131'),
    (45, 'M', '113123'),
    (46, 'N', '113321'),
    (47, 'O', '133121'),
    (48, 'P', '313121'),
    (49, 'Q', '211331'),
    (50, 'R', '231131'),
    (51, 'S', '213113'),
    (52, 'T', '213311'),
    (53, 'U', '213131'),
    (54, 'V', '311123'),
    (55, 'W', '311321'),
    (56, 'X', '331121'),
    (57, 'Y', '312113'),
    (58, 'Z', '312311'),
    (59, '[', '332111'),
    (60, '\\', '314111'),
    (61, ']', '221411'),
    (62, '^', '431111'),
    (63, '_', '111224'),
    (64, '`', '111422'),
    (65, 'a', '121124'),
    (66, 'b', '121421'),
    (67, 'c', '141122'),
    (68, 'd', '141221'),
    (69, 'e', '112214'),
    (70, 'f', '112412'),
    (71, 'g', '122114'),
    (72, 'h', '122411'),
    (73, 'i', '142112'),
    (74, 'j', '142211'),
    (75, 'k', '241211'),
    (76, 'l', '221114'),
    (77, 'm', '413111'),
    (78, 'n', '241112'),
    (79, 'o', '134111'),
    (80, 'p', '111242'),
    (81, 'q', '121142'),
    (82, 'r', '121241'),
    (83, 's', '114212'),
    (84, 't', '124112'),
    (85, 'u', '124211'),
    (86, 'v', '411212'),
    (87, 'w', '421112'),
    (88, 'x', '421211'),
    (89, 'y', '212141'),
    (90, 'z', '214121'),
    (91, '{', '412121'),
    (92, '|', '111143'),
    (93, '}', '111341'),
    (94, '~', '131141'),
    (95, 'DEL', '114113'),
    (96, 'FNC_3', '114311'),
    (97, 'FNC_2', '411113'),
    (98, 'Shift_A', '411311'),
    (99, 'Code_C', '113141'),
    (100, 'FNC_4', '114131'),
    (101, 'Code_A', '311141'),
    (102, 'FNC_1', '411131'),
    (103, 'Start_A', '211412'),
    (104, 'Start_B', '211214'),
    (105, 'Start_C', '211232'),
    (106, 'Stop', '2331112'),
]


VALUES_TO_WIDTHS = {value: width for value, _, width in VALUES_CHARS_WIDTHS}
CHARS_TO_VALUES = {char: value for value, char, _ in VALUES_CHARS_WIDTHS}


SVG_TEMPLATE = Template(
    """
<svg xmlns="http://www.w3.org/2000/svg" width="{{ image_width }}" height="{{ image_height }}" viewBox="0 0 {{ image_width }} {{ image_height }}">
  <rect width="{{ image_width }}" height="{{ image_height }}" fill="white"/>
  {%- for bar_x, bar_width in bars %}
  <rect x="{{ bar_x }}" width="{{ bar_width }}" height="{{ image_height }}"/>
  {%- endfor %}
</svg>
""".strip()
)


def render_svg(text, *, thickness=3):
    values = list(_generate_values(text))
    bar_widths = list(_generate_bars(values, thickness))
    return _generate_svg(bar_widths)


def _generate_values(text):
    check_digit_calculation_values = []

    # start symbol
    start_symbol_value = _to_value('Start_B')
    yield start_symbol_value
    check_digit_calculation_values.append(start_symbol_value)

    text_values = list(map(_to_value, text))
    yield from text_values
    check_digit_calculation_values.extend(text_values)

    # check digit symbol
    check_digit_value = _calculate_check_digit_value(
        check_digit_calculation_values
    )
    yield check_digit_value

    # stop symbol
    stop_symbol_value = _to_value('Stop')
    yield stop_symbol_value


def _to_value(char):
    return CHARS_TO_VALUES[char]


def _calculate_check_digit_value(values):
    # Important: *Both* the start code *and* the
    # first encoded symbol are in position 1.
    symbol_products_sum = sum(
        max(1, position) * value for position, value in enumerate(values)
    )

    return symbol_products_sum % 103


def _generate_bars(values, thickness):
    for value in values:
        for width in VALUES_TO_WIDTHS[value]:
            bar_width = int(width) * thickness
            yield bar_width


def _generate_svg(bar_widths, *, image_height=100):
    image_width = sum(bar_widths)
    x = 0

    # Calculate where the individual bars are positioned
    # horizontally and how wide they are.
    bar_positions_and_widths = list(
        _calculate_bar_positions_and_widths(x, bar_widths)
    )

    # Render template.
    return SVG_TEMPLATE.render(
        image_width=image_width,
        image_height=image_height,
        bars=bar_positions_and_widths,
    )


def _calculate_bar_positions_and_widths(start_x, bar_widths):
    """Yield a (horizontal position, width) pair for each bar."""
    x = start_x

    draw_bar = True
    for width in bar_widths:
        if draw_bar:
            yield x, width

        draw_bar = not draw_bar
        x += width