├── .flake8 ├── .github └── workflows │ └── test.yaml ├── .gitignore ├── .vermin ├── CHANGELOG.rst ├── LICENSE ├── Makefile ├── README.rst ├── TODO.rst ├── __init__.py ├── images ├── 1_basic.jpg ├── 2_columns.jpg ├── 3_security_level.jpg └── 4_rendering.jpg ├── pdf417_specification.pdf ├── pdf417gen ├── __init__.py ├── __main__.py ├── codes.py ├── compaction │ ├── __init__.py │ ├── byte.py │ ├── numeric.py │ ├── optimizations.py │ └── text.py ├── console.py ├── data.py ├── encoding.py ├── error_correction.py ├── rendering.py ├── types.py └── util.py ├── pyproject.toml └── tests ├── __init__,py ├── integration ├── __init__.py ├── conftest.py ├── test_integration.py └── testing_utils.py ├── performance.py ├── test_compaction.py ├── test_console.py ├── test_encode.py ├── test_error_correction.py ├── test_optimizations.py └── test_renderers.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=100 3 | exclude=.git,migrations,node_modules,docs 4 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 11 | 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v4 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade p 22 | # Until pdf417decoder does another release, we need to install from the dev branch 23 | pip install git+https://github.com/sparkfish/pdf417decoder.git@08c01172b7150bb2d2c0591566f43d45f9294fac#subdirectory=python 24 | pip install -e ".[test]" 25 | - name: Run tests 26 | run: | 27 | pytest 28 | - name: Validate minimum required version 29 | run: | 30 | vermin pdf417gen 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | .cache/ 4 | .pypirc 5 | /.coverage 6 | /.env 7 | build/ 8 | dist/ 9 | env*/ 10 | tmp/ 11 | /pyrightconfig.json 12 | /uv.lock 13 | .venv/ 14 | .vscode/ 15 | -------------------------------------------------------------------------------- /.vermin: -------------------------------------------------------------------------------- 1 | [vermin] 2 | only_show_violations = yes 3 | show_tips = no 4 | targets = 3.8- 5 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | pdf417gen changelog 3 | =================== 4 | 5 | 0.8.1 (2025-01-23) 6 | ------------------ 7 | 8 | * Fix a bug where byte compaction would generate the wrong number of code words 9 | (thanks @odony) 10 | 11 | 0.8.0 (2024-07-04) 12 | ------------------ 13 | 14 | * **BC BREAK**: Require Python 3.8+ 15 | * Modernized packaging 16 | * Minor performance improvements 17 | 18 | 0.7.1 (2020-01-12) 19 | ------------------ 20 | 21 | * Fix issue with Pillow 7 which changed the default resize filter to BICUBIC. 22 | * Minor performance improvement. 23 | 24 | 0.7.0 (2018-11-05) 25 | ------------------ 26 | 27 | * Fix max allowed code words calculation (#9) 28 | * Optimization: don't switch to numeric mode for fewer than 13 digits 29 | (#12, thanks to @Pavkazzz for the original implementation) 30 | 31 | These changes allow significantly more data to be encoded. 32 | 33 | 0.6.0 (2017-05-06) 34 | ------------------ 35 | 36 | * Add a CLI interface 37 | * Fix error in CHARACTERS_LOOKUP (#8) 38 | 39 | 0.5.0 (2017-02-11) 40 | ------------------ 41 | 42 | * Drop support for Python 3.0, 3.1 and 3.2 43 | * Fix handling of byte and string input in Python 3 (#4) 44 | 45 | 0.4.0 (skipped) 46 | --------------- 47 | 48 | 0.3.0 (2016-09-04) 49 | ------------------ 50 | 51 | * **BC BREAK**: renamed package from ``pdf417`` to ``pdf417gen`` for consistency 52 | with the name of the PyPI package 53 | * Now works with Pillow>=2.0.0, instead of 3.0.0 54 | 55 | 0.2.0 (2016-08-21) 56 | ------------------ 57 | 58 | * Add SVG renederer 59 | 60 | 0.1.0 (2016-08-20) 61 | ------------------ 62 | 63 | * Initial release 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | PDF417-Py License 2 | 3 | Copyright (c) 2016 Ivan Habunek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default : clean dist 2 | 3 | phony: test 4 | 5 | test: 6 | pytest 7 | 8 | htmlcov: 9 | pytest --cov=pdf417gen --cov-report=html 10 | 11 | dist: 12 | python -m build 13 | 14 | clean: 15 | rm -rf build dist *.egg-info MANIFEST htmlcov 16 | 17 | publish: 18 | twine upload dist/* 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | PDF417 barcode generator for Python 3 | =================================== 4 | 5 | .. image:: https://img.shields.io/travis/ihabunek/pdf417-py.svg?maxAge=3600&style=flat-square 6 | :target: https://travis-ci.org/ihabunek/pdf417-py 7 | .. image:: https://img.shields.io/badge/author-%40ihabunek-blue.svg?maxAge=3600&style=flat-square 8 | :target: https://twitter.com/ihabunek 9 | .. image:: https://img.shields.io/github/license/ihabunek/pdf417-py.svg?maxAge=3600&style=flat-square 10 | :target: https://opensource.org/licenses/MIT 11 | .. image:: https://img.shields.io/pypi/v/pdf417gen.svg?maxAge=3600&style=flat-square 12 | :target: https://pypi.python.org/pypi/pdf417gen 13 | 14 | 15 | Easily encode your data into a 2D barcode using the PDF417 format. 16 | 17 | .. image:: https://raw.githubusercontent.com/ihabunek/pdf417-py/master/images/1_basic.jpg 18 | 19 | Licensed under the MIT License, see `LICENSE `_. 20 | 21 | Installation 22 | ------------ 23 | 24 | Install using pip: 25 | 26 | .. code-block:: 27 | 28 | pip install pdf417gen 29 | 30 | 31 | CLI 32 | --- 33 | 34 | The ``pdf417gen`` command can be used to generate a barcode from commandline. It 35 | takes the input either as an argument or from stdin. 36 | 37 | .. code-block:: bash 38 | 39 | # Show help 40 | pdf417gen encode --help 41 | 42 | # Encode given text and display the barcode 43 | pdf417gen encode "Beautiful is better than ugly" 44 | 45 | # Encode given text and save barcode to a file (extension determines format) 46 | pdf417gen encode -o barcode.png "Explicit is better than implicit" 47 | 48 | # Input from a file 49 | pdf417gen encode < input.txt 50 | 51 | # Piped input 52 | python -c "import this" | pdf417gen encode 53 | 54 | # Use Macro PDF417 for large data with optional compression 55 | # produces barcode_01.png, barcode_02.png, ... 56 | pdf417gen encode --macro --compress -o barcode.png < large_data.txt 57 | 58 | 59 | Usage 60 | ----- 61 | 62 | Creating bar codes is done in two steps: 63 | 64 | * Encode a string to a list of code words using ``encode()`` 65 | * Render the barcode using one of the rendering functions: ``render_image()``, 66 | ``render_svg()``. 67 | 68 | Usage overview: 69 | 70 | .. code-block:: python 71 | 72 | from pdf417gen import encode, render_image, render_svg 73 | 74 | # Some data to encode 75 | text = """Beautiful is better than ugly. 76 | Explicit is better than implicit. 77 | Simple is better than complex. 78 | Complex is better than complicated.""" 79 | 80 | # Convert to code words 81 | codes = encode(text) 82 | 83 | # Generate barcode as image 84 | image = render_image(codes) # Pillow Image object 85 | image.save('barcode.jpg') 86 | 87 | # Generate barcode as SVG 88 | svg = render_svg(codes) # ElementTree object 89 | svg.write("barcode.svg") 90 | 91 | 92 | Supports unicode: 93 | 94 | .. code-block:: python 95 | 96 | # These two inputs encode to the same code words 97 | encode("love 💔") 98 | encode(b"love \xf0\x9f\x92\x94") 99 | 100 | # Default encoding is UTF-8, but you can specify your own 101 | encode("love 💔", encoding="utf-8") 102 | 103 | 104 | Encoding data 105 | ------------- 106 | 107 | The first step is to encode your data to a list of code words. 108 | 109 | .. code-block:: python 110 | 111 | encode(data, columns=6, security_level=2˙) 112 | 113 | Columns 114 | ~~~~~~~ 115 | 116 | The bar code size can be customized by defining the number of columns used to 117 | render the data, between 1 and 30, the default value is 6. A bar code can have a 118 | maximum of 90 rows, so for larger data sets you may need to increase the number 119 | of columns to decrease the rows count. 120 | 121 | .. code-block:: python 122 | 123 | codes = encode(text, columns=12) 124 | image = render_image(codes) 125 | image.show() 126 | 127 | .. image:: https://raw.githubusercontent.com/ihabunek/pdf417-py/master/images/2_columns.jpg 128 | 129 | Security level 130 | ~~~~~~~~~~~~~~ 131 | 132 | Increasing the security level will produce stronger (and more numerous) error 133 | correction codes, making the bar code larger, but less prone to corruption. The 134 | security level can range from 0 to 8, and procuces ``2^(level+1)`` error 135 | correction code words, meaning level 0 produces 2 code words and level 8 136 | produces 512. The default security level is 2. 137 | 138 | .. code-block:: python 139 | 140 | codes = encode(text, columns=12, security_level=6) 141 | image = render_image(codes) 142 | image.show() 143 | 144 | .. image:: https://raw.githubusercontent.com/ihabunek/pdf417-py/master/images/3_security_level.jpg 145 | 146 | Macro PDF417 147 | ~~~~~~~~~~~~ 148 | 149 | The `encode_macro` function can be used to encode large data sets that span multiple barcodes. 150 | 151 | .. code-block:: python 152 | 153 | from pdf417gen import encode_macro, render_image 154 | 155 | # Encode using Macro PDF417 156 | codes_list = encode_macro(large_text, columns=10) 157 | 158 | # Each barcode by default has some whitespace so we can create a page full of codes 159 | y_offset = 0 160 | for code in codes_list: 161 | image = render_image(code) 162 | combined_image.paste(image, (0, y_offset)) 163 | y_offset += image.height 164 | 165 | .. note:: 166 | 167 | Each barcode will be saved as `barcode_1.png`, `barcode_2.png`, etc. 168 | 169 | Render image 170 | ------------ 171 | 172 | The ``render_image`` function takes the following options: 173 | 174 | * ``scale`` - module width, in pixels (default: 3) 175 | * ``ratio`` - module height to width ratio (default: 3) 176 | * ``padding`` - image padding, in pixels (default: 20) 177 | * ``fg_color`` - foreground color (default: ``#000000``) 178 | * ``bg_color`` - background color (default: ``#FFFFFF``) 179 | 180 | .. note:: 181 | 182 | A module is the smallest element of a barcode, analogous to a pixel. Modules 183 | in a PDF417 bar code are tall and narrow. 184 | 185 | The function returns a Pillow Image_ object containing the barcode. 186 | 187 | Colors can be specified as hex codes or using HTML color names. 188 | 189 | .. code-block:: python 190 | 191 | codes = encode(text, columns=3) 192 | image = render_image(codes, scale=5, ratio=2, padding=5, fg_color="Indigo", bg_color="#ddd") 193 | image.show() 194 | 195 | .. image:: https://raw.githubusercontent.com/ihabunek/pdf417-py/master/images/4_rendering.jpg 196 | 197 | Render SVG 198 | ---------- 199 | 200 | The ``render_svg`` function takes the following options: 201 | 202 | * ``scale`` - module width, in pixels (default: 3) 203 | * ``ratio`` - module height to width ratio (default: 3) 204 | * ``padding`` - image padding, in pixels (default: 20) 205 | * ``color`` - foreground color (default: `#000000`) 206 | 207 | The function returns a ElementTree_ object containing the barcode in SVG format. 208 | 209 | Unlike ``render_image``, this function does not take a background color option. 210 | The background is left transparent. 211 | 212 | .. code-block:: python 213 | 214 | codes = encode(text, columns=3) 215 | svg = render_svg(codes, scale=5, ratio=2, color="Seaweed") 216 | svg.write('barcode.svg') 217 | 218 | See also 219 | -------- 220 | 221 | * pdf417-php_ - a PHP implementation 222 | * golang-pdf417_ - a Go implementation 223 | 224 | .. _pdf417-php: https://github.com/ihabunek/pdf417-php 225 | .. _golang-pdf417: https://github.com/ruudk/golang-pdf417 226 | .. _ElementTree: https://docs.python.org/3.5/library/xml.etree.elementtree.html#elementtree-objects 227 | .. _Image: https://pillow.readthedocs.io/en/3.2.x/reference/Image.html 228 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | TODO 2 | ==== 3 | 4 | Optimizations: 5 | 6 | * Use Mode Shift to switch to Byte compaction mode for a single code word 7 | see chapter 2.2.4.3 Mode Latch and Mode Shift Codewords 8 | 9 | * Use Sub-mode Shift codes when compacting text 10 | see chapter 2.2.4.4 Text Compaction Mode (TC) 11 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/pdf417-py/c163f2a6af8d159a54c56c431da6cc642c1c98b6/__init__.py -------------------------------------------------------------------------------- /images/1_basic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/pdf417-py/c163f2a6af8d159a54c56c431da6cc642c1c98b6/images/1_basic.jpg -------------------------------------------------------------------------------- /images/2_columns.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/pdf417-py/c163f2a6af8d159a54c56c431da6cc642c1c98b6/images/2_columns.jpg -------------------------------------------------------------------------------- /images/3_security_level.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/pdf417-py/c163f2a6af8d159a54c56c431da6cc642c1c98b6/images/3_security_level.jpg -------------------------------------------------------------------------------- /images/4_rendering.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/pdf417-py/c163f2a6af8d159a54c56c431da6cc642c1c98b6/images/4_rendering.jpg -------------------------------------------------------------------------------- /pdf417_specification.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/pdf417-py/c163f2a6af8d159a54c56c431da6cc642c1c98b6/pdf417_specification.pdf -------------------------------------------------------------------------------- /pdf417gen/__init__.py: -------------------------------------------------------------------------------- 1 | from pdf417gen.encoding import encode, encode_macro 2 | from pdf417gen.rendering import render_image, render_svg 3 | 4 | __all__ = ["encode", "encode_macro", "render_image", "render_svg"] 5 | -------------------------------------------------------------------------------- /pdf417gen/__main__.py: -------------------------------------------------------------------------------- 1 | from pdf417gen import console 2 | 3 | console.main() 4 | -------------------------------------------------------------------------------- /pdf417gen/codes.py: -------------------------------------------------------------------------------- 1 | # Converts high-level (base 929) code words into low-level code words (binary 2 | # patterns for drawing the bar code). 3 | CODES = [ 4 | [0x1d5c0, 0x1eaf0, 0x1f57c, 0x1d4e0, 0x1ea78, 0x1f53e, 0x1a8c0, 0x1d470, 0x1a860, 0x15040, 0x1a830, 0x15020, 0x1adc0, 0x1d6f0, 0x1eb7c, 0x1ace0, 0x1d678, 0x1eb3e, 0x158c0, 0x1ac70, 0x15860, 0x15dc0, 0x1aef0, 0x1d77c, 0x15ce0, 0x1ae78, 0x1d73e, 0x15c70, 0x1ae3c, 0x15ef0, 0x1af7c, 0x15e78, 0x1af3e, 0x15f7c, 0x1f5fa, 0x1d2e0, 0x1e978, 0x1f4be, 0x1a4c0, 0x1d270, 0x1e93c, 0x1a460, 0x1d238, 0x14840, 0x1a430, 0x1d21c, 0x14820, 0x1a418, 0x14810, 0x1a6e0, 0x1d378, 0x1e9be, 0x14cc0, 0x1a670, 0x1d33c, 0x14c60, 0x1a638, 0x1d31e, 0x14c30, 0x1a61c, 0x14ee0, 0x1a778, 0x1d3be, 0x14e70, 0x1a73c, 0x14e38, 0x1a71e, 0x14f78, 0x1a7be, 0x14f3c, 0x14f1e, 0x1a2c0, 0x1d170, 0x1e8bc, 0x1a260, 0x1d138, 0x1e89e, 0x14440, 0x1a230, 0x1d11c, 0x14420, 0x1a218, 0x14410, 0x14408, 0x146c0, 0x1a370, 0x1d1bc, 0x14660, 0x1a338, 0x1d19e, 0x14630, 0x1a31c, 0x14618, 0x1460c, 0x14770, 0x1a3bc, 0x14738, 0x1a39e, 0x1471c, 0x147bc, 0x1a160, 0x1d0b8, 0x1e85e, 0x14240, 0x1a130, 0x1d09c, 0x14220, 0x1a118, 0x1d08e, 0x14210, 0x1a10c, 0x14208, 0x1a106, 0x14360, 0x1a1b8, 0x1d0de, 0x14330, 0x1a19c, 0x14318, 0x1a18e, 0x1430c, 0x14306, 0x1a1de, 0x1438e, 0x14140, 0x1a0b0, 0x1d05c, 0x14120, 0x1a098, 0x1d04e, 0x14110, 0x1a08c, 0x14108, 0x1a086, 0x14104, 0x141b0, 0x14198, 0x1418c, 0x140a0, 0x1d02e, 0x1a04c, 0x1a046, 0x14082, 0x1cae0, 0x1e578, 0x1f2be, 0x194c0, 0x1ca70, 0x1e53c, 0x19460, 0x1ca38, 0x1e51e, 0x12840, 0x19430, 0x12820, 0x196e0, 0x1cb78, 0x1e5be, 0x12cc0, 0x19670, 0x1cb3c, 0x12c60, 0x19638, 0x12c30, 0x12c18, 0x12ee0, 0x19778, 0x1cbbe, 0x12e70, 0x1973c, 0x12e38, 0x12e1c, 0x12f78, 0x197be, 0x12f3c, 0x12fbe, 0x1dac0, 0x1ed70, 0x1f6bc, 0x1da60, 0x1ed38, 0x1f69e, 0x1b440, 0x1da30, 0x1ed1c, 0x1b420, 0x1da18, 0x1ed0e, 0x1b410, 0x1da0c, 0x192c0, 0x1c970, 0x1e4bc, 0x1b6c0, 0x19260, 0x1c938, 0x1e49e, 0x1b660, 0x1db38, 0x1ed9e, 0x16c40, 0x12420, 0x19218, 0x1c90e, 0x16c20, 0x1b618, 0x16c10, 0x126c0, 0x19370, 0x1c9bc, 0x16ec0, 0x12660, 0x19338, 0x1c99e, 0x16e60, 0x1b738, 0x1db9e, 0x16e30, 0x12618, 0x16e18, 0x12770, 0x193bc, 0x16f70, 0x12738, 0x1939e, 0x16f38, 0x1b79e, 0x16f1c, 0x127bc, 0x16fbc, 0x1279e, 0x16f9e, 0x1d960, 0x1ecb8, 0x1f65e, 0x1b240, 0x1d930, 0x1ec9c, 0x1b220, 0x1d918, 0x1ec8e, 0x1b210, 0x1d90c, 0x1b208, 0x1b204, 0x19160, 0x1c8b8, 0x1e45e, 0x1b360, 0x19130, 0x1c89c, 0x16640, 0x12220, 0x1d99c, 0x1c88e, 0x16620, 0x12210, 0x1910c, 0x16610, 0x1b30c, 0x19106, 0x12204, 0x12360, 0x191b8, 0x1c8de, 0x16760, 0x12330, 0x1919c, 0x16730, 0x1b39c, 0x1918e, 0x16718, 0x1230c, 0x12306, 0x123b8, 0x191de, 0x167b8, 0x1239c, 0x1679c, 0x1238e, 0x1678e, 0x167de, 0x1b140, 0x1d8b0, 0x1ec5c, 0x1b120, 0x1d898, 0x1ec4e, 0x1b110, 0x1d88c, 0x1b108, 0x1d886, 0x1b104, 0x1b102, 0x12140, 0x190b0, 0x1c85c, 0x16340, 0x12120, 0x19098, 0x1c84e, 0x16320, 0x1b198, 0x1d8ce, 0x16310, 0x12108, 0x19086, 0x16308, 0x1b186, 0x16304, 0x121b0, 0x190dc, 0x163b0, 0x12198, 0x190ce, 0x16398, 0x1b1ce, 0x1638c, 0x12186, 0x16386, 0x163dc, 0x163ce, 0x1b0a0, 0x1d858, 0x1ec2e, 0x1b090, 0x1d84c, 0x1b088, 0x1d846, 0x1b084, 0x1b082, 0x120a0, 0x19058, 0x1c82e, 0x161a0, 0x12090, 0x1904c, 0x16190, 0x1b0cc, 0x19046, 0x16188, 0x12084, 0x16184, 0x12082, 0x120d8, 0x161d8, 0x161cc, 0x161c6, 0x1d82c, 0x1d826, 0x1b042, 0x1902c, 0x12048, 0x160c8, 0x160c4, 0x160c2, 0x18ac0, 0x1c570, 0x1e2bc, 0x18a60, 0x1c538, 0x11440, 0x18a30, 0x1c51c, 0x11420, 0x18a18, 0x11410, 0x11408, 0x116c0, 0x18b70, 0x1c5bc, 0x11660, 0x18b38, 0x1c59e, 0x11630, 0x18b1c, 0x11618, 0x1160c, 0x11770, 0x18bbc, 0x11738, 0x18b9e, 0x1171c, 0x117bc, 0x1179e, 0x1cd60, 0x1e6b8, 0x1f35e, 0x19a40, 0x1cd30, 0x1e69c, 0x19a20, 0x1cd18, 0x1e68e, 0x19a10, 0x1cd0c, 0x19a08, 0x1cd06, 0x18960, 0x1c4b8, 0x1e25e, 0x19b60, 0x18930, 0x1c49c, 0x13640, 0x11220, 0x1cd9c, 0x1c48e, 0x13620, 0x19b18, 0x1890c, 0x13610, 0x11208, 0x13608, 0x11360, 0x189b8, 0x1c4de, 0x13760, 0x11330, 0x1cdde, 0x13730, 0x19b9c, 0x1898e, 0x13718, 0x1130c, 0x1370c, 0x113b8, 0x189de, 0x137b8, 0x1139c, 0x1379c, 0x1138e, 0x113de, 0x137de, 0x1dd40, 0x1eeb0, 0x1f75c, 0x1dd20, 0x1ee98, 0x1f74e, 0x1dd10, 0x1ee8c, 0x1dd08, 0x1ee86, 0x1dd04, 0x19940, 0x1ccb0, 0x1e65c, 0x1bb40, 0x19920, 0x1eedc, 0x1e64e, 0x1bb20, 0x1dd98, 0x1eece, 0x1bb10, 0x19908, 0x1cc86, 0x1bb08, 0x1dd86, 0x19902, 0x11140, 0x188b0, 0x1c45c, 0x13340, 0x11120, 0x18898, 0x1c44e, 0x17740, 0x13320, 0x19998, 0x1ccce, 0x17720, 0x1bb98, 0x1ddce, 0x18886, 0x17710, 0x13308, 0x19986, 0x17708, 0x11102, 0x111b0, 0x188dc, 0x133b0, 0x11198, 0x188ce, 0x177b0, 0x13398, 0x199ce, 0x17798, 0x1bbce, 0x11186, 0x13386, 0x111dc, 0x133dc, 0x111ce, 0x177dc, 0x133ce, 0x1dca0, 0x1ee58, 0x1f72e, 0x1dc90, 0x1ee4c, 0x1dc88, 0x1ee46, 0x1dc84, 0x1dc82, 0x198a0, 0x1cc58, 0x1e62e, 0x1b9a0, 0x19890, 0x1ee6e, 0x1b990, 0x1dccc, 0x1cc46, 0x1b988, 0x19884, 0x1b984, 0x19882, 0x1b982, 0x110a0, 0x18858, 0x1c42e, 0x131a0, 0x11090, 0x1884c, 0x173a0, 0x13190, 0x198cc, 0x18846, 0x17390, 0x1b9cc, 0x11084, 0x17388, 0x13184, 0x11082, 0x13182, 0x110d8, 0x1886e, 0x131d8, 0x110cc, 0x173d8, 0x131cc, 0x110c6, 0x173cc, 0x131c6, 0x110ee, 0x173ee, 0x1dc50, 0x1ee2c, 0x1dc48, 0x1ee26, 0x1dc44, 0x1dc42, 0x19850, 0x1cc2c, 0x1b8d0, 0x19848, 0x1cc26, 0x1b8c8, 0x1dc66, 0x1b8c4, 0x19842, 0x1b8c2, 0x11050, 0x1882c, 0x130d0, 0x11048, 0x18826, 0x171d0, 0x130c8, 0x19866, 0x171c8, 0x1b8e6, 0x11042, 0x171c4, 0x130c2, 0x171c2, 0x130ec, 0x171ec, 0x171e6, 0x1ee16, 0x1dc22, 0x1cc16, 0x19824, 0x19822, 0x11028, 0x13068, 0x170e8, 0x11022, 0x13062, 0x18560, 0x10a40, 0x18530, 0x10a20, 0x18518, 0x1c28e, 0x10a10, 0x1850c, 0x10a08, 0x18506, 0x10b60, 0x185b8, 0x1c2de, 0x10b30, 0x1859c, 0x10b18, 0x1858e, 0x10b0c, 0x10b06, 0x10bb8, 0x185de, 0x10b9c, 0x10b8e, 0x10bde, 0x18d40, 0x1c6b0, 0x1e35c, 0x18d20, 0x1c698, 0x18d10, 0x1c68c, 0x18d08, 0x1c686, 0x18d04, 0x10940, 0x184b0, 0x1c25c, 0x11b40, 0x10920, 0x1c6dc, 0x1c24e, 0x11b20, 0x18d98, 0x1c6ce, 0x11b10, 0x10908, 0x18486, 0x11b08, 0x18d86, 0x10902, 0x109b0, 0x184dc, 0x11bb0, 0x10998, 0x184ce, 0x11b98, 0x18dce, 0x11b8c, 0x10986, 0x109dc, 0x11bdc, 0x109ce, 0x11bce, 0x1cea0, 0x1e758, 0x1f3ae, 0x1ce90, 0x1e74c, 0x1ce88, 0x1e746, 0x1ce84, 0x1ce82, 0x18ca0, 0x1c658, 0x19da0, 0x18c90, 0x1c64c, 0x19d90, 0x1cecc, 0x1c646, 0x19d88, 0x18c84, 0x19d84, 0x18c82, 0x19d82, 0x108a0, 0x18458, 0x119a0, 0x10890, 0x1c66e, 0x13ba0, 0x11990, 0x18ccc, 0x18446, 0x13b90, 0x19dcc, 0x10884, 0x13b88, 0x11984, 0x10882, 0x11982, 0x108d8, 0x1846e, 0x119d8, 0x108cc, 0x13bd8, 0x119cc, 0x108c6, 0x13bcc, 0x119c6, 0x108ee, 0x119ee, 0x13bee, 0x1ef50, 0x1f7ac, 0x1ef48, 0x1f7a6, 0x1ef44, 0x1ef42, 0x1ce50, 0x1e72c, 0x1ded0, 0x1ef6c, 0x1e726, 0x1dec8, 0x1ef66, 0x1dec4, 0x1ce42, 0x1dec2, 0x18c50, 0x1c62c, 0x19cd0, 0x18c48, 0x1c626, 0x1bdd0, 0x19cc8, 0x1ce66, 0x1bdc8, 0x1dee6, 0x18c42, 0x1bdc4, 0x19cc2, 0x1bdc2, 0x10850, 0x1842c, 0x118d0, 0x10848, 0x18426, 0x139d0, 0x118c8, 0x18c66, 0x17bd0, 0x139c8, 0x19ce6, 0x10842, 0x17bc8, 0x1bde6, 0x118c2, 0x17bc4, 0x1086c, 0x118ec, 0x10866, 0x139ec, 0x118e6, 0x17bec, 0x139e6, 0x17be6, 0x1ef28, 0x1f796, 0x1ef24, 0x1ef22, 0x1ce28, 0x1e716, 0x1de68, 0x1ef36, 0x1de64, 0x1ce22, 0x1de62, 0x18c28, 0x1c616, 0x19c68, 0x18c24, 0x1bce8, 0x19c64, 0x18c22, 0x1bce4, 0x19c62, 0x1bce2, 0x10828, 0x18416, 0x11868, 0x18c36, 0x138e8, 0x11864, 0x10822, 0x179e8, 0x138e4, 0x11862, 0x179e4, 0x138e2, 0x179e2, 0x11876, 0x179f6, 0x1ef12, 0x1de34, 0x1de32, 0x19c34, 0x1bc74, 0x1bc72, 0x11834, 0x13874, 0x178f4, 0x178f2, 0x10540, 0x10520, 0x18298, 0x10510, 0x10508, 0x10504, 0x105b0, 0x10598, 0x1058c, 0x10586, 0x105dc, 0x105ce, 0x186a0, 0x18690, 0x1c34c, 0x18688, 0x1c346, 0x18684, 0x18682, 0x104a0, 0x18258, 0x10da0, 0x186d8, 0x1824c, 0x10d90, 0x186cc, 0x10d88, 0x186c6, 0x10d84, 0x10482, 0x10d82, 0x104d8, 0x1826e, 0x10dd8, 0x186ee, 0x10dcc, 0x104c6, 0x10dc6, 0x104ee, 0x10dee, 0x1c750, 0x1c748, 0x1c744, 0x1c742, 0x18650, 0x18ed0, 0x1c76c, 0x1c326, 0x18ec8, 0x1c766, 0x18ec4, 0x18642, 0x18ec2, 0x10450, 0x10cd0, 0x10448, 0x18226, 0x11dd0, 0x10cc8, 0x10444, 0x11dc8, 0x10cc4, 0x10442, 0x11dc4, 0x10cc2, 0x1046c, 0x10cec, 0x10466, 0x11dec, 0x10ce6, 0x11de6, 0x1e7a8, 0x1e7a4, 0x1e7a2, 0x1c728, 0x1cf68, 0x1e7b6, 0x1cf64, 0x1c722, 0x1cf62, 0x18628, 0x1c316, 0x18e68, 0x1c736, 0x19ee8, 0x18e64, 0x18622, 0x19ee4, 0x18e62, 0x19ee2, 0x10428, 0x18216, 0x10c68, 0x18636, 0x11ce8, 0x10c64, 0x10422, 0x13de8, 0x11ce4, 0x10c62, 0x13de4, 0x11ce2, 0x10436, 0x10c76, 0x11cf6, 0x13df6, 0x1f7d4, 0x1f7d2, 0x1e794, 0x1efb4, 0x1e792, 0x1efb2, 0x1c714, 0x1cf34, 0x1c712, 0x1df74, 0x1cf32, 0x1df72, 0x18614, 0x18e34, 0x18612, 0x19e74, 0x18e32, 0x1bef4], # noqa 5 | [0x1f560, 0x1fab8, 0x1ea40, 0x1f530, 0x1fa9c, 0x1ea20, 0x1f518, 0x1fa8e, 0x1ea10, 0x1f50c, 0x1ea08, 0x1f506, 0x1ea04, 0x1eb60, 0x1f5b8, 0x1fade, 0x1d640, 0x1eb30, 0x1f59c, 0x1d620, 0x1eb18, 0x1f58e, 0x1d610, 0x1eb0c, 0x1d608, 0x1eb06, 0x1d604, 0x1d760, 0x1ebb8, 0x1f5de, 0x1ae40, 0x1d730, 0x1eb9c, 0x1ae20, 0x1d718, 0x1eb8e, 0x1ae10, 0x1d70c, 0x1ae08, 0x1d706, 0x1ae04, 0x1af60, 0x1d7b8, 0x1ebde, 0x15e40, 0x1af30, 0x1d79c, 0x15e20, 0x1af18, 0x1d78e, 0x15e10, 0x1af0c, 0x15e08, 0x1af06, 0x15f60, 0x1afb8, 0x1d7de, 0x15f30, 0x1af9c, 0x15f18, 0x1af8e, 0x15f0c, 0x15fb8, 0x1afde, 0x15f9c, 0x15f8e, 0x1e940, 0x1f4b0, 0x1fa5c, 0x1e920, 0x1f498, 0x1fa4e, 0x1e910, 0x1f48c, 0x1e908, 0x1f486, 0x1e904, 0x1e902, 0x1d340, 0x1e9b0, 0x1f4dc, 0x1d320, 0x1e998, 0x1f4ce, 0x1d310, 0x1e98c, 0x1d308, 0x1e986, 0x1d304, 0x1d302, 0x1a740, 0x1d3b0, 0x1e9dc, 0x1a720, 0x1d398, 0x1e9ce, 0x1a710, 0x1d38c, 0x1a708, 0x1d386, 0x1a704, 0x1a702, 0x14f40, 0x1a7b0, 0x1d3dc, 0x14f20, 0x1a798, 0x1d3ce, 0x14f10, 0x1a78c, 0x14f08, 0x1a786, 0x14f04, 0x14fb0, 0x1a7dc, 0x14f98, 0x1a7ce, 0x14f8c, 0x14f86, 0x14fdc, 0x14fce, 0x1e8a0, 0x1f458, 0x1fa2e, 0x1e890, 0x1f44c, 0x1e888, 0x1f446, 0x1e884, 0x1e882, 0x1d1a0, 0x1e8d8, 0x1f46e, 0x1d190, 0x1e8cc, 0x1d188, 0x1e8c6, 0x1d184, 0x1d182, 0x1a3a0, 0x1d1d8, 0x1e8ee, 0x1a390, 0x1d1cc, 0x1a388, 0x1d1c6, 0x1a384, 0x1a382, 0x147a0, 0x1a3d8, 0x1d1ee, 0x14790, 0x1a3cc, 0x14788, 0x1a3c6, 0x14784, 0x14782, 0x147d8, 0x1a3ee, 0x147cc, 0x147c6, 0x147ee, 0x1e850, 0x1f42c, 0x1e848, 0x1f426, 0x1e844, 0x1e842, 0x1d0d0, 0x1e86c, 0x1d0c8, 0x1e866, 0x1d0c4, 0x1d0c2, 0x1a1d0, 0x1d0ec, 0x1a1c8, 0x1d0e6, 0x1a1c4, 0x1a1c2, 0x143d0, 0x1a1ec, 0x143c8, 0x1a1e6, 0x143c4, 0x143c2, 0x143ec, 0x143e6, 0x1e828, 0x1f416, 0x1e824, 0x1e822, 0x1d068, 0x1e836, 0x1d064, 0x1d062, 0x1a0e8, 0x1d076, 0x1a0e4, 0x1a0e2, 0x141e8, 0x1a0f6, 0x141e4, 0x141e2, 0x1e814, 0x1e812, 0x1d034, 0x1d032, 0x1a074, 0x1a072, 0x1e540, 0x1f2b0, 0x1f95c, 0x1e520, 0x1f298, 0x1f94e, 0x1e510, 0x1f28c, 0x1e508, 0x1f286, 0x1e504, 0x1e502, 0x1cb40, 0x1e5b0, 0x1f2dc, 0x1cb20, 0x1e598, 0x1f2ce, 0x1cb10, 0x1e58c, 0x1cb08, 0x1e586, 0x1cb04, 0x1cb02, 0x19740, 0x1cbb0, 0x1e5dc, 0x19720, 0x1cb98, 0x1e5ce, 0x19710, 0x1cb8c, 0x19708, 0x1cb86, 0x19704, 0x19702, 0x12f40, 0x197b0, 0x1cbdc, 0x12f20, 0x19798, 0x1cbce, 0x12f10, 0x1978c, 0x12f08, 0x19786, 0x12f04, 0x12fb0, 0x197dc, 0x12f98, 0x197ce, 0x12f8c, 0x12f86, 0x12fdc, 0x12fce, 0x1f6a0, 0x1fb58, 0x16bf0, 0x1f690, 0x1fb4c, 0x169f8, 0x1f688, 0x1fb46, 0x168fc, 0x1f684, 0x1f682, 0x1e4a0, 0x1f258, 0x1f92e, 0x1eda0, 0x1e490, 0x1fb6e, 0x1ed90, 0x1f6cc, 0x1f246, 0x1ed88, 0x1e484, 0x1ed84, 0x1e482, 0x1ed82, 0x1c9a0, 0x1e4d8, 0x1f26e, 0x1dba0, 0x1c990, 0x1e4cc, 0x1db90, 0x1edcc, 0x1e4c6, 0x1db88, 0x1c984, 0x1db84, 0x1c982, 0x1db82, 0x193a0, 0x1c9d8, 0x1e4ee, 0x1b7a0, 0x19390, 0x1c9cc, 0x1b790, 0x1dbcc, 0x1c9c6, 0x1b788, 0x19384, 0x1b784, 0x19382, 0x1b782, 0x127a0, 0x193d8, 0x1c9ee, 0x16fa0, 0x12790, 0x193cc, 0x16f90, 0x1b7cc, 0x193c6, 0x16f88, 0x12784, 0x16f84, 0x12782, 0x127d8, 0x193ee, 0x16fd8, 0x127cc, 0x16fcc, 0x127c6, 0x16fc6, 0x127ee, 0x1f650, 0x1fb2c, 0x165f8, 0x1f648, 0x1fb26, 0x164fc, 0x1f644, 0x1647e, 0x1f642, 0x1e450, 0x1f22c, 0x1ecd0, 0x1e448, 0x1f226, 0x1ecc8, 0x1f666, 0x1ecc4, 0x1e442, 0x1ecc2, 0x1c8d0, 0x1e46c, 0x1d9d0, 0x1c8c8, 0x1e466, 0x1d9c8, 0x1ece6, 0x1d9c4, 0x1c8c2, 0x1d9c2, 0x191d0, 0x1c8ec, 0x1b3d0, 0x191c8, 0x1c8e6, 0x1b3c8, 0x1d9e6, 0x1b3c4, 0x191c2, 0x1b3c2, 0x123d0, 0x191ec, 0x167d0, 0x123c8, 0x191e6, 0x167c8, 0x1b3e6, 0x167c4, 0x123c2, 0x167c2, 0x123ec, 0x167ec, 0x123e6, 0x167e6, 0x1f628, 0x1fb16, 0x162fc, 0x1f624, 0x1627e, 0x1f622, 0x1e428, 0x1f216, 0x1ec68, 0x1f636, 0x1ec64, 0x1e422, 0x1ec62, 0x1c868, 0x1e436, 0x1d8e8, 0x1c864, 0x1d8e4, 0x1c862, 0x1d8e2, 0x190e8, 0x1c876, 0x1b1e8, 0x1d8f6, 0x1b1e4, 0x190e2, 0x1b1e2, 0x121e8, 0x190f6, 0x163e8, 0x121e4, 0x163e4, 0x121e2, 0x163e2, 0x121f6, 0x163f6, 0x1f614, 0x1617e, 0x1f612, 0x1e414, 0x1ec34, 0x1e412, 0x1ec32, 0x1c834, 0x1d874, 0x1c832, 0x1d872, 0x19074, 0x1b0f4, 0x19072, 0x1b0f2, 0x120f4, 0x161f4, 0x120f2, 0x161f2, 0x1f60a, 0x1e40a, 0x1ec1a, 0x1c81a, 0x1d83a, 0x1903a, 0x1b07a, 0x1e2a0, 0x1f158, 0x1f8ae, 0x1e290, 0x1f14c, 0x1e288, 0x1f146, 0x1e284, 0x1e282, 0x1c5a0, 0x1e2d8, 0x1f16e, 0x1c590, 0x1e2cc, 0x1c588, 0x1e2c6, 0x1c584, 0x1c582, 0x18ba0, 0x1c5d8, 0x1e2ee, 0x18b90, 0x1c5cc, 0x18b88, 0x1c5c6, 0x18b84, 0x18b82, 0x117a0, 0x18bd8, 0x1c5ee, 0x11790, 0x18bcc, 0x11788, 0x18bc6, 0x11784, 0x11782, 0x117d8, 0x18bee, 0x117cc, 0x117c6, 0x117ee, 0x1f350, 0x1f9ac, 0x135f8, 0x1f348, 0x1f9a6, 0x134fc, 0x1f344, 0x1347e, 0x1f342, 0x1e250, 0x1f12c, 0x1e6d0, 0x1e248, 0x1f126, 0x1e6c8, 0x1f366, 0x1e6c4, 0x1e242, 0x1e6c2, 0x1c4d0, 0x1e26c, 0x1cdd0, 0x1c4c8, 0x1e266, 0x1cdc8, 0x1e6e6, 0x1cdc4, 0x1c4c2, 0x1cdc2, 0x189d0, 0x1c4ec, 0x19bd0, 0x189c8, 0x1c4e6, 0x19bc8, 0x1cde6, 0x19bc4, 0x189c2, 0x19bc2, 0x113d0, 0x189ec, 0x137d0, 0x113c8, 0x189e6, 0x137c8, 0x19be6, 0x137c4, 0x113c2, 0x137c2, 0x113ec, 0x137ec, 0x113e6, 0x137e6, 0x1fba8, 0x175f0, 0x1bafc, 0x1fba4, 0x174f8, 0x1ba7e, 0x1fba2, 0x1747c, 0x1743e, 0x1f328, 0x1f996, 0x132fc, 0x1f768, 0x1fbb6, 0x176fc, 0x1327e, 0x1f764, 0x1f322, 0x1767e, 0x1f762, 0x1e228, 0x1f116, 0x1e668, 0x1e224, 0x1eee8, 0x1f776, 0x1e222, 0x1eee4, 0x1e662, 0x1eee2, 0x1c468, 0x1e236, 0x1cce8, 0x1c464, 0x1dde8, 0x1cce4, 0x1c462, 0x1dde4, 0x1cce2, 0x1dde2, 0x188e8, 0x1c476, 0x199e8, 0x188e4, 0x1bbe8, 0x199e4, 0x188e2, 0x1bbe4, 0x199e2, 0x1bbe2, 0x111e8, 0x188f6, 0x133e8, 0x111e4, 0x177e8, 0x133e4, 0x111e2, 0x177e4, 0x133e2, 0x177e2, 0x111f6, 0x133f6, 0x1fb94, 0x172f8, 0x1b97e, 0x1fb92, 0x1727c, 0x1723e, 0x1f314, 0x1317e, 0x1f734, 0x1f312, 0x1737e, 0x1f732, 0x1e214, 0x1e634, 0x1e212, 0x1ee74, 0x1e632, 0x1ee72, 0x1c434, 0x1cc74, 0x1c432, 0x1dcf4, 0x1cc72, 0x1dcf2, 0x18874, 0x198f4, 0x18872, 0x1b9f4, 0x198f2, 0x1b9f2, 0x110f4, 0x131f4, 0x110f2, 0x173f4, 0x131f2, 0x173f2, 0x1fb8a, 0x1717c, 0x1713e, 0x1f30a, 0x1f71a, 0x1e20a, 0x1e61a, 0x1ee3a, 0x1c41a, 0x1cc3a, 0x1dc7a, 0x1883a, 0x1987a, 0x1b8fa, 0x1107a, 0x130fa, 0x171fa, 0x170be, 0x1e150, 0x1f0ac, 0x1e148, 0x1f0a6, 0x1e144, 0x1e142, 0x1c2d0, 0x1e16c, 0x1c2c8, 0x1e166, 0x1c2c4, 0x1c2c2, 0x185d0, 0x1c2ec, 0x185c8, 0x1c2e6, 0x185c4, 0x185c2, 0x10bd0, 0x185ec, 0x10bc8, 0x185e6, 0x10bc4, 0x10bc2, 0x10bec, 0x10be6, 0x1f1a8, 0x1f8d6, 0x11afc, 0x1f1a4, 0x11a7e, 0x1f1a2, 0x1e128, 0x1f096, 0x1e368, 0x1e124, 0x1e364, 0x1e122, 0x1e362, 0x1c268, 0x1e136, 0x1c6e8, 0x1c264, 0x1c6e4, 0x1c262, 0x1c6e2, 0x184e8, 0x1c276, 0x18de8, 0x184e4, 0x18de4, 0x184e2, 0x18de2, 0x109e8, 0x184f6, 0x11be8, 0x109e4, 0x11be4, 0x109e2, 0x11be2, 0x109f6, 0x11bf6, 0x1f9d4, 0x13af8, 0x19d7e, 0x1f9d2, 0x13a7c, 0x13a3e, 0x1f194, 0x1197e, 0x1f3b4, 0x1f192, 0x13b7e, 0x1f3b2, 0x1e114, 0x1e334, 0x1e112, 0x1e774, 0x1e332, 0x1e772, 0x1c234, 0x1c674, 0x1c232, 0x1cef4, 0x1c672, 0x1cef2, 0x18474, 0x18cf4, 0x18472, 0x19df4, 0x18cf2, 0x19df2, 0x108f4, 0x119f4, 0x108f2, 0x13bf4, 0x119f2, 0x13bf2, 0x17af0, 0x1bd7c, 0x17a78, 0x1bd3e, 0x17a3c, 0x17a1e, 0x1f9ca, 0x1397c, 0x1fbda, 0x17b7c, 0x1393e, 0x17b3e, 0x1f18a, 0x1f39a, 0x1f7ba, 0x1e10a, 0x1e31a, 0x1e73a, 0x1ef7a, 0x1c21a, 0x1c63a, 0x1ce7a, 0x1defa, 0x1843a, 0x18c7a, 0x19cfa, 0x1bdfa, 0x1087a, 0x118fa, 0x139fa, 0x17978, 0x1bcbe, 0x1793c, 0x1791e, 0x138be, 0x179be, 0x178bc, 0x1789e, 0x1785e, 0x1e0a8, 0x1e0a4, 0x1e0a2, 0x1c168, 0x1e0b6, 0x1c164, 0x1c162, 0x182e8, 0x1c176, 0x182e4, 0x182e2, 0x105e8, 0x182f6, 0x105e4, 0x105e2, 0x105f6, 0x1f0d4, 0x10d7e, 0x1f0d2, 0x1e094, 0x1e1b4, 0x1e092, 0x1e1b2, 0x1c134, 0x1c374, 0x1c132, 0x1c372, 0x18274, 0x186f4, 0x18272, 0x186f2, 0x104f4, 0x10df4, 0x104f2, 0x10df2, 0x1f8ea, 0x11d7c, 0x11d3e, 0x1f0ca, 0x1f1da, 0x1e08a, 0x1e19a, 0x1e3ba, 0x1c11a, 0x1c33a, 0x1c77a, 0x1823a, 0x1867a, 0x18efa, 0x1047a, 0x10cfa, 0x11dfa, 0x13d78, 0x19ebe, 0x13d3c, 0x13d1e, 0x11cbe, 0x13dbe, 0x17d70, 0x1bebc, 0x17d38, 0x1be9e, 0x17d1c, 0x17d0e, 0x13cbc, 0x17dbc, 0x13c9e, 0x17d9e, 0x17cb8, 0x1be5e, 0x17c9c, 0x17c8e, 0x13c5e, 0x17cde, 0x17c5c, 0x17c4e, 0x17c2e, 0x1c0b4, 0x1c0b2, 0x18174, 0x18172, 0x102f4, 0x102f2, 0x1e0da, 0x1c09a, 0x1c1ba, 0x1813a, 0x1837a, 0x1027a, 0x106fa, 0x10ebe, 0x11ebc, 0x11e9e, 0x13eb8, 0x19f5e, 0x13e9c, 0x13e8e, 0x11e5e, 0x13ede, 0x17eb0, 0x1bf5c, 0x17e98, 0x1bf4e, 0x17e8c, 0x17e86, 0x13e5c, 0x17edc, 0x13e4e, 0x17ece, 0x17e58, 0x1bf2e, 0x17e4c, 0x17e46, 0x13e2e, 0x17e6e, 0x17e2c, 0x17e26, 0x10f5e, 0x11f5c, 0x11f4e, 0x13f58, 0x19fae, 0x13f4c, 0x13f46, 0x11f2e, 0x13f6e, 0x13f2c, 0x13f26], # noqa 6 | [0x1abe0, 0x1d5f8, 0x153c0, 0x1a9f0, 0x1d4fc, 0x151e0, 0x1a8f8, 0x1d47e, 0x150f0, 0x1a87c, 0x15078, 0x1fad0, 0x15be0, 0x1adf8, 0x1fac8, 0x159f0, 0x1acfc, 0x1fac4, 0x158f8, 0x1ac7e, 0x1fac2, 0x1587c, 0x1f5d0, 0x1faec, 0x15df8, 0x1f5c8, 0x1fae6, 0x15cfc, 0x1f5c4, 0x15c7e, 0x1f5c2, 0x1ebd0, 0x1f5ec, 0x1ebc8, 0x1f5e6, 0x1ebc4, 0x1ebc2, 0x1d7d0, 0x1ebec, 0x1d7c8, 0x1ebe6, 0x1d7c4, 0x1d7c2, 0x1afd0, 0x1d7ec, 0x1afc8, 0x1d7e6, 0x1afc4, 0x14bc0, 0x1a5f0, 0x1d2fc, 0x149e0, 0x1a4f8, 0x1d27e, 0x148f0, 0x1a47c, 0x14878, 0x1a43e, 0x1483c, 0x1fa68, 0x14df0, 0x1a6fc, 0x1fa64, 0x14cf8, 0x1a67e, 0x1fa62, 0x14c7c, 0x14c3e, 0x1f4e8, 0x1fa76, 0x14efc, 0x1f4e4, 0x14e7e, 0x1f4e2, 0x1e9e8, 0x1f4f6, 0x1e9e4, 0x1e9e2, 0x1d3e8, 0x1e9f6, 0x1d3e4, 0x1d3e2, 0x1a7e8, 0x1d3f6, 0x1a7e4, 0x1a7e2, 0x145e0, 0x1a2f8, 0x1d17e, 0x144f0, 0x1a27c, 0x14478, 0x1a23e, 0x1443c, 0x1441e, 0x1fa34, 0x146f8, 0x1a37e, 0x1fa32, 0x1467c, 0x1463e, 0x1f474, 0x1477e, 0x1f472, 0x1e8f4, 0x1e8f2, 0x1d1f4, 0x1d1f2, 0x1a3f4, 0x1a3f2, 0x142f0, 0x1a17c, 0x14278, 0x1a13e, 0x1423c, 0x1421e, 0x1fa1a, 0x1437c, 0x1433e, 0x1f43a, 0x1e87a, 0x1d0fa, 0x14178, 0x1a0be, 0x1413c, 0x1411e, 0x141be, 0x140bc, 0x1409e, 0x12bc0, 0x195f0, 0x1cafc, 0x129e0, 0x194f8, 0x1ca7e, 0x128f0, 0x1947c, 0x12878, 0x1943e, 0x1283c, 0x1f968, 0x12df0, 0x196fc, 0x1f964, 0x12cf8, 0x1967e, 0x1f962, 0x12c7c, 0x12c3e, 0x1f2e8, 0x1f976, 0x12efc, 0x1f2e4, 0x12e7e, 0x1f2e2, 0x1e5e8, 0x1f2f6, 0x1e5e4, 0x1e5e2, 0x1cbe8, 0x1e5f6, 0x1cbe4, 0x1cbe2, 0x197e8, 0x1cbf6, 0x197e4, 0x197e2, 0x1b5e0, 0x1daf8, 0x1ed7e, 0x169c0, 0x1b4f0, 0x1da7c, 0x168e0, 0x1b478, 0x1da3e, 0x16870, 0x1b43c, 0x16838, 0x1b41e, 0x1681c, 0x125e0, 0x192f8, 0x1c97e, 0x16de0, 0x124f0, 0x1927c, 0x16cf0, 0x1b67c, 0x1923e, 0x16c78, 0x1243c, 0x16c3c, 0x1241e, 0x16c1e, 0x1f934, 0x126f8, 0x1937e, 0x1fb74, 0x1f932, 0x16ef8, 0x1267c, 0x1fb72, 0x16e7c, 0x1263e, 0x16e3e, 0x1f274, 0x1277e, 0x1f6f4, 0x1f272, 0x16f7e, 0x1f6f2, 0x1e4f4, 0x1edf4, 0x1e4f2, 0x1edf2, 0x1c9f4, 0x1dbf4, 0x1c9f2, 0x1dbf2, 0x193f4, 0x193f2, 0x165c0, 0x1b2f0, 0x1d97c, 0x164e0, 0x1b278, 0x1d93e, 0x16470, 0x1b23c, 0x16438, 0x1b21e, 0x1641c, 0x1640e, 0x122f0, 0x1917c, 0x166f0, 0x12278, 0x1913e, 0x16678, 0x1b33e, 0x1663c, 0x1221e, 0x1661e, 0x1f91a, 0x1237c, 0x1fb3a, 0x1677c, 0x1233e, 0x1673e, 0x1f23a, 0x1f67a, 0x1e47a, 0x1ecfa, 0x1c8fa, 0x1d9fa, 0x191fa, 0x162e0, 0x1b178, 0x1d8be, 0x16270, 0x1b13c, 0x16238, 0x1b11e, 0x1621c, 0x1620e, 0x12178, 0x190be, 0x16378, 0x1213c, 0x1633c, 0x1211e, 0x1631e, 0x121be, 0x163be, 0x16170, 0x1b0bc, 0x16138, 0x1b09e, 0x1611c, 0x1610e, 0x120bc, 0x161bc, 0x1209e, 0x1619e, 0x160b8, 0x1b05e, 0x1609c, 0x1608e, 0x1205e, 0x160de, 0x1605c, 0x1604e, 0x115e0, 0x18af8, 0x1c57e, 0x114f0, 0x18a7c, 0x11478, 0x18a3e, 0x1143c, 0x1141e, 0x1f8b4, 0x116f8, 0x18b7e, 0x1f8b2, 0x1167c, 0x1163e, 0x1f174, 0x1177e, 0x1f172, 0x1e2f4, 0x1e2f2, 0x1c5f4, 0x1c5f2, 0x18bf4, 0x18bf2, 0x135c0, 0x19af0, 0x1cd7c, 0x134e0, 0x19a78, 0x1cd3e, 0x13470, 0x19a3c, 0x13438, 0x19a1e, 0x1341c, 0x1340e, 0x112f0, 0x1897c, 0x136f0, 0x11278, 0x1893e, 0x13678, 0x19b3e, 0x1363c, 0x1121e, 0x1361e, 0x1f89a, 0x1137c, 0x1f9ba, 0x1377c, 0x1133e, 0x1373e, 0x1f13a, 0x1f37a, 0x1e27a, 0x1e6fa, 0x1c4fa, 0x1cdfa, 0x189fa, 0x1bae0, 0x1dd78, 0x1eebe, 0x174c0, 0x1ba70, 0x1dd3c, 0x17460, 0x1ba38, 0x1dd1e, 0x17430, 0x1ba1c, 0x17418, 0x1ba0e, 0x1740c, 0x132e0, 0x19978, 0x1ccbe, 0x176e0, 0x13270, 0x1993c, 0x17670, 0x1bb3c, 0x1991e, 0x17638, 0x1321c, 0x1761c, 0x1320e, 0x1760e, 0x11178, 0x188be, 0x13378, 0x1113c, 0x17778, 0x1333c, 0x1111e, 0x1773c, 0x1331e, 0x1771e, 0x111be, 0x133be, 0x177be, 0x172c0, 0x1b970, 0x1dcbc, 0x17260, 0x1b938, 0x1dc9e, 0x17230, 0x1b91c, 0x17218, 0x1b90e, 0x1720c, 0x17206, 0x13170, 0x198bc, 0x17370, 0x13138, 0x1989e, 0x17338, 0x1b99e, 0x1731c, 0x1310e, 0x1730e, 0x110bc, 0x131bc, 0x1109e, 0x173bc, 0x1319e, 0x1739e, 0x17160, 0x1b8b8, 0x1dc5e, 0x17130, 0x1b89c, 0x17118, 0x1b88e, 0x1710c, 0x17106, 0x130b8, 0x1985e, 0x171b8, 0x1309c, 0x1719c, 0x1308e, 0x1718e, 0x1105e, 0x130de, 0x171de, 0x170b0, 0x1b85c, 0x17098, 0x1b84e, 0x1708c, 0x17086, 0x1305c, 0x170dc, 0x1304e, 0x170ce, 0x17058, 0x1b82e, 0x1704c, 0x17046, 0x1302e, 0x1706e, 0x1702c, 0x17026, 0x10af0, 0x1857c, 0x10a78, 0x1853e, 0x10a3c, 0x10a1e, 0x10b7c, 0x10b3e, 0x1f0ba, 0x1e17a, 0x1c2fa, 0x185fa, 0x11ae0, 0x18d78, 0x1c6be, 0x11a70, 0x18d3c, 0x11a38, 0x18d1e, 0x11a1c, 0x11a0e, 0x10978, 0x184be, 0x11b78, 0x1093c, 0x11b3c, 0x1091e, 0x11b1e, 0x109be, 0x11bbe, 0x13ac0, 0x19d70, 0x1cebc, 0x13a60, 0x19d38, 0x1ce9e, 0x13a30, 0x19d1c, 0x13a18, 0x19d0e, 0x13a0c, 0x13a06, 0x11970, 0x18cbc, 0x13b70, 0x11938, 0x18c9e, 0x13b38, 0x1191c, 0x13b1c, 0x1190e, 0x13b0e, 0x108bc, 0x119bc, 0x1089e, 0x13bbc, 0x1199e, 0x13b9e, 0x1bd60, 0x1deb8, 0x1ef5e, 0x17a40, 0x1bd30, 0x1de9c, 0x17a20, 0x1bd18, 0x1de8e, 0x17a10, 0x1bd0c, 0x17a08, 0x1bd06, 0x17a04, 0x13960, 0x19cb8, 0x1ce5e, 0x17b60, 0x13930, 0x19c9c, 0x17b30, 0x1bd9c, 0x19c8e, 0x17b18, 0x1390c, 0x17b0c, 0x13906, 0x17b06, 0x118b8, 0x18c5e, 0x139b8, 0x1189c, 0x17bb8, 0x1399c, 0x1188e, 0x17b9c, 0x1398e, 0x17b8e, 0x1085e, 0x118de, 0x139de, 0x17bde, 0x17940, 0x1bcb0, 0x1de5c, 0x17920, 0x1bc98, 0x1de4e, 0x17910, 0x1bc8c, 0x17908, 0x1bc86, 0x17904, 0x17902, 0x138b0, 0x19c5c, 0x179b0, 0x13898, 0x19c4e, 0x17998, 0x1bcce, 0x1798c, 0x13886, 0x17986, 0x1185c, 0x138dc, 0x1184e, 0x179dc, 0x138ce, 0x179ce, 0x178a0, 0x1bc58, 0x1de2e, 0x17890, 0x1bc4c, 0x17888, 0x1bc46, 0x17884, 0x17882, 0x13858, 0x19c2e, 0x178d8, 0x1384c, 0x178cc, 0x13846, 0x178c6, 0x1182e, 0x1386e, 0x178ee, 0x17850, 0x1bc2c, 0x17848, 0x1bc26, 0x17844, 0x17842, 0x1382c, 0x1786c, 0x13826, 0x17866, 0x17828, 0x1bc16, 0x17824, 0x17822, 0x13816, 0x17836, 0x10578, 0x182be, 0x1053c, 0x1051e, 0x105be, 0x10d70, 0x186bc, 0x10d38, 0x1869e, 0x10d1c, 0x10d0e, 0x104bc, 0x10dbc, 0x1049e, 0x10d9e, 0x11d60, 0x18eb8, 0x1c75e, 0x11d30, 0x18e9c, 0x11d18, 0x18e8e, 0x11d0c, 0x11d06, 0x10cb8, 0x1865e, 0x11db8, 0x10c9c, 0x11d9c, 0x10c8e, 0x11d8e, 0x1045e, 0x10cde, 0x11dde, 0x13d40, 0x19eb0, 0x1cf5c, 0x13d20, 0x19e98, 0x1cf4e, 0x13d10, 0x19e8c, 0x13d08, 0x19e86, 0x13d04, 0x13d02, 0x11cb0, 0x18e5c, 0x13db0, 0x11c98, 0x18e4e, 0x13d98, 0x19ece, 0x13d8c, 0x11c86, 0x13d86, 0x10c5c, 0x11cdc, 0x10c4e, 0x13ddc, 0x11cce, 0x13dce, 0x1bea0, 0x1df58, 0x1efae, 0x1be90, 0x1df4c, 0x1be88, 0x1df46, 0x1be84, 0x1be82, 0x13ca0, 0x19e58, 0x1cf2e, 0x17da0, 0x13c90, 0x19e4c, 0x17d90, 0x1becc, 0x19e46, 0x17d88, 0x13c84, 0x17d84, 0x13c82, 0x17d82, 0x11c58, 0x18e2e, 0x13cd8, 0x11c4c, 0x17dd8, 0x13ccc, 0x11c46, 0x17dcc, 0x13cc6, 0x17dc6, 0x10c2e, 0x11c6e, 0x13cee, 0x17dee, 0x1be50, 0x1df2c, 0x1be48, 0x1df26, 0x1be44, 0x1be42, 0x13c50, 0x19e2c, 0x17cd0, 0x13c48, 0x19e26, 0x17cc8, 0x1be66, 0x17cc4, 0x13c42, 0x17cc2, 0x11c2c, 0x13c6c, 0x11c26, 0x17cec, 0x13c66, 0x17ce6, 0x1be28, 0x1df16, 0x1be24, 0x1be22, 0x13c28, 0x19e16, 0x17c68, 0x13c24, 0x17c64, 0x13c22, 0x17c62, 0x11c16, 0x13c36, 0x17c76, 0x1be14, 0x1be12, 0x13c14, 0x17c34, 0x13c12, 0x17c32, 0x102bc, 0x1029e, 0x106b8, 0x1835e, 0x1069c, 0x1068e, 0x1025e, 0x106de, 0x10eb0, 0x1875c, 0x10e98, 0x1874e, 0x10e8c, 0x10e86, 0x1065c, 0x10edc, 0x1064e, 0x10ece, 0x11ea0, 0x18f58, 0x1c7ae, 0x11e90, 0x18f4c, 0x11e88, 0x18f46, 0x11e84, 0x11e82, 0x10e58, 0x1872e, 0x11ed8, 0x18f6e, 0x11ecc, 0x10e46, 0x11ec6, 0x1062e, 0x10e6e, 0x11eee, 0x19f50, 0x1cfac, 0x19f48, 0x1cfa6, 0x19f44, 0x19f42, 0x11e50, 0x18f2c, 0x13ed0, 0x19f6c, 0x18f26, 0x13ec8, 0x11e44, 0x13ec4, 0x11e42, 0x13ec2, 0x10e2c, 0x11e6c, 0x10e26, 0x13eec, 0x11e66, 0x13ee6, 0x1dfa8, 0x1efd6, 0x1dfa4, 0x1dfa2, 0x19f28, 0x1cf96, 0x1bf68, 0x19f24, 0x1bf64, 0x19f22, 0x1bf62, 0x11e28, 0x18f16, 0x13e68, 0x11e24, 0x17ee8, 0x13e64, 0x11e22, 0x17ee4, 0x13e62, 0x17ee2, 0x10e16, 0x11e36, 0x13e76, 0x17ef6, 0x1df94, 0x1df92, 0x19f14, 0x1bf34, 0x19f12, 0x1bf32, 0x11e14, 0x13e34, 0x11e12, 0x17e74, 0x13e32, 0x17e72, 0x1df8a, 0x19f0a, 0x1bf1a, 0x11e0a, 0x13e1a, 0x17e3a, 0x1035c, 0x1034e, 0x10758, 0x183ae, 0x1074c, 0x10746, 0x1032e, 0x1076e, 0x10f50, 0x187ac, 0x10f48, 0x187a6, 0x10f44, 0x10f42, 0x1072c, 0x10f6c, 0x10726, 0x10f66, 0x18fa8, 0x1c7d6, 0x18fa4, 0x18fa2, 0x10f28, 0x18796, 0x11f68, 0x18fb6, 0x11f64, 0x10f22, 0x11f62, 0x10716, 0x10f36, 0x11f76, 0x1cfd4, 0x1cfd2, 0x18f94, 0x19fb4, 0x18f92, 0x19fb2, 0x10f14, 0x11f34, 0x10f12, 0x13f74, 0x11f32, 0x13f72, 0x1cfca, 0x18f8a, 0x19f9a, 0x10f0a, 0x11f1a, 0x13f3a, 0x103ac, 0x103a6, 0x107a8, 0x183d6, 0x107a4, 0x107a2, 0x10396, 0x107b6, 0x187d4, 0x187d2, 0x10794, 0x10fb4, 0x10792, 0x10fb2, 0x1c7ea], # noqa 7 | ] 8 | 9 | 10 | def map_code_word(table: int, word: int): 11 | """Maps high-level code words to low level code words. 12 | """ 13 | assert 0 <= table <= 2, "table must be between 0 and 2, given: %r" % table 14 | assert 0 <= word <= 929, "word must be between 0 and 929, given: %r" % word 15 | 16 | return CODES[table][word] 17 | -------------------------------------------------------------------------------- /pdf417gen/compaction/__init__.py: -------------------------------------------------------------------------------- 1 | from itertools import chain, groupby 2 | from typing import Generator, Iterable, List 3 | 4 | from pdf417gen.compaction import optimizations 5 | from pdf417gen.compaction.byte import compact_bytes 6 | from pdf417gen.compaction.numeric import compact_numbers 7 | from pdf417gen.compaction.text import compact_text 8 | from pdf417gen.data import CHARACTERS_LOOKUP 9 | from pdf417gen.types import Codeword, Chunk, CompactionFn 10 | 11 | 12 | # Codes for switching between compacting modes 13 | TEXT_LATCH = 900 14 | BYTE_LATCH = 901 15 | BYTE_LATCH_ALT = 924 16 | BYTE_SWITCH = 913 17 | NUMERIC_LATCH = 902 18 | 19 | 20 | def compact(data: bytes, force_binary: bool = False) -> Iterable[Codeword]: 21 | """ 22 | Encodes given data into an array of PDF417 code words. 23 | 24 | Args: 25 | data: The data bytes to encode 26 | force_binary: If True, forces byte compaction mode for all data, 27 | bypassing optimizations (useful for pre-compressed data) 28 | """ 29 | if force_binary: 30 | # Skip optimizations and directly use byte compaction 31 | return _compact_chunks([Chunk(data, compact_bytes)]) 32 | 33 | # Normal path with optimizations 34 | chunks = _split_to_chunks(data) 35 | chunks = optimizations.replace_short_numeric_chunks(chunks) 36 | chunks = optimizations.merge_chunks_with_same_compact_fn(chunks) 37 | return _compact_chunks(chunks) 38 | 39 | 40 | def _compact_chunks(chunks: Iterable[Chunk]) -> Iterable[Codeword]: 41 | compacted_chunks = ( 42 | _compact_chunk(ordinal, chunk) for ordinal, chunk in enumerate(chunks)) 43 | 44 | return chain(*compacted_chunks) 45 | 46 | 47 | def _compact_chunk(ordinal: int, chunk: Chunk): 48 | code_words: List[Codeword] = [] 49 | 50 | # Add the switch code if required 51 | add_switch_code = ordinal > 0 or chunk.compact_fn != compact_text 52 | if add_switch_code: 53 | code_words.append(get_switch_code(chunk)) 54 | 55 | code_words.extend(chunk.compact_fn(chunk.data)) 56 | 57 | return code_words 58 | 59 | 60 | def _split_to_chunks(data: bytes) -> Generator[Chunk, None, None]: 61 | """ 62 | Splits a string into chunks which can be compacted with the same compacting 63 | function. 64 | """ 65 | for fn, chunk in groupby(data, key=get_optimal_compactor_fn): 66 | yield Chunk(bytes(chunk), fn) 67 | 68 | 69 | def get_optimal_compactor_fn(char: int) -> CompactionFn: 70 | if 48 <= char <= 57: 71 | return compact_numbers 72 | 73 | if char in CHARACTERS_LOOKUP: 74 | return compact_text 75 | 76 | return compact_bytes 77 | 78 | 79 | def get_switch_code(chunk: Chunk): 80 | if chunk.compact_fn == compact_text: 81 | return TEXT_LATCH 82 | 83 | if chunk.compact_fn == compact_bytes: 84 | return BYTE_LATCH_ALT if len(chunk.data) % 6 == 0 else BYTE_LATCH 85 | 86 | if chunk.compact_fn == compact_numbers: 87 | return NUMERIC_LATCH 88 | 89 | assert False, "Nonexistant compaction function" 90 | -------------------------------------------------------------------------------- /pdf417gen/compaction/byte.py: -------------------------------------------------------------------------------- 1 | """ 2 | Byte Compaction Mode (BC) 3 | 4 | Can encode: ASCII 0 to 255 5 | Rate compaction: 1.2 byte per code word 6 | """ 7 | 8 | from itertools import chain 9 | from typing import Iterable, List, Tuple 10 | from pdf417gen.types import Codeword 11 | from pdf417gen.util import switch_base, chunks 12 | 13 | 14 | def compact_bytes(data: bytes) -> Iterable[Codeword]: 15 | """Encodes data into code words using the Byte compaction mode.""" 16 | compacted_chunks = (_compact_chunk(chunk) for chunk in chunks(data, size=6)) 17 | return chain(*compacted_chunks) 18 | 19 | 20 | def _compact_chunk(chunk: Tuple[int, ...]) -> List[Codeword]: 21 | """ 22 | Chunks of exactly 6 bytes are encoded into 5 codewords by using a base 256 23 | to base 900 transformation. Smaller chunks are left unchanged. 24 | """ 25 | digits = [i for i in chunk] 26 | 27 | if len(chunk) == 6: 28 | base900 = switch_base(digits, 256, 900) 29 | return [0] * (5 - len(base900)) + base900 30 | 31 | return digits 32 | -------------------------------------------------------------------------------- /pdf417gen/compaction/numeric.py: -------------------------------------------------------------------------------- 1 | """ 2 | Numeric Compaction Mode (NC) 3 | 4 | Can encode: Digits 0-9, ASCII 5 | Rate compaction: 2.9 bytes per code word 6 | """ 7 | 8 | from itertools import chain 9 | from typing import Iterable, List, Tuple 10 | from pdf417gen.types import Codeword 11 | from pdf417gen.util import to_base, chunks 12 | 13 | 14 | def _compact_chunk(chunk: Tuple[int, ...]) -> List[Codeword]: 15 | number = "".join(chr(x) for x in chunk) 16 | value = int("1" + number) 17 | return to_base(value, 900) 18 | 19 | 20 | def compact_numbers(data: bytes) -> Iterable[Codeword]: 21 | """Encodes data into code words using the Numeric compaction mode.""" 22 | compacted_chunks = (_compact_chunk(chunk) for chunk in chunks(data, size=44)) 23 | return chain(*compacted_chunks) 24 | -------------------------------------------------------------------------------- /pdf417gen/compaction/optimizations.py: -------------------------------------------------------------------------------- 1 | from itertools import chain, groupby 2 | from typing import Generator, Iterable 3 | from pdf417gen.compaction.numeric import compact_numbers 4 | from pdf417gen.compaction.text import compact_text 5 | from pdf417gen.types import Chunk 6 | from pdf417gen.util import iterate_prev_next 7 | 8 | 9 | def replace_short_numeric_chunks(chunks: Iterable[Chunk]) -> Generator[Chunk, None, None]: 10 | """ 11 | The Numeric Compaction mode can pack almost 3 digits (2.93) into a symbol 12 | character. Though Numeric Compaction mode can be invoked at any digit 13 | length, it is recommended to use Numeric Compaction mode when there are 14 | more than 13 consecutive digits. Otherwise, use Text Compaction mode. 15 | """ 16 | for prev, chunk, next in iterate_prev_next(chunks): 17 | is_short_numeric_chunk = ( 18 | chunk.compact_fn == compact_numbers 19 | and len(chunk.data) < 13 20 | ) 21 | 22 | borders_text_chunk = ( 23 | (prev and prev.compact_fn == compact_text) or 24 | (next and next.compact_fn == compact_text) 25 | ) 26 | 27 | if is_short_numeric_chunk and borders_text_chunk: 28 | yield Chunk(chunk.data, compact_text) 29 | else: 30 | yield chunk 31 | 32 | 33 | def merge_chunks_with_same_compact_fn(chunks: Iterable[Chunk]) -> Generator[Chunk, None, None]: 34 | for compact_fn, group in groupby(chunks, key=lambda x: x[1]): 35 | data = chain.from_iterable(chunk.data for chunk in group) 36 | yield Chunk(bytes(data), compact_fn) 37 | -------------------------------------------------------------------------------- /pdf417gen/compaction/text.py: -------------------------------------------------------------------------------- 1 | """ 2 | Text Compaction Mode (TC) 3 | 4 | Can encode: ASCII 9, 10, 13 and 32-126 5 | Rate compaction: 2 bytes per code word 6 | """ 7 | 8 | from typing import Iterable, Tuple 9 | from pdf417gen.data import CHARACTERS_LOOKUP, SWITCH_CODES 10 | from pdf417gen.types import Codeword, Submode 11 | from pdf417gen.util import chunks 12 | 13 | 14 | def _exists_in_submode(char: int, submode: Submode) -> bool: 15 | return char in CHARACTERS_LOOKUP and submode in CHARACTERS_LOOKUP[char] 16 | 17 | 18 | def _get_submode(char: int) -> Submode: 19 | if char not in CHARACTERS_LOOKUP: 20 | raise ValueError("Cannot encode char: {}".format(char)) 21 | 22 | submodes = CHARACTERS_LOOKUP[char].keys() 23 | 24 | preference = [Submode.LOWER, Submode.UPPER, Submode.MIXED, Submode.PUNCT] 25 | 26 | for submode in preference: 27 | if submode in submodes: 28 | return submode 29 | 30 | raise ValueError("Cannot encode char: {}".format(char)) 31 | 32 | 33 | def compact_text_interim(data: bytes): 34 | """Encodes text data to interim code words.""" 35 | 36 | def _interim_text_generator(chars: bytes): 37 | # By default, encoding starts in uppercase submode 38 | submode = Submode.UPPER 39 | 40 | for char in chars: 41 | # Switch submode if needed 42 | if not _exists_in_submode(char, submode): 43 | prev_submode = submode 44 | submode = _get_submode(char) 45 | for code in SWITCH_CODES[prev_submode][submode]: 46 | yield code 47 | 48 | yield CHARACTERS_LOOKUP[char][submode] 49 | 50 | return _interim_text_generator(data) 51 | 52 | 53 | # Since each code word consists of 2 characters, a padding value is 54 | # needed when encoding a single character. 29 is used as padding because 55 | # it's a switch in all 4 submodes, and doesn't add any data. 56 | PADDING_INTERIM_CODE = 29 57 | 58 | 59 | def _compact_chunk(chunk: Tuple[int, ...]) -> Codeword: 60 | if len(chunk) == 1: 61 | chunk = (chunk[0], PADDING_INTERIM_CODE) 62 | 63 | return 30 * chunk[0] + chunk[1] 64 | 65 | 66 | def compact_text(data: bytes) -> Iterable[Codeword]: 67 | """Encodes data into code words using the Text compaction mode.""" 68 | interim_codes = compact_text_interim(data) 69 | return (_compact_chunk(chunk) for chunk in chunks(interim_codes, 2)) 70 | -------------------------------------------------------------------------------- /pdf417gen/console.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import zlib 4 | 5 | from argparse import ArgumentParser, RawDescriptionHelpFormatter 6 | from typing import List, Union 7 | from PIL import Image 8 | 9 | from pdf417gen import encode, render_image 10 | 11 | 12 | def print_usage(): 13 | print("Usage: pdf417gen [command]") 14 | print("") 15 | print("Commands:") 16 | print(" help show this help message and exit") 17 | print(" encode generate a bar code from given input") 18 | print("") 19 | print("https://github.com/ihabunek/pdf417gen") 20 | 21 | 22 | def print_err(msg: str): 23 | sys.stderr.write('\033[91m' + msg + '\033[0m' + "\n") 24 | 25 | 26 | def get_parser() -> ArgumentParser: 27 | # Use the formatter that preserves description formatting 28 | parser = ArgumentParser( 29 | usage="%(prog)s encode [options] [text]", 30 | epilog="https://github.com/ihabunek/pdf417gen", 31 | description="Generate a PDF417 barcode from given input", 32 | formatter_class=RawDescriptionHelpFormatter 33 | ) 34 | 35 | parser.add_argument("text", type=str, nargs="?", 36 | help="Text or data to encode. Alternatively data can be piped in.") 37 | 38 | parser.add_argument("-c", "--columns", dest="columns", type=int, 39 | help="The number of columns (default: 6).", 40 | default=6) 41 | 42 | parser.add_argument("-l", "--security-level", dest="security_level", type=int, 43 | help="Security level (default: 2).", 44 | default=2) 45 | 46 | parser.add_argument("-e", "--encoding", dest="encoding", type=str, 47 | help="Character encoding used to decode input (default: utf-8).", 48 | default='utf-8') 49 | 50 | parser.add_argument("-s", "--scale", dest="scale", type=int, 51 | help="Module width in pixels (default: 3).", 52 | default=3) 53 | 54 | parser.add_argument("-r", "--ratio", dest="ratio", type=int, 55 | help="Module height to width ratio (default: 3).", 56 | default=3) 57 | 58 | parser.add_argument("-p", "--padding", dest="padding", type=int, 59 | help="Image padding in pixels (default: 20).", 60 | default=20) 61 | 62 | parser.add_argument("-f", "--foreground-color", dest="fg_color", type=str, 63 | help="Foreground color in hex (default: #000000).", 64 | default="#000000") 65 | 66 | parser.add_argument("-b", "--background-color", dest="bg_color", type=str, 67 | help="Background color in hex (default: #FFFFFF).", 68 | default="#FFFFFF") 69 | 70 | parser.add_argument("-o", "--output", dest="output", type=str, 71 | help="Target file (if not given, will just show the barcode).") 72 | 73 | # Create a group for advanced options 74 | advanced_group = parser.add_argument_group('Advanced Options') 75 | 76 | # Add force binary option 77 | advanced_group.add_argument("--force-binary", dest="force_binary", action="store_true", 78 | help="Force byte compaction mode (useful for pre-compressed data).") 79 | 80 | # Add compression option 81 | advanced_group.add_argument("--compress", dest="compress", action="store_true", 82 | help="Precompress data using zlib before encoding (useful for text data).") 83 | 84 | # Create a group for macro options 85 | macro_group = parser.add_argument_group('Macro PDF417 Options (for large data)') 86 | 87 | # Add macro encoding support 88 | macro_group.add_argument("--macro", dest="use_macro", action="store_true", 89 | help="Use Macro PDF417 for large data.") 90 | 91 | macro_group.add_argument("--segment-size", dest="segment_size", type=int, 92 | help="Maximum size in bytes for each segment (default: 800).", 93 | default=800) 94 | 95 | macro_group.add_argument("--file-name", dest="file_name", type=str, 96 | help="Include file name in Macro PDF417 metadata.") 97 | 98 | return parser 99 | 100 | 101 | def do_encode(raw_args: List[str]): 102 | args = get_parser().parse_args(raw_args) 103 | data: Union[str, bytes] = args.text 104 | 105 | # If no text is given, check stdin 106 | if not data: 107 | data = sys.stdin.buffer.read() 108 | 109 | if not data: 110 | print_err("No input given") 111 | return 112 | 113 | try: 114 | # Apply compression if requested 115 | if args.compress: 116 | if isinstance(data, str): 117 | data = data.encode(args.encoding) 118 | data = zlib.compress(data) 119 | args.force_binary = True # Force binary mode for compressed data 120 | 121 | if args.use_macro: 122 | # Use macro encoding for large data 123 | from pdf417gen import encode_macro 124 | 125 | codes = encode_macro( 126 | data, 127 | columns=args.columns, 128 | security_level=args.security_level, 129 | encoding=args.encoding, 130 | segment_size=args.segment_size, 131 | file_name=args.file_name, 132 | force_binary=args.force_binary, 133 | ) 134 | 135 | # Handle multiple barcodes 136 | images = [] 137 | for i, barcode in enumerate(codes): 138 | image = render_image( 139 | barcode, 140 | scale=args.scale, 141 | ratio=args.ratio, 142 | padding=args.padding, 143 | fg_color=args.fg_color, 144 | bg_color=args.bg_color, 145 | ) 146 | images.append(image) 147 | 148 | if args.output: 149 | # Save multiple images with suffix 150 | base_name, ext = os.path.splitext(args.output) 151 | for i, image in enumerate(images): 152 | output_file = f"{base_name}_{i+1:03d}{ext}" 153 | image.save(output_file) 154 | print(f"Saved {len(images)} barcode images with prefix {base_name}_") 155 | else: 156 | # Show first image if there are too many 157 | if len(images) > 5: 158 | print(f"Generated {len(images)} barcode images. Showing first one.") 159 | images[0].show() 160 | else: 161 | # Concatenate images into one before showing 162 | total_width = max(img.width for img in images) 163 | total_height = sum(img.height for img in images) 164 | combined_image = Image.new('RGB', (total_width, total_height), args.bg_color) 165 | 166 | y_offset = 0 167 | for img in images: 168 | combined_image.paste(img, (0, y_offset)) 169 | y_offset += img.height 170 | 171 | combined_image.show() 172 | else: 173 | # Standard encoding 174 | codes = encode( 175 | data, 176 | columns=args.columns, 177 | security_level=args.security_level, 178 | encoding=args.encoding, 179 | force_binary=args.force_binary 180 | ) 181 | 182 | image = render_image( 183 | codes, 184 | scale=args.scale, 185 | ratio=args.ratio, 186 | padding=args.padding, 187 | fg_color=args.fg_color, 188 | bg_color=args.bg_color, 189 | ) 190 | 191 | if args.output: 192 | image.save(args.output) 193 | else: 194 | image.show() 195 | except Exception as e: 196 | print_err(str(e)) 197 | return 198 | 199 | 200 | def main(): 201 | command = sys.argv[1] if len(sys.argv) > 1 else None 202 | args = sys.argv[2:] 203 | 204 | if command == "encode": 205 | do_encode(args) 206 | else: 207 | print_usage() 208 | -------------------------------------------------------------------------------- /pdf417gen/data.py: -------------------------------------------------------------------------------- 1 | from pdf417gen.types import Submode 2 | 3 | # Shorthand for sub-modes 4 | UPPER = Submode.UPPER 5 | LOWER = Submode.LOWER 6 | MIXED = Submode.MIXED 7 | PUNCT = Submode.PUNCT 8 | 9 | CHARACTERS_LOOKUP = { 10 | 9: {MIXED: 12, PUNCT: 12}, # \t 11 | 10: {PUNCT: 15}, # \n 12 | 13: {MIXED: 11, PUNCT: 11}, # \r 13 | 32: {UPPER: 26, LOWER: 26, MIXED: 26}, # SPACE 14 | 33: {PUNCT: 10}, # ! 15 | 34: {PUNCT: 20}, # " 16 | 35: {MIXED: 15}, # # 17 | 36: {MIXED: 18, PUNCT: 18}, # $ 18 | 37: {MIXED: 21}, # % 19 | 38: {MIXED: 10}, # & 20 | 39: {PUNCT: 28}, # ' 21 | 40: {PUNCT: 23}, # ( 22 | 41: {PUNCT: 24}, # ) 23 | 42: {MIXED: 22, PUNCT: 22}, # * 24 | 43: {MIXED: 20}, # + 25 | 44: {MIXED: 13, PUNCT: 13}, # , 26 | 45: {MIXED: 16, PUNCT: 16}, # - 27 | 46: {MIXED: 17, PUNCT: 17}, # . 28 | 47: {MIXED: 19, PUNCT: 19}, # / 29 | 48: {MIXED: 0}, # 0 30 | 49: {MIXED: 1}, # 1 31 | 50: {MIXED: 2}, # 2 32 | 51: {MIXED: 3}, # 3 33 | 52: {MIXED: 4}, # 4 34 | 53: {MIXED: 5}, # 5 35 | 54: {MIXED: 6}, # 6 36 | 55: {MIXED: 7}, # 7 37 | 56: {MIXED: 8}, # 8 38 | 57: {MIXED: 9}, # 9 39 | 58: {MIXED: 14, PUNCT: 14}, # : 40 | 59: {PUNCT: 0}, # ; 41 | 60: {PUNCT: 1}, # < 42 | 61: {MIXED: 23}, # = 43 | 62: {PUNCT: 2}, # > 44 | 63: {PUNCT: 25}, # ? 45 | 64: {PUNCT: 3}, # @ 46 | 65: {UPPER: 0}, # A 47 | 66: {UPPER: 1}, # B 48 | 67: {UPPER: 2}, # C 49 | 68: {UPPER: 3}, # D 50 | 69: {UPPER: 4}, # E 51 | 70: {UPPER: 5}, # F 52 | 71: {UPPER: 6}, # G 53 | 72: {UPPER: 7}, # H 54 | 73: {UPPER: 8}, # I 55 | 74: {UPPER: 9}, # J 56 | 75: {UPPER: 10}, # K 57 | 76: {UPPER: 11}, # L 58 | 77: {UPPER: 12}, # M 59 | 78: {UPPER: 13}, # N 60 | 79: {UPPER: 14}, # O 61 | 80: {UPPER: 15}, # P 62 | 81: {UPPER: 16}, # Q 63 | 82: {UPPER: 17}, # R 64 | 83: {UPPER: 18}, # S 65 | 84: {UPPER: 19}, # T 66 | 85: {UPPER: 20}, # U 67 | 86: {UPPER: 21}, # V 68 | 87: {UPPER: 22}, # W 69 | 88: {UPPER: 23}, # X 70 | 89: {UPPER: 24}, # Y 71 | 90: {UPPER: 25}, # Z 72 | 91: {PUNCT: 4}, # [ 73 | 92: {PUNCT: 5}, # \ 74 | 93: {PUNCT: 6}, # ] 75 | 94: {MIXED: 24}, # ^ 76 | 95: {PUNCT: 7}, # _ 77 | 96: {PUNCT: 8}, # ` 78 | 97: {LOWER: 0}, # a 79 | 98: {LOWER: 1}, # b 80 | 99: {LOWER: 2}, # c 81 | 100: {LOWER: 3}, # d 82 | 101: {LOWER: 4}, # e 83 | 102: {LOWER: 5}, # f 84 | 103: {LOWER: 6}, # g 85 | 104: {LOWER: 7}, # h 86 | 105: {LOWER: 8}, # i 87 | 106: {LOWER: 9}, # j 88 | 107: {LOWER: 10}, # k 89 | 108: {LOWER: 11}, # l 90 | 109: {LOWER: 12}, # m 91 | 110: {LOWER: 13}, # n 92 | 111: {LOWER: 14}, # o 93 | 112: {LOWER: 15}, # p 94 | 113: {LOWER: 16}, # q 95 | 114: {LOWER: 17}, # r 96 | 115: {LOWER: 18}, # s 97 | 116: {LOWER: 19}, # t 98 | 117: {LOWER: 20}, # u 99 | 118: {LOWER: 21}, # v 100 | 119: {LOWER: 22}, # w 101 | 120: {LOWER: 23}, # x 102 | 121: {LOWER: 24}, # y 103 | 122: {LOWER: 25}, # z 104 | 123: {PUNCT: 26}, # { 105 | 124: {PUNCT: 21}, # | 106 | 125: {PUNCT: 27}, # } 107 | 126: {PUNCT: 9}, # ~ 108 | } 109 | 110 | # Switch codes between submodes 111 | SWITCH_CODE_LOOKUP = { 112 | UPPER: {LOWER: 27, MIXED: 28}, 113 | LOWER: {MIXED: 28}, 114 | MIXED: {PUNCT: 25, LOWER: 27, UPPER: 28}, 115 | PUNCT: {UPPER: 29}, 116 | } 117 | 118 | SINGLE_SWITCH_CODE_LOOKUP = { 119 | UPPER: {PUNCT: 29}, 120 | LOWER: {UPPER: 27, PUNCT: 29}, 121 | MIXED: {PUNCT: 29}, 122 | } 123 | 124 | # How to switch between submodes (can require two switch codes). */ 125 | SWITCH_CODES = { 126 | LOWER: { 127 | UPPER: [SWITCH_CODE_LOOKUP[LOWER][MIXED], 128 | SWITCH_CODE_LOOKUP[MIXED][UPPER]], 129 | MIXED: [SWITCH_CODE_LOOKUP[LOWER][MIXED]], 130 | PUNCT: [SWITCH_CODE_LOOKUP[LOWER][MIXED], 131 | SWITCH_CODE_LOOKUP[MIXED][PUNCT]], 132 | }, 133 | UPPER: { 134 | LOWER: [SWITCH_CODE_LOOKUP[UPPER][LOWER]], 135 | MIXED: [SWITCH_CODE_LOOKUP[UPPER][MIXED]], 136 | PUNCT: [SWITCH_CODE_LOOKUP[UPPER][MIXED], 137 | SWITCH_CODE_LOOKUP[MIXED][PUNCT]], 138 | }, 139 | MIXED: { 140 | LOWER: [SWITCH_CODE_LOOKUP[MIXED][LOWER]], 141 | UPPER: [SWITCH_CODE_LOOKUP[MIXED][UPPER]], 142 | PUNCT: [SWITCH_CODE_LOOKUP[MIXED][PUNCT]], 143 | }, 144 | PUNCT: { 145 | LOWER: [SWITCH_CODE_LOOKUP[PUNCT][UPPER], 146 | SWITCH_CODE_LOOKUP[UPPER][LOWER]], 147 | UPPER: [SWITCH_CODE_LOOKUP[PUNCT][UPPER]], 148 | MIXED: [SWITCH_CODE_LOOKUP[PUNCT][UPPER], 149 | SWITCH_CODE_LOOKUP[UPPER][MIXED]], 150 | }, 151 | } 152 | 153 | # Reed solomon error correction factors, per level 0-8 154 | ERROR_CORRECTION_FACTORS = [ 155 | [27, 917], 156 | [522, 568, 723, 809], 157 | [237, 308, 436, 284, 646, 653, 428, 379], 158 | [274, 562, 232, 755, 599, 524, 801, 132, 295, 116, 442, 428, 295, 42, 176, 65], 159 | [361, 575, 922, 525, 176, 586, 640, 321, 536, 742, 677, 742, 687, 284, 193, 517, 273, 494, 263, 147, 593, 800, 571, 320, 803, 133, 231, 390, 685, 330, 63, 410], 160 | [539, 422, 6, 93, 862, 771, 453, 106, 610, 287, 107, 505, 733, 877, 381, 612, 723, 476, 462, 172, 430, 609, 858, 822, 543, 376, 511, 400, 672, 762, 283, 184, 440, 35, 519, 31, 460, 594, 225, 535, 517, 352, 605, 158, 651, 201, 488, 502, 648, 733, 717, 83, 404, 97, 280, 771, 840, 629, 4, 381, 843, 623, 264, 543], 161 | [521, 310, 864, 547, 858, 580, 296, 379, 53, 779, 897, 444, 400, 925, 749, 415, 822, 93, 217, 208, 928, 244, 583, 620, 246, 148, 447, 631, 292, 908, 490, 704, 516, 258, 457, 907, 594, 723, 674, 292, 272, 96, 684, 432, 686, 606, 860, 569, 193, 219, 129, 186, 236, 287, 192, 775, 278, 173, 40, 379, 712, 463, 646, 776, 171, 491, 297, 763, 156, 732, 95, 270, 447, 90, 507, 48, 228, 821, 808, 898, 784, 663, 627, 378, 382, 262, 380, 602, 754, 336, 89, 614, 87, 432, 670, 616, 157, 374, 242, 726, 600, 269, 375, 898, 845, 454, 354, 130, 814, 587, 804, 34, 211, 330, 539, 297, 827, 865, 37, 517, 834, 315, 550, 86, 801, 4, 108, 539], 162 | [524, 894, 75, 766, 882, 857, 74, 204, 82, 586, 708, 250, 905, 786, 138, 720, 858, 194, 311, 913, 275, 190, 375, 850, 438, 733, 194, 280, 201, 280, 828, 757, 710, 814, 919, 89, 68, 569, 11, 204, 796, 605, 540, 913, 801, 700, 799, 137, 439, 418, 592, 668, 353, 859, 370, 694, 325, 240, 216, 257, 284, 549, 209, 884, 315, 70, 329, 793, 490, 274, 877, 162, 749, 812, 684, 461, 334, 376, 849, 521, 307, 291, 803, 712, 19, 358, 399, 908, 103, 511, 51, 8, 517, 225, 289, 470, 637, 731, 66, 255, 917, 269, 463, 830, 730, 433, 848, 585, 136, 538, 906, 90, 2, 290, 743, 199, 655, 903, 329, 49, 802, 580, 355, 588, 188, 462, 10, 134, 628, 320, 479, 130, 739, 71, 263, 318, 374, 601, 192, 605, 142, 673, 687, 234, 722, 384, 177, 752, 607, 640, 455, 193, 689, 707, 805, 641, 48, 60, 732, 621, 895, 544, 261, 852, 655, 309, 697, 755, 756, 60, 231, 773, 434, 421, 726, 528, 503, 118, 49, 795, 32, 144, 500, 238, 836, 394, 280, 566, 319, 9, 647, 550, 73, 914, 342, 126, 32, 681, 331, 792, 620, 60, 609, 441, 180, 791, 893, 754, 605, 383, 228, 749, 760, 213, 54, 297, 134, 54, 834, 299, 922, 191, 910, 532, 609, 829, 189, 20, 167, 29, 872, 449, 83, 402, 41, 656, 505, 579, 481, 173, 404, 251, 688, 95, 497, 555, 642, 543, 307, 159, 924, 558, 648, 55, 497, 10], 163 | [352, 77, 373, 504, 35, 599, 428, 207, 409, 574, 118, 498, 285, 380, 350, 492, 197, 265, 920, 155, 914, 299, 229, 643, 294, 871, 306, 88, 87, 193, 352, 781, 846, 75, 327, 520, 435, 543, 203, 666, 249, 346, 781, 621, 640, 268, 794, 534, 539, 781, 408, 390, 644, 102, 476, 499, 290, 632, 545, 37, 858, 916, 552, 41, 542, 289, 122, 272, 383, 800, 485, 98, 752, 472, 761, 107, 784, 860, 658, 741, 290, 204, 681, 407, 855, 85, 99, 62, 482, 180, 20, 297, 451, 593, 913, 142, 808, 684, 287, 536, 561, 76, 653, 899, 729, 567, 744, 390, 513, 192, 516, 258, 240, 518, 794, 395, 768, 848, 51, 610, 384, 168, 190, 826, 328, 596, 786, 303, 570, 381, 415, 641, 156, 237, 151, 429, 531, 207, 676, 710, 89, 168, 304, 402, 40, 708, 575, 162, 864, 229, 65, 861, 841, 512, 164, 477, 221, 92, 358, 785, 288, 357, 850, 836, 827, 736, 707, 94, 8, 494, 114, 521, 2, 499, 851, 543, 152, 729, 771, 95, 248, 361, 578, 323, 856, 797, 289, 51, 684, 466, 533, 820, 669, 45, 902, 452, 167, 342, 244, 173, 35, 463, 651, 51, 699, 591, 452, 578, 37, 124, 298, 332, 552, 43, 427, 119, 662, 777, 475, 850, 764, 364, 578, 911, 283, 711, 472, 420, 245, 288, 594, 394, 511, 327, 589, 777, 699, 688, 43, 408, 842, 383, 721, 521, 560, 644, 714, 559, 62, 145, 873, 663, 713, 159, 672, 729, 624, 59, 193, 417, 158, 209, 563, 564, 343, 693, 109, 608, 563, 365, 181, 772, 677, 310, 248, 353, 708, 410, 579, 870, 617, 841, 632, 860, 289, 536, 35, 777, 618, 586, 424, 833, 77, 597, 346, 269, 757, 632, 695, 751, 331, 247, 184, 45, 787, 680, 18, 66, 407, 369, 54, 492, 228, 613, 830, 922, 437, 519, 644, 905, 789, 420, 305, 441, 207, 300, 892, 827, 141, 537, 381, 662, 513, 56, 252, 341, 242, 797, 838, 837, 720, 224, 307, 631, 61, 87, 560, 310, 756, 665, 397, 808, 851, 309, 473, 795, 378, 31, 647, 915, 459, 806, 590, 731, 425, 216, 548, 249, 321, 881, 699, 535, 673, 782, 210, 815, 905, 303, 843, 922, 281, 73, 469, 791, 660, 162, 498, 308, 155, 422, 907, 817, 187, 62, 16, 425, 535, 336, 286, 437, 375, 273, 610, 296, 183, 923, 116, 667, 751, 353, 62, 366, 691, 379, 687, 842, 37, 357, 720, 742, 330, 5, 39, 923, 311, 424, 242, 749, 321, 54, 669, 316, 342, 299, 534, 105, 667, 488, 640, 672, 576, 540, 316, 486, 721, 610, 46, 656, 447, 171, 616, 464, 190, 531, 297, 321, 762, 752, 533, 175, 134, 14, 381, 433, 717, 45, 111, 20, 596, 284, 736, 138, 646, 411, 877, 669, 141, 919, 45, 780, 407, 164, 332, 899, 165, 726, 600, 325, 498, 655, 357, 752, 768, 223, 849, 647, 63, 310, 863, 251, 366, 304, 282, 738, 675, 410, 389, 244, 31, 121, 303, 263], 164 | ] 165 | -------------------------------------------------------------------------------- /pdf417gen/encoding.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | from typing import List, Tuple, Union, Optional, Dict, Any 4 | 5 | from pdf417gen.codes import map_code_word 6 | from pdf417gen.compaction import compact 7 | from pdf417gen.compaction.numeric import compact_numbers 8 | from pdf417gen.error_correction import compute_error_correction_code_words 9 | from pdf417gen.types import Barcode, Codeword 10 | from pdf417gen.util import chunks, to_bytes 11 | 12 | START_CHARACTER = 0x1fea8 13 | STOP_CHARACTER = 0x3fa29 14 | PADDING_CODE_WORD: Codeword = 900 15 | 16 | # Maximum nubmer of code words which can be contained in a bar code, including 17 | # the length descriptor, data, error correction and padding 18 | MAX_CODE_WORDS = 928 19 | 20 | # Limits on the number of rows and columns which can be contained in a bar code 21 | MIN_ROWS = 3 22 | MAX_ROWS = 90 23 | 24 | # Macro PDF417 control block markers 25 | MACRO_MARKER: Codeword = 928 26 | MACRO_TERMINATOR: Codeword = 922 27 | MACRO_OPTIONAL_FIELD_MARKER: Codeword = 923 28 | 29 | # Macro PDF417 optional field designators 30 | MACRO_FILE_NAME: Codeword = 0 31 | MACRO_SEGMENT_COUNT: Codeword = 1 32 | MACRO_TIME_STAMP: Codeword = 2 33 | MACRO_SENDER: Codeword = 3 34 | MACRO_ADDRESSEE: Codeword = 4 35 | MACRO_FILE_SIZE: Codeword = 5 36 | MACRO_CHECKSUM: Codeword = 6 37 | 38 | 39 | def encode( 40 | data: Union[str, bytes], 41 | columns: int = 6, 42 | security_level: int = 2, 43 | encoding: str = "utf-8", 44 | force_rows: Optional[int] = None, 45 | control_block: Optional[List[Codeword]] = None, 46 | force_binary: bool = False 47 | ) -> Barcode: 48 | """ 49 | Encode data into a PDF417 barcode. 50 | 51 | Args: 52 | data: The data to encode (string or bytes) 53 | columns: Number of columns (1-30) 54 | security_level: Error correction level (0-8) 55 | encoding: Character encoding for string data 56 | force_rows: Force exact number of rows (3-90). If None, the number of rows is calculated 57 | control_block: Optional control block for Macro PDF417 58 | force_binary: Force byte compaction mode (useful for pre-compressed data) 59 | 60 | Returns: 61 | Encoded PDF417 barcode 62 | """ 63 | if columns < 1 or columns > 30: 64 | raise ValueError("'columns' must be between 1 and 30. Given: %r" % columns) 65 | 66 | if force_rows is not None: 67 | if force_rows < MIN_ROWS or force_rows > MAX_ROWS: 68 | raise ValueError("'force_rows' must be between 3 and 90. Given: %r" % force_rows) 69 | if security_level < 0 or security_level > 8: 70 | raise ValueError("'security_level' must be between 1 and 8. Given: %r" % security_level) 71 | 72 | # Prepare input 73 | data_bytes = to_bytes(data, encoding) 74 | 75 | # Convert data to code words and split into rows 76 | code_words = encode_high(data_bytes, columns, security_level, control_block, force_rows, force_binary) 77 | rows = list(chunks(code_words, columns)) 78 | 79 | return list(encode_rows(rows, columns, security_level)) 80 | 81 | 82 | def encode_rows(rows: List[Tuple[Codeword, ...]], num_cols: int, security_level: int): 83 | num_rows = len(rows) 84 | 85 | for row_no, row_data in enumerate(rows): 86 | left = get_left_code_word(row_no, num_rows, num_cols, security_level) 87 | right = get_right_code_word(row_no, num_rows, num_cols, security_level) 88 | 89 | yield encode_row(row_no, row_data, left, right) 90 | 91 | 92 | def encode_row(row_no: int, row_words: Tuple[Codeword, ...], left: Codeword, right: Codeword): 93 | table_idx = row_no % 3 94 | 95 | # Convert high level code words to low level code words 96 | left_low = map_code_word(table_idx, left) 97 | right_low = map_code_word(table_idx, right) 98 | row_words_low = [map_code_word(table_idx, word) for word in row_words] 99 | 100 | return [START_CHARACTER, left_low] + row_words_low + [right_low, STOP_CHARACTER] 101 | 102 | 103 | def encode_high( 104 | data: bytes, 105 | columns: int, 106 | security_level: int, 107 | control_block: Optional[List[Codeword]] = None, 108 | force_rows: Optional[int] = None, 109 | force_binary: bool = False 110 | ) -> List[Codeword]: 111 | """Converts the input string to high level code words. 112 | 113 | Including the length indicator, control block (if provided), and the error correction words. 114 | """ 115 | if not control_block: 116 | control_block = [] 117 | # Encode data to code words 118 | data_words = list(compact(data, force_binary)) 119 | 120 | # Calculate total payload length including control block if present 121 | payload_length = len(data_words) + len(control_block) 122 | 123 | # Get the padding to align data to column count 124 | ec_count = 2 ** (security_level + 1) 125 | padding_words = get_padding(payload_length, ec_count, columns, force_rows) 126 | padding_count = len(padding_words) 127 | 128 | # Length descriptor includes all components except error correction 129 | length_descriptor = payload_length + padding_count + 1 130 | 131 | # Total number of code words and number of rows 132 | cw_count = length_descriptor + ec_count 133 | row_count = math.ceil(cw_count / columns) 134 | 135 | # Check the generated bar code's size is within specification parameters 136 | validate_barcode_size(length_descriptor, row_count) 137 | 138 | # Join encoded data with the length specifier, data and padding 139 | extended_words = [length_descriptor] + data_words + padding_words + control_block 140 | 141 | # Calculate error correction words 142 | ec_words = compute_error_correction_code_words(extended_words, security_level) 143 | 144 | return extended_words + ec_words 145 | 146 | 147 | def validate_barcode_size(length_descriptor: int, row_count: int): 148 | if length_descriptor > MAX_CODE_WORDS: 149 | raise ValueError( 150 | "Data too long. Generated bar code has length descriptor of %d. " 151 | "Maximum is %d." % (length_descriptor, MAX_CODE_WORDS)) 152 | 153 | if row_count < MIN_ROWS: 154 | raise ValueError( 155 | "Generated bar code has %d rows. Minimum is %d rows. " 156 | "Try decreasing column count." % (row_count, MIN_ROWS)) 157 | 158 | if row_count > MAX_ROWS: 159 | raise ValueError( 160 | "Generated bar code has %d rows. Maximum is %d rows. " 161 | "Try increasing column count." % (row_count, MAX_ROWS)) 162 | 163 | 164 | def get_left_code_word(row_no: int, num_rows: int, num_cols: int, security_level: int) -> Codeword: 165 | table_id = row_no % 3 166 | 167 | if table_id == 0: 168 | x = (num_rows - 1) // 3 169 | elif table_id == 1: 170 | x = security_level * 3 + (num_rows - 1) % 3 171 | elif table_id == 2: 172 | x = num_cols - 1 173 | else: 174 | raise ValueError("Invalid table_id") 175 | 176 | return 30 * (row_no // 3) + x 177 | 178 | 179 | def get_right_code_word(row_no: int, num_rows: int, num_cols: int, security_level: int) -> Codeword: 180 | table_id = row_no % 3 181 | 182 | if table_id == 0: 183 | x = num_cols - 1 184 | elif table_id == 1: 185 | x = (num_rows - 1) // 3 186 | elif table_id == 2: 187 | x = security_level * 3 + (num_rows - 1) % 3 188 | else: 189 | raise ValueError("Invalid table_id") 190 | 191 | return 30 * (row_no // 3) + x 192 | 193 | 194 | def get_padding(data_count: int, ec_count: int, num_cols: int, force_rows: Optional[int]) -> List[Codeword]: 195 | # Total number of data words and error correction words, additionally 196 | # reserve 1 code word for the length descriptor 197 | total_count = data_count + ec_count + 1 198 | 199 | if force_rows is None: 200 | mod = total_count % num_cols 201 | return [PADDING_CODE_WORD] * (num_cols - mod) if mod > 0 else [] 202 | else: 203 | fill = (force_rows * num_cols) - total_count 204 | if fill < 0: 205 | raise ValueError("Not enough space in the barcode to fit the data") 206 | return [PADDING_CODE_WORD] * fill 207 | 208 | def encode_macro( 209 | data: Union[str, bytes], 210 | columns: int = 6, 211 | security_level: int = 2, 212 | encoding: str = "utf-8", 213 | segment_size: int = 800, 214 | force_rows: Optional[int] = None, 215 | file_id: Optional[List[Codeword]] = None, 216 | file_name: Optional[str] = None, 217 | segment_count: bool = True, 218 | sender: Optional[str] = None, 219 | addressee: Optional[str] = None, 220 | file_size: bool = False, 221 | checksum: Optional[Union[bool, int]] = None, 222 | force_binary: bool = False 223 | ) -> List[Barcode]: 224 | """ 225 | Encode data using Macro PDF417 for large data that needs to be split across 226 | multiple barcodes. 227 | 228 | Args: 229 | data: The data to encode 230 | columns: Number of columns in each symbol (1-30) 231 | security_level: Error correction level (0-8) 232 | encoding: Character encoding for the data 233 | segment_size: Maximum size in bytes for each segment 234 | file_id: Custom file ID codewords or None for auto-generated 235 | file_name: Name of the file to include in the barcode 236 | segment_count: Whether to include the segment count in the barcode (default, to allow multi page outputs) 237 | sender: Name of the sender to include 238 | addressee: Name of the recipient to include 239 | file_size: Whether to include the file size in the barcode 240 | checksum: True to auto-generate, or an integer value (0-65535) 241 | force_binary: Force byte compaction mode (useful for pre-compressed data) 242 | 243 | Timestamps are not supported because the max timestamp is in 1991. 244 | 245 | Returns: 246 | List of PDF417 barcodes, each represented as a list of rows 247 | """ 248 | if columns < 1 or columns > 30: 249 | raise ValueError("'columns' must be between 1 and 30. Given: %r" % columns) 250 | 251 | if security_level < 0 or security_level > 8: 252 | raise ValueError("'security_level' must be between 0 and 8. Given: %r" % security_level) 253 | 254 | # Prepare input data as bytes 255 | data_bytes = to_bytes(data, encoding) 256 | data_size = len(data_bytes) 257 | 258 | # Auto-generate file ID if not provided 259 | if file_id is None: 260 | file_id = [int(time.time()) % 900] 261 | 262 | # Calculate how many segments we need 263 | segments: List[bytes] = [] 264 | for i in range(0, data_size, segment_size): 265 | segments.append(data_bytes[i:i+segment_size]) 266 | 267 | segment_count_value = len(segments) 268 | 269 | # Build optional fields dictionary 270 | optional_fields: Dict[int, Any] = {} 271 | 272 | if file_name is not None: 273 | optional_fields[MACRO_FILE_NAME] = file_name 274 | 275 | if segment_count: 276 | optional_fields[MACRO_SEGMENT_COUNT] = segment_count_value 277 | 278 | if sender is not None: 279 | optional_fields[MACRO_SENDER] = sender 280 | 281 | if addressee is not None: 282 | optional_fields[MACRO_ADDRESSEE] = addressee 283 | 284 | if file_size: 285 | optional_fields[MACRO_FILE_SIZE] = data_size 286 | 287 | if checksum is not None: 288 | if checksum is True: 289 | # TODO compute checksum of the data 290 | raise ValueError("Auto-generated checksum is not supported") 291 | else: 292 | optional_fields[MACRO_CHECKSUM] = checksum 293 | 294 | # Generate barcodes for each segment 295 | barcodes: List[List[List[int]]] = [] 296 | for i, segment_data in enumerate(segments): 297 | # Determine if this is the last segment 298 | is_last = (i == segment_count_value - 1) 299 | 300 | # Create control block for this segment 301 | control_block = create_macro_control_block( 302 | segment_index=i, 303 | file_id=file_id, 304 | optional_fields=optional_fields, 305 | is_last=is_last 306 | ) 307 | 308 | # Encode segment with control block 309 | barcode = encode( 310 | segment_data, 311 | columns, 312 | security_level, 313 | encoding=encoding, 314 | force_rows=force_rows, 315 | control_block=control_block, 316 | force_binary=force_binary 317 | ) 318 | 319 | barcodes.append(barcode) 320 | 321 | return barcodes 322 | 323 | def create_macro_control_block( 324 | segment_index: int, 325 | file_id: List[Codeword], 326 | optional_fields: Dict[int, Any] = {}, 327 | is_last: bool = False 328 | ) -> List[Codeword]: 329 | """ 330 | Create a Macro PDF417 control block. 331 | 332 | Args: 333 | segment_index: Index of this segment (0-99998) 334 | file_id: List of codewords for file ID 335 | optional_fields: Optional fields to include 336 | is_last: Whether this is the last segment 337 | 338 | Returns: 339 | List of codewords for the control block 340 | """ 341 | if segment_index < 0 or segment_index > 99998: 342 | raise ValueError(f"Segment index must be between 0 and 99998. Given: {segment_index}") 343 | 344 | control_block = [MACRO_MARKER] 345 | 346 | # Add segment index (padded to 5 digits and numeric-compacted) 347 | segment_index_str = f"{segment_index:05d}" 348 | numeric_compacted = compact_numbers(to_bytes(segment_index_str)) 349 | control_block.extend(numeric_compacted) 350 | 351 | # Add file ID 352 | control_block.extend(file_id) 353 | 354 | # Add optional fields if provided 355 | if optional_fields: 356 | for field_id, value in optional_fields.items(): 357 | if field_id not in range(7): # Valid field designators are 0-6 358 | raise ValueError(f"Invalid field ID: {field_id}. Must be between 0 and 6.") 359 | 360 | field_codewords = encode_optional_field(field_id, value) 361 | if field_codewords: 362 | control_block.extend(field_codewords) 363 | 364 | # Add terminator for last segment 365 | if is_last: 366 | control_block.append(MACRO_TERMINATOR) 367 | 368 | return control_block 369 | 370 | def encode_optional_field(field_id: int, value: Any) -> List[Codeword]: 371 | """ 372 | Encode an optional field for the control block. 373 | 374 | Args: 375 | field_id: Field designator (0-6) 376 | value: Field value to encode 377 | 378 | Returns: 379 | List of codewords for the optional field or empty list if invalid 380 | """ 381 | result : List[Codeword] = [MACRO_OPTIONAL_FIELD_MARKER, field_id] 382 | 383 | if field_id == MACRO_SEGMENT_COUNT: 384 | # Segment count (numeric compaction, 5 digits max) 385 | count_str = f"{value:05d}" 386 | result.extend(compact_numbers(to_bytes(count_str))) 387 | 388 | elif field_id == MACRO_TIME_STAMP: 389 | # Timestamp allows for four code words for the timestamp 390 | # however these can only represent epoch 900^4 which was 391 | # apparently "far in the future" when the standard was written 392 | # but was actually back in 1991 393 | raise ValueError("Timestamp field is not supported") 394 | 395 | elif field_id in (MACRO_FILE_NAME, MACRO_SENDER, MACRO_ADDRESSEE): 396 | # Text fields use text compaction 397 | text_value = str(value) 398 | compacted = list(compact(to_bytes(text_value))) 399 | result.extend(compacted) 400 | 401 | elif field_id == MACRO_FILE_SIZE: 402 | # File size (numeric) 403 | file_size = int(value) 404 | compacted = list(compact_numbers(to_bytes(str(file_size)))) 405 | result.extend(compacted) 406 | 407 | elif field_id == MACRO_CHECKSUM: 408 | # Checksum (CRC-16 CCITT) 409 | checksum = int(value) 410 | if checksum < 0 or checksum > 65535: 411 | raise ValueError("Checksum must be between 0 and 65535") 412 | # encode as five digits so we always get two code words 413 | checksum_digits = f"{value:05d}" 414 | if isinstance(value, int): 415 | checksum = value 416 | compacted = list(compact_numbers(to_bytes(checksum_digits))) 417 | result.extend(compacted) 418 | 419 | else: 420 | return [] 421 | 422 | return result -------------------------------------------------------------------------------- /pdf417gen/error_correction.py: -------------------------------------------------------------------------------- 1 | from builtins import range 2 | from typing import List 3 | 4 | from pdf417gen.types import Codeword 5 | 6 | from .data import ERROR_CORRECTION_FACTORS 7 | 8 | 9 | def compute_error_correction_code_words(data_words: List[Codeword], level: int): 10 | assert 0 <= level <= 8 11 | 12 | # Correction factors for the given level 13 | factors = ERROR_CORRECTION_FACTORS[level] 14 | 15 | # Number of EC words 16 | count = 2 ** (level + 1) 17 | 18 | # Correction code words list, prepopulated with zeros 19 | ec_words = [0] * count 20 | 21 | # Do the math 22 | for data_word in data_words: 23 | temp = (data_word + ec_words[-1]) % 929 24 | 25 | for x in range(count - 1, -1, -1): 26 | word = ec_words[x - 1] if x > 0 else 0 27 | ec_words[x] = (word + 929 - (temp * factors[x]) % 929) % 929 28 | 29 | return [929 - x if x > 0 else x for x in reversed(ec_words)] 30 | -------------------------------------------------------------------------------- /pdf417gen/rendering.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple, Union 2 | from PIL import Image, ImageColor, ImageOps 3 | from PIL.Image import Resampling 4 | from xml.etree.ElementTree import ElementTree, Element, SubElement 5 | 6 | ColorTuple = Union[Tuple[int, int, int], Tuple[int, int, int, int]] 7 | Color = Union[ColorTuple, str] 8 | 9 | 10 | def barcode_size(codes: List[List[int]]) -> Tuple[int, int]: 11 | """Returns the barcode size in modules.""" 12 | num_rows = len(codes) 13 | num_cols = len(codes[0]) 14 | 15 | # 17 modules per column, last column has an additional module 16 | width = num_cols * 17 + 1 17 | height = num_rows 18 | 19 | return width, height 20 | 21 | 22 | def modules(codes: List[List[int]]): 23 | """Iterates over codes and yields barcode moudles as (y, x) tuples.""" 24 | 25 | for row_id, row in enumerate(codes): 26 | col_id = 0 27 | for value in row: 28 | for digit in format(value, 'b'): 29 | if digit == "1": 30 | yield col_id, row_id 31 | col_id += 1 32 | 33 | 34 | def parse_color(color: str) -> ColorTuple: 35 | return ImageColor.getrgb(color) 36 | 37 | 38 | def rgb_to_hex(color: Color) -> str: 39 | return '#{0:02x}{1:02x}{2:02x}'.format(*color) 40 | 41 | 42 | def render_image( 43 | codes: List[List[int]], 44 | scale: int = 3, 45 | ratio: int = 3, 46 | padding: int = 20, 47 | fg_color: str = "#000", 48 | bg_color: str = "#FFF" 49 | ) -> Image.Image: 50 | width, height = barcode_size(codes) 51 | 52 | # Translate hex code colors to RGB tuples 53 | bg_color_tuple = parse_color(bg_color) 54 | fg_color_tuple = parse_color(fg_color) 55 | 56 | # Construct the image 57 | image = Image.new("RGB", (width, height), bg_color_tuple) 58 | 59 | # Draw the pixle grid 60 | px = image.load() 61 | if px is None: 62 | raise ValueError("Failed loading image") 63 | 64 | for x, y in modules(codes): 65 | px[x, y] = fg_color_tuple 66 | 67 | # Scale and add padding 68 | image = image.resize((scale * width, scale * height * ratio), resample=Resampling.NEAREST) 69 | image = ImageOps.expand(image, padding, bg_color_tuple) 70 | 71 | return image 72 | 73 | 74 | def render_svg( 75 | codes: List[List[int]], 76 | scale: int = 3, 77 | ratio: int = 3, 78 | color: str = "#000", 79 | description: Optional[str] = None 80 | ): 81 | # Barcode size in modules 82 | width, height = barcode_size(codes) 83 | 84 | # Size of each module 85 | scale_x = scale 86 | scale_y = scale * ratio 87 | 88 | color = rgb_to_hex(parse_color(color)) 89 | 90 | root = Element('svg', { 91 | "version": "1.1", 92 | "xmlns": "http://www.w3.org/2000/svg", 93 | "width": str(width * scale_x), 94 | "height": str(height * scale_y), 95 | }) 96 | 97 | if description: 98 | description_element = SubElement(root, 'description') 99 | description_element.text = description 100 | 101 | group = SubElement(root, 'g', { 102 | "id": "barcode", 103 | "fill": color, 104 | "stroke": "none" 105 | }) 106 | 107 | # Generate the barcode modules 108 | for col_id, row_id in modules(codes): 109 | SubElement(group, 'rect', { 110 | "x": str(col_id * scale_x), 111 | "y": str(row_id * scale_y), 112 | "width": str(scale_x), 113 | "height": str(scale_y), 114 | }) 115 | 116 | return ElementTree(element=root) 117 | -------------------------------------------------------------------------------- /pdf417gen/types.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | from typing import Callable, Iterable, List, NamedTuple 3 | 4 | 5 | Codeword = int 6 | """Codeword is an unit of data in the barcode encoded in base 929. 7 | Codewords are represented as integers between 0 and 928.""" 8 | 9 | Barcode = List[List[int]] 10 | """Barcode is a sequence of codewords represented as low level code words ready to render.""" 11 | 12 | CompactionFn = Callable[[bytes], Iterable[Codeword]] 13 | """A function used to convert bytes into codewords""" 14 | 15 | 16 | class Chunk(NamedTuple): 17 | """A chunk of barcode data with accompanying compaction function. 18 | 19 | All `data` must be supported by the `compact_fn`. 20 | """ 21 | 22 | data: bytes 23 | compact_fn: CompactionFn 24 | 25 | 26 | class Submode(Enum): 27 | """Text compaction sub-modes""" 28 | UPPER = auto() 29 | LOWER = auto() 30 | MIXED = auto() 31 | PUNCT = auto() 32 | -------------------------------------------------------------------------------- /pdf417gen/util.py: -------------------------------------------------------------------------------- 1 | from builtins import bytes, str, zip 2 | from itertools import tee, islice, chain 3 | from typing import Any, Generator, Iterable, Iterator, List, Optional, Tuple, TypeVar 4 | 5 | T = TypeVar("T") 6 | 7 | 8 | def from_base(digits: List[int], base: int) -> int: 9 | return sum(v * (base ** (len(digits) - k - 1)) for k, v in enumerate(digits)) 10 | 11 | 12 | def to_base(value: int, base: int) -> List[int]: 13 | digits: List[int] = [] 14 | 15 | while value > 0: 16 | digits.insert(0, value % base) 17 | value //= base 18 | 19 | return digits 20 | 21 | 22 | def switch_base(digits: List[int], source_base: int, target_base: int) -> List[int]: 23 | return to_base(from_base(digits, source_base), target_base) 24 | 25 | 26 | def chunks(iterable: Iterable[T], size: int) -> Generator[Tuple[T, ...], None, None]: 27 | """Generator which chunks data into chunks of given size.""" 28 | it = iter(iterable) 29 | while True: 30 | chunk = tuple(islice(it, size)) 31 | if not chunk: 32 | return 33 | yield chunk 34 | 35 | 36 | def to_bytes(input: Any, encoding: str = "utf-8") -> bytes: 37 | if isinstance(input, bytes): 38 | return input 39 | 40 | if isinstance(input, str): 41 | return bytes(input, encoding) 42 | 43 | raise ValueError("Invalid input, expected string or bytes") 44 | 45 | 46 | def iterate_prev_next(iterable: Iterable[T]) -> Iterator[Tuple[Optional[T], T, Optional[T]]]: 47 | """ 48 | Creates an iterator which provides previous, current and next item. 49 | """ 50 | prevs, items, nexts = tee(iterable, 3) 51 | prevs = chain([None], prevs) 52 | nexts = chain(islice(nexts, 1, None), [None]) 53 | return zip(prevs, items, nexts) 54 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=64", "setuptools_scm>=8"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pdf417gen" 7 | authors = [{ name="Ivan Habunek", email="ivan@habunek.com" }] 8 | description = "PDF417 2D barcode generator for Python" 9 | keywords=["pdf417", "2d", "barcode", "generator"] 10 | readme = "README.rst" 11 | license = { file="LICENSE" } 12 | requires-python = ">=3.8" 13 | dynamic = ["version"] 14 | 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | "Environment :: Console", 20 | ] 21 | 22 | dependencies = [ 23 | "Pillow>=3.3.0" 24 | ] 25 | 26 | [tool.setuptools] 27 | packages = [ 28 | "pdf417gen", 29 | "pdf417gen.compaction" 30 | ] 31 | 32 | [tool.setuptools_scm] 33 | 34 | [project.optional-dependencies] 35 | dev = [ 36 | "build", 37 | "twine", 38 | ] 39 | 40 | test = [ 41 | "mock", 42 | "pytest", 43 | "pytest-cov", 44 | "vermin", 45 | ] 46 | 47 | [project.urls] 48 | "Homepage" = "https://github.com/ihabunek/pdf417-py/" 49 | 50 | [project.scripts] 51 | pdf417gen = "pdf417gen.console:main" 52 | 53 | [tool.pyright] 54 | include = ["pdf417gen"] 55 | typeCheckingMode = "strict" 56 | 57 | [tool.ruff] 58 | line-length = 100 59 | -------------------------------------------------------------------------------- /tests/__init__,py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/pdf417-py/c163f2a6af8d159a54c56c431da6cc642c1c98b6/tests/__init__,py -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/pdf417-py/c163f2a6af8d159a54c56c431da6cc642c1c98b6/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | # Perform the import check once when pytest starts 4 | dependency_missing = False 5 | dependency_import_error = None 6 | try: 7 | # Use __import__ for dynamic import checking based on the string name 8 | import pdf417decoder 9 | print(f"\nINFO: Optional dependency 'pdf417decoder' found. " 10 | f"Integration tests in will be collected.") 11 | except ImportError as e: 12 | dependency_missing = True 13 | dependency_import_error = e 14 | print(f"\nWARNING: Optional dependency 'pdf417decoder' not found (Error: {e}). " 15 | f"Integration tests in 'integration/' will be skipped.") 16 | 17 | # Define the reason message for skipping 18 | skip_reason = f"Skipping integration tests: Optional dependency 'pdf417decoder' not found." 19 | 20 | def pytest_collection_modifyitems(config, items): 21 | """ 22 | Hook to modify the list of collected test items. 23 | 24 | Skips tests if pdf417decoder is not installed. 25 | """ 26 | if not dependency_missing: 27 | # Dependency was found, don't skip anything based on this condition 28 | return 29 | 30 | skip_marker = pytest.mark.skip(reason=skip_reason) 31 | 32 | integration_path_prefix = "tests/integration/" 33 | 34 | for item in items: 35 | # item.location[0] usually gives the file path relative to the rootdir 36 | if item.location[0].startswith(integration_path_prefix): 37 | item.add_marker(skip_marker) 38 | -------------------------------------------------------------------------------- /tests/integration/test_integration.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | import os 3 | import pytest 4 | from pdf417gen import encode, render_image 5 | try: 6 | from .testing_utils import encode_large_data, decode_images 7 | except ImportError: 8 | # these tests will be skipped if pdf417decoder is not installed 9 | pass 10 | 11 | def test_encode_and_decode_short_string(): 12 | # Setup 13 | test_data = "Hello, PDF417!" 14 | 15 | # Encode the data to PDF417 barcode 16 | codes = encode(test_data, columns=3) 17 | image = render_image(codes) # Returns a PIL Image 18 | 19 | # Use the decode_images utility to decode the image 20 | result = decode_images([image]) 21 | 22 | # Assert the decoded data matches the original data 23 | assert result.decode('utf-8') == test_data 24 | 25 | # Needs the dev branch of the pdf417decoder package 26 | # e.g. https://github.com/sparkfish/pdf417decoder.git#subdirectory=python 27 | def test_encode_and_decode_large_data(): 28 | # Setup 29 | test_data = b"Large data " * 1000 # Large data to encode 30 | 31 | # Encode the data to PDF417 barcode 32 | images = encode_large_data(test_data) 33 | 34 | # Use the decode_images utility to decode the images 35 | result = decode_images(images) 36 | 37 | # Assert the decoded data matches the original data 38 | assert result == test_data 39 | 40 | # Needs the dev branch of the pdf417decoder package 41 | # e.g. https://github.com/sparkfish/pdf417decoder.git#subdirectory=python 42 | def test_encode_and_decode_binary_data_with_forced_binary(): 43 | """Test encoding/decoding binary data with force_binary option enabled.""" 44 | # Create some compressed binary data 45 | original_data = b"This is some test data that will be compressed " * 50 46 | compressed_data = zlib.compress(original_data) 47 | 48 | # Encode with force_binary=True to preserve binary data structure 49 | images = encode_large_data(compressed_data, force_binary=True) 50 | 51 | # Decode the images 52 | result = decode_images(images) 53 | 54 | # Decompress and verify 55 | decompressed = zlib.decompress(result) 56 | assert decompressed == original_data 57 | 58 | # Add another test with random binary data 59 | @pytest.mark.parametrize("size", [100, 5000]) 60 | def test_encode_and_decode_random_binary(size: int): 61 | """Test encoding/decoding random binary data with force_binary option.""" 62 | # Generate random binary data 63 | random_data = os.urandom(size) 64 | 65 | # Encode with force_binary=True 66 | images = encode_large_data(random_data, force_binary=True) 67 | 68 | # Decode and verify 69 | result = decode_images(images) 70 | assert result == random_data -------------------------------------------------------------------------------- /tests/integration/testing_utils.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | from pdf417decoder import PDF417Decoder # type: ignore 3 | from pdf417gen import encode_macro 4 | import pdf417gen.rendering 5 | from typing import List, Union 6 | 7 | # add-hoc untility to decode a list of images and reassamble the encoded data 8 | def decode_images(images: List[Image.Image]) -> bytearray: 9 | info_list = [] 10 | for image in images: 11 | decoder = PDF417Decoder(image) 12 | if not decoder.decode(): 13 | raise ValueError('Failed to decode image') 14 | info_list.extend(decoder.barcodes_info) # type: ignore 15 | return PDF417Decoder.assemble_data(info_list) # type: ignore 16 | 17 | def encode_large_data(data: Union[str, bytes], columns: int = 10, scale: int = 3, force_binary: bool = False) -> List[Image.Image]: 18 | """ 19 | Encode large data using Macro PDF417 and return a list of PIL Images. 20 | 21 | Args: 22 | data: String or bytes data to encode 23 | columns: Number of columns (1-30) 24 | scale: Scale factor for the barcode images 25 | force_binary: Force byte compaction mode (useful for pre-compressed data) 26 | 27 | Returns: 28 | List of PIL Image objects containing the encoded barcodes 29 | """ 30 | barcodes = encode_macro(data, columns=columns, file_name = "foobar", force_binary=force_binary) 31 | return [pdf417gen.rendering.render_image(barcode, scale=scale) for barcode in barcodes] 32 | -------------------------------------------------------------------------------- /tests/performance.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple performance test. 3 | """ 4 | 5 | from datetime import datetime 6 | from pdf417gen import encode, render_image, render_svg 7 | 8 | ZEN = """ 9 | Beautiful is better than ugly. 10 | Explicit is better than implicit. 11 | Simple is better than complex. 12 | Complex is better than complicated. 13 | Flat is better than nested. 14 | Sparse is better than dense. 15 | Readability counts. 16 | Special cases aren't special enough to break the rules. 17 | Although practicality beats purity. 18 | Errors should never pass silently. 19 | Unless explicitly silenced. 20 | In the face of ambiguity, refuse the temptation to guess. 21 | There should be one-- and preferably only one --obvious way to do it. 22 | Although that way may not be obvious at first unless you're Dutch. 23 | Now is better than never. 24 | Although never is often better than *right* now. 25 | If the implementation is hard to explain, it's a bad idea. 26 | If the implementation is easy to explain, it may be a good idea. 27 | Namespaces are one honking great idea -- let's do more of those! 28 | """.strip() 29 | 30 | 31 | def test_encode(cycles=100): 32 | start = datetime.now() 33 | for _ in range(cycles): 34 | encode(ZEN) 35 | duration = datetime.now() - start 36 | print("Encode x{}: {}".format(cycles, duration)) 37 | 38 | 39 | def test_render_image(cycles=100): 40 | codes = encode(ZEN) 41 | start = datetime.now() 42 | for _ in range(cycles): 43 | render_image(codes) 44 | duration = datetime.now() - start 45 | print("Render image x{}: {}".format(cycles, duration)) 46 | 47 | 48 | def test_render_svg(cycles=100): 49 | codes = encode(ZEN) 50 | start = datetime.now() 51 | for _ in range(cycles): 52 | render_svg(codes) 53 | duration = datetime.now() - start 54 | print("Render SVG x{}: {}".format(cycles, duration)) 55 | 56 | 57 | if __name__ == "__main__": 58 | test_encode() 59 | test_render_image() 60 | test_render_svg() 61 | -------------------------------------------------------------------------------- /tests/test_compaction.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pdf417gen.compaction import compact, compact_bytes, compact_numbers, compact_text 4 | from pdf417gen.compaction import optimizations, _split_to_chunks, Chunk 5 | from pdf417gen.compaction.text import compact_text_interim 6 | from pdf417gen.encoding import to_bytes 7 | from pdf417gen.data import SWITCH_CODE_LOOKUP 8 | from pdf417gen.types import Submode 9 | 10 | 11 | def test_byte_compactor(): 12 | def do_compact(str): 13 | return list(compact_bytes(to_bytes(str))) 14 | 15 | assert do_compact("alcool") == [163, 238, 432, 766, 244] 16 | assert do_compact("alcoolique") == [163, 238, 432, 766, 244, 105, 113, 117, 101] 17 | assert do_compact("\00alc\00l") == [0, 573, 880, 505, 712] 18 | 19 | def test_text_compactor_interim(): 20 | def do_compact(str): 21 | return list(compact_text_interim(to_bytes(str))) 22 | 23 | # Latch codes for single-code transitions 24 | lm = SWITCH_CODE_LOOKUP[Submode.LOWER][Submode.MIXED] 25 | ul = SWITCH_CODE_LOOKUP[Submode.UPPER][Submode.LOWER] 26 | um = SWITCH_CODE_LOOKUP[Submode.UPPER][Submode.MIXED] 27 | ml = SWITCH_CODE_LOOKUP[Submode.MIXED][Submode.LOWER] 28 | mu = SWITCH_CODE_LOOKUP[Submode.MIXED][Submode.UPPER] 29 | mp = SWITCH_CODE_LOOKUP[Submode.MIXED][Submode.PUNCT] 30 | pu = SWITCH_CODE_LOOKUP[Submode.PUNCT][Submode.UPPER] 31 | 32 | # Upper transitions 33 | assert do_compact("Ff") == [5, ul, 5] 34 | assert do_compact("F#") == [5, um, 15] 35 | assert do_compact("F!") == [5, um, mp, 10] 36 | 37 | # Lower transitions 38 | assert do_compact("fF") == [ul, 5, lm, mu, 5] 39 | assert do_compact("f#") == [ul, 5, lm, 15] 40 | assert do_compact("f!") == [ul, 5, lm, mp, 10] 41 | 42 | # Mixed transitions 43 | assert do_compact("#f") == [um, 15, ml, 5] 44 | assert do_compact("#F") == [um, 15, mu, 5] 45 | assert do_compact("#!") == [um, 15, mp, 10] 46 | 47 | # Punct transitions 48 | assert do_compact("!f") == [um, mp, 10, pu, ul, 5] 49 | assert do_compact("!F") == [um, mp, 10, pu, 5] 50 | assert do_compact("!#") == [um, mp, 10, pu, um, 15] 51 | 52 | 53 | # Bug where the letter g would be encoded as " in the PUNCT submode 54 | # https://github.com/ihabunek/pdf417-py/issues/8 55 | def test_text_compactor_interim_error_letter_g(): 56 | def do_compact(str): 57 | return list(compact_text_interim(to_bytes(str))) 58 | 59 | assert do_compact(">g") == [ 60 | 28, # switch to MIXED 61 | 25, # switch to PUNCT 62 | 2, # Encode >" 63 | 29, # switch to UPPER 64 | 27, # switch to LOWER 65 | 6, # encode g 66 | ] 67 | 68 | 69 | def test_text_compactor(): 70 | def do_compact(str): 71 | return list(compact_text(to_bytes(str))) 72 | 73 | assert do_compact("Super ") == [567, 615, 137, 809] 74 | assert do_compact("Super !") == [567, 615, 137, 808, 760] 75 | 76 | 77 | def test_numbers_compactor(): 78 | numbers = [ord(x) for x in "01234"] 79 | assert list(compact_numbers(numbers)) == [112, 434] 80 | 81 | 82 | def test_compact(): 83 | def do_compact(str): 84 | return list(compact(to_bytes(str))) 85 | 86 | # When starting with text, the first code word does not need to be the switch 87 | # Use 13 digites to avoid optimization which keeps it in text mode 88 | assert do_compact("ABC1234567890123") == [ 89 | 1, 89, 902, 17, 110, 836, 811, 223 90 | ] 91 | 92 | # When starting with numbers, we do need to switch 93 | assert do_compact("1234567890123ABC") == [ 94 | 902, 17, 110, 836, 811, 223, 95 | 900, 1, 89 96 | ] 97 | 98 | # Also with bytes 99 | assert do_compact(b"\x0B") == [901, 11] 100 | 101 | # Alternate bytes switch code when number of bytes is divisble by 6 102 | assert do_compact(b"\x0B\x0B\x0B\x0B\x0B\x0B") == [924, 18, 455, 694, 754, 291] 103 | 104 | 105 | def test_compact_force_binary_text(): 106 | """Test that force_binary=True correctly handles text data.""" 107 | def do_compact(data, force=False): 108 | return list(compact(to_bytes(data), force_binary=force)) 109 | 110 | # Text that would normally use text compaction 111 | text_data = "HelloWorld" 112 | text_normal = do_compact(text_data) 113 | text_forced = do_compact(text_data, force=True) 114 | 115 | # Normal should use text compaction (no byte switch) 116 | assert 901 not in text_normal[:1] # No BYTE_LATCH at start 117 | assert 924 not in text_normal[:1] # No BYTE_LATCH_ALT at start 118 | 119 | # Forced binary should start with byte latch 120 | assert text_forced[0] in (901, 924) 121 | 122 | 123 | def test_compact_force_binary_numeric(): 124 | """Test that force_binary=True correctly handles numeric data.""" 125 | def do_compact(data, force=False): 126 | return list(compact(to_bytes(data), force_binary=force)) 127 | 128 | # Numeric data that would normally use numeric compaction 129 | num_data = "1234567890" 130 | num_normal = do_compact(num_data) 131 | num_forced = do_compact(num_data, force=True) 132 | 133 | # Normal should use numeric compaction (902) 134 | assert num_normal[0] == 902 135 | 136 | # Forced binary should use byte compaction 137 | assert num_forced[0] in (901, 924) 138 | 139 | def test_compact_force_binary_already_binary(): 140 | """Test that force_binary=True correctly handles binary data.""" 141 | def do_compact(data, force=False): 142 | if isinstance(data, str): 143 | data = to_bytes(data) 144 | return list(compact(data, force_binary=force)) 145 | 146 | # Binary data (would use byte compaction either way) 147 | binary_data = b"\x01\x02\x03\x04\x05" 148 | bin_normal = do_compact(binary_data) 149 | bin_forced = do_compact(binary_data, force=True) 150 | 151 | # Both should start with byte latch 152 | assert bin_normal[0] in (901, 924) 153 | assert bin_forced[0] in (901, 924) 154 | 155 | # Should be identical since both use byte compaction 156 | assert bin_normal == bin_forced 157 | 158 | @pytest.mark.parametrize("data,expected", [ 159 | ('aabb1122foobar💔', [ 160 | ('aabb', compact_text), 161 | ('1122', compact_numbers), 162 | ('foobar', compact_text), 163 | ('💔', compact_bytes), 164 | ]), 165 | ]) 166 | def test_split_to_chunks(data, expected): 167 | data = to_bytes(data) 168 | expected = [Chunk(text.encode(), fn) for text, fn in expected] 169 | assert list(_split_to_chunks(data)) == expected 170 | 171 | 172 | @pytest.mark.parametrize("data,expected", [ 173 | # Don't switch to text mode for chunks shorter than 13 numeric chars 174 | # if bordering text chunk 175 | ('foo1234567890bar', [ 176 | ('foo1234567890bar', compact_text), 177 | ]), 178 | ('1234567890bar', [ 179 | ('1234567890bar', compact_text), 180 | ]), 181 | ('foo1234567890', [ 182 | ('foo1234567890', compact_text), 183 | ]), 184 | ('foo1234567890💔', [ 185 | ('foo1234567890', compact_text), 186 | ('💔', compact_bytes), 187 | ]), 188 | ('💔1234567890foo', [ 189 | ('💔', compact_bytes), 190 | ('1234567890foo', compact_text), 191 | ]), 192 | 193 | # Switch for 13+ chars or when not bordering text chunk 194 | ('foo1234567890123bar', [ 195 | ('foo', compact_text), 196 | ('1234567890123', compact_numbers), 197 | ('bar', compact_text), 198 | ]), 199 | ('1234567890', [ 200 | ('1234567890', compact_numbers), 201 | ]), 202 | ('💔1234567890💔', [ 203 | ('💔', compact_bytes), 204 | ('1234567890', compact_numbers), 205 | ('💔', compact_bytes), 206 | ]), 207 | ]) 208 | def test_optimizations(data, expected): 209 | def chars(string): 210 | return [i for i in to_bytes(string)] 211 | 212 | data = to_bytes(data) 213 | expected = [Chunk(text.encode(), fn) for text, fn in expected] 214 | 215 | actual = _split_to_chunks(data) 216 | actual = optimizations.replace_short_numeric_chunks(actual) 217 | actual = optimizations.merge_chunks_with_same_compact_fn(actual) 218 | 219 | assert list(actual) == expected 220 | -------------------------------------------------------------------------------- /tests/test_console.py: -------------------------------------------------------------------------------- 1 | from mock import patch 2 | from pdf417gen import console 3 | 4 | 5 | def test_print_usage(capsys): 6 | console.print_usage() 7 | out, err = capsys.readouterr() 8 | assert "Usage: pdf417gen [command]" in out 9 | assert not err 10 | 11 | 12 | def test_print_err(capsys): 13 | console.print_err("foo") 14 | out, err = capsys.readouterr() 15 | assert not out 16 | assert "foo" in err 17 | 18 | 19 | @patch('pdf417gen.console.encode', return_value="RETVAL") 20 | @patch('pdf417gen.console.render_image') 21 | def test_encode(render_image, encode, capsys): 22 | text = "foo" 23 | 24 | console.do_encode([text]) 25 | 26 | encode.assert_called_once_with( 27 | text, 28 | columns=6, 29 | encoding='utf-8', 30 | security_level=2, 31 | force_binary=False, 32 | ) 33 | 34 | render_image.assert_called_once_with( 35 | 'RETVAL', 36 | bg_color='#FFFFFF', 37 | fg_color='#000000', 38 | padding=20, 39 | ratio=3, 40 | scale=3 41 | ) 42 | 43 | 44 | @patch('sys.stdin.read', return_value="") 45 | @patch('pdf417gen.console.encode', return_value="RETVAL") 46 | @patch('pdf417gen.console.render_image') 47 | def test_encode_no_input(render_image, encode, read, capsys): 48 | console.do_encode([]) 49 | 50 | encode.assert_not_called() 51 | render_image.assert_not_called() 52 | read.assert_called_once_with() 53 | 54 | out, err = capsys.readouterr() 55 | assert not out 56 | assert "No input given" in err 57 | 58 | 59 | @patch('pdf417gen.console.encode', return_value="RETVAL") 60 | @patch('pdf417gen.console.render_image') 61 | def test_encode_exception(render_image, encode, capsys): 62 | encode.side_effect = ValueError("FAILED") 63 | 64 | console.do_encode(["foo"]) 65 | 66 | encode.assert_called_once_with("foo", 67 | columns=6, 68 | encoding='utf-8', 69 | security_level=2, 70 | force_binary=False 71 | ) 72 | render_image.assert_not_called() 73 | 74 | out, err = capsys.readouterr() 75 | assert not out 76 | assert "FAILED" in err 77 | -------------------------------------------------------------------------------- /tests/test_encode.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pdf417gen.compaction import TEXT_LATCH, NUMERIC_LATCH 4 | from pdf417gen.encoding import encode, encode_high, to_bytes, encode_macro 5 | 6 | TEST_DATA = '\n'.join([ 7 | 'HRVHUB30', 8 | 'HRK', 9 | '000000010000000', 10 | 'Ivan Habunek', 11 | 'Savska cesta 13', 12 | '10000 Zagreb', 13 | 'Big Fish Software d.o.o.', 14 | 'Savska cesta 13', 15 | '10000 Zagreb', 16 | 'HR6623400091110651272', 17 | '00', 18 | 'HR123456', 19 | 'ANTS', 20 | 'Razvoj paketa za bar kodove\n' 21 | ]) 22 | 23 | 24 | def test_encode_high(): 25 | 26 | # High level encoding 27 | expected = [ 28 | 130, 227, 637, 601, 843, 25, 479, 227, 328, 765, 29 | 30 | NUMERIC_LATCH, 1, 624, 142, 113, 522, 200, 31 | 32 | TEXT_LATCH, 865, 479, 267, 630, 416, 868, 237, 1, 613, 130, 865, 479, 33 | 567, 21, 550, 26, 64, 559, 26, 841, 115, 479, 841, 0, 0, 808, 777, 6, 34 | 514, 58, 765, 871, 818, 206, 868, 177, 258, 236, 868, 567, 425, 592, 17, 35 | 146, 118, 537, 448, 537, 448, 535, 479, 567, 21, 550, 26, 64, 559, 26, 36 | 841, 115, 479, 841, 0, 0, 808, 777, 6, 514, 58, 765, 877, 539, 37 | 38 | NUMERIC_LATCH, 31, 251, 786, 557, 565, 1, 372, 39 | 40 | TEXT_LATCH, 865, 479, 840, 25, 479, 227, 841, 63, 125, 205, 479, 13, 41 | 588, 865, 479, 537, 25, 644, 296, 450, 304, 570, 805, 26, 30, 536, 314, 42 | 104, 634, 865, 479, 73, 714, 436, 412, 39, 661, 428, 120 43 | 44 | ] 45 | 46 | assert encode_high(to_bytes(TEST_DATA), 6, 2) == expected 47 | 48 | 49 | def test_encode_low(): 50 | 51 | # Low level encoding 52 | expected = [ 53 | [130728, 119920, 82192, 93980, 67848, 99590, 66798, 110200, 128318, 260649], 54 | [130728, 129678, 101252, 127694, 75652, 113982, 97944, 129720, 129678, 260649], 55 | [130728, 86496, 66846, 104188, 106814, 96800, 93944, 102290, 119934, 260649], 56 | [130728, 128190, 73160, 96008, 102812, 67872, 115934, 73156, 119520, 260649], 57 | [130728, 120588, 104224, 129720, 129938, 119200, 81084, 101252, 120588, 260649], 58 | [130728, 125892, 113798, 88188, 71822, 129766, 108158, 113840, 120784, 260649], 59 | [130728, 85880, 120638, 66758, 119006, 96008, 66758, 120256, 85560, 260649], 60 | [130728, 128176, 128352, 99048, 123146, 128280, 115920, 110492, 128176, 260649], 61 | [130728, 129634, 99166, 67438, 81644, 127604, 67404, 111676, 85054, 260649], 62 | [130728, 107422, 91664, 121136, 73156, 78032, 79628, 99680, 107452, 260649], 63 | [130728, 119692, 125744, 107396, 85894, 70600, 123914, 70600, 119692, 260649], 64 | [130728, 129588, 77902, 105628, 67960, 113798, 88188, 71822, 107390, 260649], 65 | [130728, 82208, 120638, 108348, 117798, 120638, 66758, 119006, 106672, 260649], 66 | [130728, 128070, 101252, 123018, 128352, 128352, 99048, 123146, 128070, 260649], 67 | [130728, 82206, 108792, 72094, 84028, 99166, 69442, 97048, 82108, 260649], 68 | [130728, 124350, 81384, 89720, 91712, 67618, 112848, 69712, 104160, 260649], 69 | [130728, 83928, 129720, 116966, 97968, 81084, 101252, 127450, 83928, 260649], 70 | [130728, 124392, 128456, 67960, 121150, 98018, 85240, 82206, 124388, 260649], 71 | [130728, 126222, 112152, 96008, 120560, 77928, 73160, 96008, 111648, 260649], 72 | [130728, 82918, 70600, 125702, 78322, 121744, 116762, 103328, 82918, 260649], 73 | [130728, 74992, 80048, 73296, 129766, 128450, 97072, 116210, 93424, 260649], 74 | [130728, 93744, 106800, 101784, 73160, 96008, 125116, 126828, 112440, 260649], 75 | [130728, 127628, 120948, 102632, 120582, 78074, 128532, 85966, 127628, 260649], 76 | ] 77 | 78 | assert list(encode(TEST_DATA, 6, 2)) == expected 79 | 80 | 81 | def test_encode_unicode(): 82 | # These two should encode to the same string 83 | uc = u"love 💔" 84 | by = b"love \xf0\x9f\x92\x94" 85 | 86 | expected = [ 87 | [130728, 120256, 108592, 115526, 126604, 103616, 66594, 126094, 128318, 260649], 88 | [130728, 125456, 83916, 107396, 83872, 97968, 77702, 98676, 128352, 260649], 89 | [130728, 86496, 128114, 90190, 98038, 72124, 72814, 81040, 86256, 260649]] 90 | 91 | assert encode(uc) == expected 92 | assert encode(by) == expected 93 | 94 | 95 | def test_force_binary_encode(): 96 | # Test forcing binary encoding for data that would normally use text compaction 97 | text_data = "ABC123" 98 | 99 | # Expected encoding when forced to binary mode 100 | expected = [ 101 | [130728, 120256, 108592, 101940, 82448, 120908, 70672, 69848, 128318, 260649], 102 | [130728, 125456, 121288, 97968, 97968, 97968, 124380, 127396, 128352, 260649], 103 | [130728, 86496, 102974, 71550, 100246, 102182, 95280, 69456, 86256, 260649] 104 | ] 105 | 106 | # Force binary encoding 107 | assert list(encode(text_data, force_binary=True)) == expected 108 | 109 | 110 | def test_force_row_height(): 111 | # Test forcing a specific row height 112 | # short that data will never naturally fill the row height 113 | test_data = "?" 114 | row_height = 15 115 | 116 | assert len(encode(test_data, force_rows=row_height, columns=6)) == row_height 117 | 118 | def test_encode_macro_single_segment(): 119 | # Test macro PDF417 with only one segment 120 | test_data = "single segment" 121 | file_id = [123] 122 | 123 | # Expected encoding for a single segment macro PDF417 124 | # TODO: Consider hooking we can compare the high level encoding 125 | expected = [ 126 | [ 127 | [130728, 125680, 110320, 98892, 121244, 117104, 103616, 69830, 128318, 260649], 128 | [130728, 129678, 118888, 119184, 100598, 97968, 97968, 97968, 129720, 260649], 129 | [130728, 86496, 102290, 116714, 106876, 83518, 106686, 100306, 119934, 260649], 130 | [130728, 89720, 125680, 82440, 118968, 122738, 68996, 102088, 119520, 260649], 131 | [130728, 120588, 123522, 110492, 72680, 80632, 120672, 108428, 120624, 260649], 132 | ] 133 | ] 134 | 135 | result = encode_macro(test_data, file_id=file_id) 136 | assert len(result) == len(expected) 137 | assert result[0] == expected[0] 138 | 139 | 140 | def test_encode_macro_multiple_segments(): 141 | # Test macro PDF417 with multiple segments 142 | file_id = [456] 143 | 144 | # First segment 145 | test_data = b"two segments" 146 | result = encode_macro(test_data, segment_size=6, file_id=file_id) 147 | assert len(result) == 2 148 | expected1 = [ 149 | [130728, 125680, 120440, 69008, 105860, 105524, 103520, 68708, 128318, 260649], 150 | [130728, 128280, 97968, 97968, 81702, 108422, 108292, 129198, 129720, 260649], 151 | [130728, 86496, 100306, 120312, 106876, 83838, 71864, 120060, 108792, 260649], 152 | [130728, 89720, 81384, 67686, 105024, 122562, 124818, 125086, 119520, 260649] 153 | ] 154 | assert result[0] == expected1 155 | expected2 = [ 156 | [130728, 125680, 120440, 67022, 70688, 105240, 100390, 68708, 128318, 260649], 157 | [130728, 128280, 97968, 81702, 108422, 108290, 129198, 81740, 129720, 260649], 158 | [130728, 86496, 120312, 106876, 83838, 100308, 71964, 67496, 108792, 260649], 159 | [130728, 89720, 102552, 100056, 94008, 99976, 121356, 117694, 119520, 260649] 160 | ] 161 | assert result[1] == expected2 162 | 163 | 164 | def test_max_barcode_size(): 165 | # Borderline 166 | encode("x" * 1853, columns=16, security_level=6) 167 | 168 | # Data too long 169 | with pytest.raises(ValueError) as ex: 170 | encode("x" * 1854, columns=16, security_level=6) 171 | assert str(ex.value) == "Data too long. Generated bar code has length descriptor of 944. Maximum is 928." 172 | 173 | # Too few rows 174 | with pytest.raises(ValueError) as ex: 175 | encode("x", columns=16, security_level=1) 176 | assert str(ex.value) == "Generated bar code has 1 rows. Minimum is 3 rows. Try decreasing column count." 177 | 178 | # Too many rows 179 | with pytest.raises(ValueError) as ex: 180 | encode("x" * 1853, columns=8, security_level=6) 181 | assert str(ex.value) == "Generated bar code has 132 rows. Maximum is 90 rows. Try increasing column count." 182 | -------------------------------------------------------------------------------- /tests/test_error_correction.py: -------------------------------------------------------------------------------- 1 | from pdf417gen.error_correction import compute_error_correction_code_words 2 | 3 | 4 | def test_error_correction(): 5 | data = [16, 902, 1, 278, 827, 900, 295, 902, 2, 326, 823, 544, 900, 149, 900, 900] 6 | 7 | expected_level_0 = [156, 765] 8 | expected_level_1 = [168, 875, 63, 355] 9 | expected_level_2 = [628, 715, 393, 299, 863, 601, 169, 708] 10 | expected_level_3 = [232, 176, 793, 616, 476, 406, 855, 445, 84, 518, 522, 721, 607, 2, 42, 578] 11 | expected_level_4 = [281, 156, 276, 668, 44, 252, 877, 30, 549, 856, 773, 639, 420, 330, 693, 329, 283, 723, 480, 482, 102, 925, 535, 892, 374, 472, 837, 331, 343, 608, 390, 364] 12 | expected_level_5 = [31, 850, 18, 870, 53, 477, 837, 130, 533, 186, 266, 450, 39, 492, 542, 653, 499, 887, 618, 103, 364, 313, 906, 396, 270, 735, 593, 81, 557, 712, 810, 48, 167, 533, 205, 577, 503, 126, 449, 189, 859, 471, 493, 849, 554, 76, 878, 893, 168, 497, 251, 704, 311, 650, 283, 268, 462, 223, 659, 763, 176, 34, 544, 304] 13 | expected_level_6 = [345, 775, 909, 489, 650, 568, 869, 577, 574, 349, 885, 317, 492, 222, 783, 451, 647, 385, 168, 366, 118, 655, 643, 551, 179, 880, 880, 752, 132, 206, 765, 862, 727, 240, 32, 266, 911, 287, 813, 437, 868, 201, 681, 867, 567, 398, 508, 564, 504, 676, 785, 554, 831, 566, 424, 93, 515, 275, 61, 544, 272, 621, 374, 922, 779, 663, 789, 295, 631, 536, 755, 465, 485, 416, 76, 412, 76, 431, 28, 614, 767, 419, 600, 779, 94, 584, 647, 846, 121, 97, 790, 205, 424, 793, 263, 271, 694, 522, 437, 817, 382, 164, 113, 849, 178, 602, 554, 261, 415, 737, 401, 675, 203, 271, 649, 120, 765, 209, 522, 687, 420, 32, 60, 266, 270, 228, 304, 270] 14 | expected_level_7 = [142, 203, 799, 4, 105, 137, 793, 914, 225, 636, 60, 171, 490, 180, 414, 141, 399, 599, 829, 288, 108, 268, 444, 481, 795, 146, 655, 778, 189, 32, 597, 206, 208, 711, 845, 608, 642, 636, 540, 795, 845, 466, 492, 659, 138, 800, 912, 171, 92, 438, 225, 301, 777, 449, 230, 448, 326, 182, 892, 681, 543, 582, 732, 758, 162, 587, 685, 378, 646, 356, 354, 25, 839, 839, 556, 253, 501, 771, 745, 616, 473, 293, 669, 822, 613, 684, 229, 265, 110, 438, 144, 727, 317, 605, 414, 497, 82, 278, 267, 323, 43, 894, 624, 282, 790, 579, 430, 255, 802, 553, 922, 604, 68, 692, 809, 909, 663, 589, 735, 670, 298, 158, 201, 68, 124, 64, 67, 338, 694, 373, 225, 579, 309, 699, 920, 432, 717, 72, 126, 819, 142, 755, 473, 630, 331, 758, 730, 65, 359, 451, 236, 16, 56, 31, 87, 587, 125, 385, 384, 197, 352, 383, 173, 271, 38, 558, 810, 260, 521, 680, 7, 319, 650, 334, 695, 708, 0, 562, 365, 204, 114, 185, 560, 746, 767, 449, 797, 688, 63, 135, 818, 805, 3, 536, 908, 532, 400, 698, 49, 212, 630, 93, 157, 275, 3, 20, 611, 179, 302, 282, 876, 665, 241, 206, 474, 80, 217, 460, 462, 751, 719, 571, 536, 794, 522, 385, 598, 756, 162, 212, 758, 662, 361, 223, 587, 857, 503, 382, 615, 86, 283, 541, 847, 518, 406, 736, 486, 408, 226, 342, 784, 772, 211, 888, 234, 335] 15 | expected_level_8 = [538, 446, 840, 510, 163, 708, 177, 666, 423, 600, 707, 913, 770, 571, 156, 683, 676, 697, 898, 776, 128, 851, 163, 854, 135, 661, 880, 279, 92, 324, 397, 207, 379, 223, 574, 9, 70, 858, 878, 579, 61, 551, 261, 388, 315, 856, 266, 865, 923, 38, 313, 62, 381, 198, 265, 256, 385, 878, 347, 532, 821, 53, 855, 225, 697, 826, 263, 334, 207, 565, 460, 496, 705, 599, 383, 289, 178, 168, 401, 268, 555, 190, 922, 284, 180, 810, 891, 832, 636, 813, 894, 495, 701, 484, 204, 793, 129, 164, 444, 228, 636, 98, 809, 57, 736, 697, 727, 534, 889, 480, 898, 773, 234, 851, 880, 843, 714, 443, 412, 489, 578, 468, 367, 663, 11, 686, 319, 352, 345, 670, 106, 106, 219, 466, 439, 350, 538, 66, 852, 175, 465, 731, 332, 110, 926, 491, 18, 422, 736, 797, 624, 376, 728, 526, 735, 200, 502, 923, 789, 529, 923, 706, 384, 869, 172, 548, 520, 463, 813, 384, 793, 231, 190, 653, 864, 351, 400, 525, 487, 828, 654, 307, 141, 638, 770, 775, 282, 54, 758, 197, 492, 320, 86, 790, 275, 237, 923, 25, 591, 605, 61, 824, 79, 631, 532, 337, 867, 423, 340, 597, 682, 923, 287, 408, 503, 361, 881, 196, 468, 759, 746, 389, 124, 784, 198, 865, 538, 451, 178, 772, 653, 121, 497, 598, 711, 716, 241, 159, 429, 88, 799, 761, 639, 105, 54, 807, 351, 435, 793, 873, 360, 8, 881, 479, 693, 576, 849, 875, 771, 621, 134, 863, 8, 171, 799, 924, 103, 63, 491, 538, 597, 855, 697, 499, 7, 886, 286, 85, 107, 220, 319, 124, 197, 150, 729, 899, 585, 540, 676, 414, 256, 856, 596, 259, 882, 436, 26, 273, 753, 127, 679, 390, 654, 42, 276, 420, 247, 629, 116, 803, 131, 25, 403, 645, 462, 897, 151, 622, 108, 167, 227, 831, 887, 662, 739, 263, 829, 56, 624, 317, 908, 378, 39, 393, 861, 338, 202, 179, 907, 109, 360, 736, 554, 342, 594, 125, 433, 394, 195, 698, 844, 912, 530, 842, 337, 294, 528, 231, 735, 93, 8, 579, 42, 148, 609, 233, 782, 887, 888, 915, 620, 78, 137, 161, 282, 217, 775, 564, 33, 195, 36, 584, 679, 775, 476, 309, 230, 303, 708, 143, 679, 502, 814, 193, 508, 532, 542, 580, 603, 641, 338, 361, 542, 537, 810, 394, 764, 136, 167, 611, 881, 775, 267, 433, 142, 202, 828, 363, 101, 728, 660, 583, 483, 786, 717, 190, 809, 422, 567, 741, 695, 310, 120, 177, 47, 494, 345, 508, 16, 639, 402, 625, 286, 298, 358, 54, 705, 916, 291, 424, 375, 883, 655, 675, 498, 498, 884, 862, 365, 310, 805, 763, 855, 354, 777, 543, 53, 773, 120, 408, 234, 728, 438, 914, 3, 670, 546, 465, 449, 923, 51, 546, 709, 648, 96, 320, 682, 326, 848, 234, 855, 791, 20, 97, 901, 351, 317, 764, 767, 312, 206, 139, 610, 578, 646, 264, 389, 238, 675, 595, 430, 88] 16 | 17 | assert compute_error_correction_code_words(data, 0) == expected_level_0 18 | assert compute_error_correction_code_words(data, 1) == expected_level_1 19 | assert compute_error_correction_code_words(data, 2) == expected_level_2 20 | assert compute_error_correction_code_words(data, 3) == expected_level_3 21 | assert compute_error_correction_code_words(data, 4) == expected_level_4 22 | assert compute_error_correction_code_words(data, 5) == expected_level_5 23 | assert compute_error_correction_code_words(data, 6) == expected_level_6 24 | assert compute_error_correction_code_words(data, 7) == expected_level_7 25 | assert compute_error_correction_code_words(data, 8) == expected_level_8 26 | -------------------------------------------------------------------------------- /tests/test_optimizations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihabunek/pdf417-py/c163f2a6af8d159a54c56c431da6cc642c1c98b6/tests/test_optimizations.py -------------------------------------------------------------------------------- /tests/test_renderers.py: -------------------------------------------------------------------------------- 1 | from pdf417gen import render_svg, render_image, encode 2 | from pdf417gen.rendering import barcode_size, rgb_to_hex 3 | from PIL.Image import Image 4 | from xml.etree.ElementTree import ElementTree 5 | 6 | codes = encode("hello world!") 7 | 8 | 9 | def modules(codes): 10 | """Iterates over barcode codes and yields barcode moudles. 11 | 12 | Yields: column number (int), row number (int), module visibility (bool). 13 | """ 14 | 15 | for row_id, row in enumerate(codes): 16 | col_id = 0 17 | for value in row: 18 | for digit in format(value, 'b'): 19 | yield col_id, row_id, digit == "1" 20 | col_id += 1 21 | 22 | 23 | def test_rgb_to_hex(): 24 | assert rgb_to_hex((255, 0, 0)) == "#ff0000" 25 | assert rgb_to_hex((0, 255, 0)) == "#00ff00" 26 | assert rgb_to_hex((0, 0, 255)) == "#0000ff" 27 | 28 | assert rgb_to_hex((100, 0, 0)) == "#640000" 29 | assert rgb_to_hex((0, 100, 0)) == "#006400" 30 | assert rgb_to_hex((0, 0, 100)) == "#000064" 31 | 32 | assert rgb_to_hex((111, 222, 32)) == "#6fde20" 33 | 34 | 35 | def test_render_svg(): 36 | scale = 2 37 | ratio = 4 38 | description = "hi there" 39 | 40 | tree = render_svg(codes, scale=scale, ratio=ratio, description=description) 41 | assert isinstance(tree, ElementTree) 42 | assert tree.findtext("description") == description 43 | 44 | # Test expected size 45 | width, height = barcode_size(codes) 46 | 47 | root = tree.getroot() 48 | 49 | assert root.get("width") == str(width * scale) 50 | assert root.get("height") == str(height * scale * ratio) 51 | assert root.get("version") == "1.1" 52 | assert root.get("xmlns") == "http://www.w3.org/2000/svg" 53 | 54 | # Check number of rendered modules (only visible ones) 55 | expected_module_count = len([v for x, y, v in modules(codes) if v]) 56 | actual_module_count = len(root.findall('g/rect')) 57 | 58 | assert expected_module_count == actual_module_count 59 | 60 | 61 | def test_render_image(): 62 | width, height = barcode_size(codes) 63 | 64 | image = render_image(codes) 65 | assert isinstance(image, Image) 66 | 67 | image = render_image(codes, scale=1, ratio=1, padding=0) 68 | assert image.size == (width, height) 69 | 70 | image = render_image(codes, scale=2, ratio=1, padding=0) 71 | assert image.size == (2 * width, 2 * height) 72 | 73 | image = render_image(codes, scale=2, ratio=2, padding=0) 74 | assert image.size == (2 * width, 4 * height) 75 | 76 | image = render_image(codes, scale=2, ratio=2, padding=20) 77 | assert image.size == (2 * width + 40, 4 * height + 40) 78 | 79 | # Check actual pixels 80 | fg_color = "LemonChiffon" 81 | bg_color = "#aabbcc" 82 | 83 | fg_parsed = (255, 250, 205) 84 | bg_parsed = (170, 187, 204) 85 | 86 | image = render_image(codes, scale=1, ratio=1, padding=0, 87 | fg_color=fg_color, bg_color=bg_color) 88 | px = image.load() 89 | 90 | for column, row, visible in modules(codes): 91 | expected = fg_parsed if visible else bg_parsed 92 | assert px[column, row] == expected 93 | --------------------------------------------------------------------------------