├── .gitattributes ├── .github └── workflows │ └── test_and_release.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── requirements-test.txt ├── requirements.txt ├── setup.py ├── test ├── barcodes │ ├── AZTEC-easy.jpg │ ├── AZTEC-utf8.png │ ├── CODE_128-easy.jpg │ ├── PDF_417-easy.bmp │ ├── QR CODE (¡filenáme törture test! 😉).png │ ├── QR_CODE-easy.png │ ├── QR_CODE-fun-with-whitespace.png │ ├── QR_CODE-png-but-wrong-extension.bmp │ ├── QR_CODE-screen_scraping_torture_test.png │ ├── bad_format.png │ └── empty.png └── test_all.py └── zxing ├── __init__.py ├── __main__.py └── version.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png -text binary 2 | -------------------------------------------------------------------------------- /.github/workflows/test_and_release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: test_and_release 5 | 6 | on: [ push, pull_request] 7 | 8 | permissions: 9 | checks: write 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.x'] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v3 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | python -m pip install setuptools flake8 pytest 29 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 30 | - name: Lint with flake8 31 | run: | 32 | # stop the build if there are Python syntax errors or undefined names 33 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 34 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 35 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 36 | - name: Build 37 | run: | 38 | python setup.py sdist 39 | - name: Store distribution packages 40 | uses: actions/upload-artifact@v3 41 | with: 42 | name: dist 43 | path: dist/ 44 | 45 | test: 46 | 47 | runs-on: ubuntu-latest 48 | needs: build 49 | strategy: 50 | matrix: 51 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.x'] 52 | 53 | steps: 54 | - uses: actions/checkout@v3 55 | - name: Set up Python 56 | uses: actions/setup-python@v3 57 | with: 58 | python-version: ${{ matrix.python-version }} 59 | - name: Install dependencies 60 | run: | 61 | python -m pip install --upgrade pip setuptools 62 | if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi 63 | sudo apt update 64 | sudo apt install -qq openjdk-11-jre # JDK is needed to run tests 65 | - name: Test 66 | run: | 67 | python setup.py build 68 | nose2 -v --pretty-assert --plugin nose2.plugins.junitxml --junit-xml 69 | - name: Publish Test Report 70 | uses: mikepenz/action-junit-report@v3 71 | if: success() || failure() # always run 72 | with: 73 | report_paths: 'nose2-junit.xml' 74 | 75 | # https://github.com/actions/starter-workflows/blob/main/ci/python-publish.yml 76 | release: 77 | 78 | runs-on: ubuntu-latest 79 | needs: test 80 | if: startsWith(github.ref, 'refs/tags/v') 81 | 82 | environment: 83 | name: pypi 84 | url: https://pypi.org/p/zxing 85 | permissions: 86 | # IMPORTANT: this permission is mandatory for trusted publishing 87 | id-token: write 88 | steps: 89 | - name: Download distribution packages 90 | uses: actions/download-artifact@v3 91 | with: 92 | name: dist 93 | path: dist/ 94 | - name: Deploy to PyPI 95 | uses: pypa/gh-action-pypi-publish@release/v1 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | build/* 3 | dist 4 | dist/* 5 | *.pyc 6 | zxing/java/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements*.txt 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-zxing 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/zxing.svg)](https://pypi.python.org/pypi/zxing) 4 | [![Build Status](https://github.com/dlenski/python-zxing/workflows/test_and_release/badge.svg)](https://github.com/dlenski/python-zxing/actions/workflows/test_and_release.yml) 5 | [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) 6 | 7 | This is a wrapper for the [ZXing barcode library](https://github.com/zxing/zxing). 8 | It will allow you to read and decode barcode images from Python. 9 | 10 | It was originally a "slightly less quick-and-dirty" fork of [oostendo/python-zxing](https://github.com/oostendo/python-zxing), but has since 11 | evolved considerably beyond that ancestral package. 12 | 13 | ## Dependencies and installation 14 | 15 | Use the Python 3 version of pip (usually invoked via `pip3`) to install: `pip3 install zxing` 16 | 17 | * You'll neeed to have a recent `java` binary somewhere in your path. (Tested with OpenJDK v7, v8, v11.) 18 | * pip will automatically download the relevant [JAR](https://en.wikipedia.org/wiki/JAR_(file_format)) files for the Java ZXing libraries (currently v3.5.3) 19 | 20 | ## Usage 21 | 22 | The `BarCodeReader` class is used to decode images: 23 | 24 | ```python 25 | >>> import zxing 26 | >>> reader = zxing.BarCodeReader() 27 | >>> print(reader.zxing_version, reader.zxing_version_info) 28 | 3.5.1 (3, 5, 1) 29 | >>> barcode = reader.decode("test/barcodes/QR_CODE-easy.png") 30 | >>> print(barcode) 31 | BarCode(raw='This should be QR_CODE', parsed='This should be QR_CODE', path='test/barcodes/QR_CODE-easy.png', format='QR_CODE', type='TEXT', points=[(15.0, 87.0), (15.0, 15.0), (87.0, 15.0), (75.0, 75.0)]) 32 | ``` 33 | 34 | The attributes of the decoded `BarCode` object are `raw`, `parsed`, `path`, `format`, `type`, `points`, and `raw_bits`. 35 | The list of formats which ZXing can decode is [here](https://zxing.github.io/zxing/apidocs/com/google/zxing/BarcodeFormat.html). 36 | 37 | The `decode()` method accepts an image path or [PIL Image object](https://pillow.readthedocs.io/en/stable/reference/Image.html) (or list thereof) 38 | and takes optional parameters `try_harder` (boolean), `possible_formats` (list of formats to consider), and `pure_barcode` (boolean). 39 | If no barcode is found, it returns a `False`-y `BarCode` object with all fields except `path` set to `None`. 40 | If it encounters any other recognizable error from the Java ZXing library, it raises `BarCodeReaderException`. 41 | 42 | ## Command-line interface 43 | 44 | The command-line interface can decode images into barcodes and output in either a human-readable or CSV format: 45 | 46 | ``` 47 | usage: zxing [-h] [-c] [--try-harder] [--pure-barcode] [-V] image [image ...] 48 | ``` 49 | 50 | Human-readable: 51 | 52 | ```sh 53 | $ zxing /tmp/barcode.png 54 | /tmp/barcode.png 55 | ================ 56 | Decoded TEXT barcode in QR_CODE format. 57 | Raw text: 'Testing 123' 58 | Parsed text: 'Testing 123' 59 | ``` 60 | 61 | CSV output (can be opened by LibreOffice or Excel): 62 | 63 | ```sh 64 | $ zxing /tmp/barcode1.png /tmp/barcode2.png /tmp/barcode3.png 65 | Filename,Format,Type,Raw,Parsed 66 | /tmp/barcode1.png,CODE_128,TEXT,Testing 123,Testing 123 67 | /tmp/barcode2.png,QR_CODE,URI,http://zxing.org,http://zxing.org 68 | /tmp/barcode3.png,QR_CODE,TEXT,"This text, ""Has stuff in it!"" Wow⏎Yes it does!","This text, ""Has stuff in it!"" Wow⏎Yes it does!" 69 | ``` 70 | 71 | ## License 72 | 73 | LGPLv3 74 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | nose2>=0.10 2 | 3 | pillow>=3.0,<6.0; python_version < '3.5' 4 | pillow>=3.0,<8.0; python_version >= '3.5' and python_version < '3.6' 5 | pillow>=8.0; python_version >= '3.6' 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlenski/python-zxing/1fcabc864cc62142f2744e1bf43e6ffb7d6e309b/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from os import R_OK, access, makedirs, path 5 | from urllib.error import URLError 6 | from urllib.request import urlretrieve 7 | 8 | from setuptools import setup 9 | 10 | if not sys.version_info[0] == 3: 11 | sys.exit("Python 2.x is not supported; Python 3.x is required.") 12 | 13 | ######################################## 14 | 15 | version_py = path.join(path.dirname(__file__), 'zxing', 'version.py') 16 | 17 | d = {} 18 | with open(version_py, 'r') as fh: 19 | exec(fh.read(), d) 20 | version_pep = d['__version__'] 21 | 22 | ######################################## 23 | 24 | 25 | def download_java_files(force=False): 26 | files = {'java/javase.jar': 'https://repo1.maven.org/maven2/com/google/zxing/javase/3.5.3/javase-3.5.3.jar', 27 | 'java/core.jar': 'https://repo1.maven.org/maven2/com/google/zxing/core/3.5.3/core-3.5.3.jar', 28 | 'java/jcommander.jar': 'https://repo1.maven.org/maven2/com/beust/jcommander/1.82/jcommander-1.82.jar'} 29 | 30 | for fn, url in files.items(): 31 | p = path.join(path.dirname(__file__), 'zxing', fn) 32 | d = path.dirname(p) 33 | if not force and access(p, R_OK): 34 | print("Already have %s." % p) 35 | else: 36 | print("Downloading %s from %s ..." % (p, url)) 37 | try: 38 | makedirs(d, exist_ok=True) 39 | urlretrieve(url, p) 40 | except (OSError, URLError) as e: 41 | raise 42 | return list(files.keys()) 43 | 44 | 45 | setup( 46 | name='zxing', 47 | version=version_pep, 48 | description="Wrapper for decoding/reading barcodes with ZXing (Zebra Crossing) library", 49 | long_description=open('README.md', encoding='utf-8').read(), 50 | long_description_content_type='text/markdown', 51 | url="https://github.com/dlenski/python-zxing", 52 | author='Daniel Lenski', 53 | author_email='dlenski@gmail.com', 54 | packages=['zxing'], 55 | package_data={'zxing': download_java_files()}, 56 | entry_points={'console_scripts': ['zxing=zxing.__main__:main']}, 57 | extras_require={ 58 | "Image": [ 59 | "pillow>=3.0,<6.0; python_version < '3.5'", 60 | "pillow>=3.0,<8.0; python_version >= '3.5' and python_version < '3.6'", 61 | "pillow>=8.0; python_version >= '3.6'", 62 | ] 63 | }, 64 | install_requires=open('requirements.txt').readlines(), 65 | python_requires=">=3", 66 | tests_require=open('requirements-test.txt').readlines(), 67 | test_suite='nose2.collector.collector', 68 | license='LGPL v3 or later', 69 | classifiers=[ 70 | 'Development Status :: 4 - Beta', 71 | 'Intended Audience :: Developers', 72 | 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)', 73 | 'Topic :: Multimedia :: Graphics :: Capture', 74 | 'Topic :: Scientific/Engineering :: Image Recognition', 75 | 'Topic :: Software Development :: Libraries :: Java Libraries', 76 | 'Topic :: Software Development :: Libraries :: Python Modules', 77 | 'Topic :: Utilities', 78 | 'Programming Language :: Python :: 3.6', 79 | 'Programming Language :: Python :: 3.7', 80 | 'Programming Language :: Python :: 3.8', 81 | 'Programming Language :: Python :: 3.9', 82 | 'Programming Language :: Python :: 3.10', 83 | 'Programming Language :: Python :: 3.11', 84 | ], 85 | ) 86 | -------------------------------------------------------------------------------- /test/barcodes/AZTEC-easy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlenski/python-zxing/1fcabc864cc62142f2744e1bf43e6ffb7d6e309b/test/barcodes/AZTEC-easy.jpg -------------------------------------------------------------------------------- /test/barcodes/AZTEC-utf8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlenski/python-zxing/1fcabc864cc62142f2744e1bf43e6ffb7d6e309b/test/barcodes/AZTEC-utf8.png -------------------------------------------------------------------------------- /test/barcodes/CODE_128-easy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlenski/python-zxing/1fcabc864cc62142f2744e1bf43e6ffb7d6e309b/test/barcodes/CODE_128-easy.jpg -------------------------------------------------------------------------------- /test/barcodes/PDF_417-easy.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlenski/python-zxing/1fcabc864cc62142f2744e1bf43e6ffb7d6e309b/test/barcodes/PDF_417-easy.bmp -------------------------------------------------------------------------------- /test/barcodes/QR CODE (¡filenáme törture test! 😉).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlenski/python-zxing/1fcabc864cc62142f2744e1bf43e6ffb7d6e309b/test/barcodes/QR CODE (¡filenáme törture test! 😉).png -------------------------------------------------------------------------------- /test/barcodes/QR_CODE-easy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlenski/python-zxing/1fcabc864cc62142f2744e1bf43e6ffb7d6e309b/test/barcodes/QR_CODE-easy.png -------------------------------------------------------------------------------- /test/barcodes/QR_CODE-fun-with-whitespace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlenski/python-zxing/1fcabc864cc62142f2744e1bf43e6ffb7d6e309b/test/barcodes/QR_CODE-fun-with-whitespace.png -------------------------------------------------------------------------------- /test/barcodes/QR_CODE-png-but-wrong-extension.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlenski/python-zxing/1fcabc864cc62142f2744e1bf43e6ffb7d6e309b/test/barcodes/QR_CODE-png-but-wrong-extension.bmp -------------------------------------------------------------------------------- /test/barcodes/QR_CODE-screen_scraping_torture_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlenski/python-zxing/1fcabc864cc62142f2744e1bf43e6ffb7d6e309b/test/barcodes/QR_CODE-screen_scraping_torture_test.png -------------------------------------------------------------------------------- /test/barcodes/bad_format.png: -------------------------------------------------------------------------------- 1 | This is not an image file format 2 | -------------------------------------------------------------------------------- /test/barcodes/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlenski/python-zxing/1fcabc864cc62142f2744e1bf43e6ffb7d6e309b/test/barcodes/empty.png -------------------------------------------------------------------------------- /test/test_all.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from itertools import product 4 | from tempfile import mkdtemp 5 | 6 | from PIL import Image 7 | 8 | from nose2.tools.decorators import with_setup 9 | from nose2.tools.such import helper 10 | from nose2.tools import params 11 | import unittest 12 | 13 | import zxing 14 | 15 | test_barcode_dir = os.path.join(os.path.dirname(__file__), 'barcodes') 16 | 17 | test_barcodes = [ 18 | ('QR_CODE-easy.png', 'QR_CODE', 'This should be QR_CODE'), 19 | ('CODE_128-easy.jpg', 'CODE_128', 'This should be CODE_128'), 20 | ('PDF_417-easy.bmp', 'PDF_417', 'This should be PDF_417'), 21 | ('AZTEC-easy.jpg', 'AZTEC', 'This should be AZTEC'), 22 | ('AZTEC-utf8.png', 'AZTEC', 'L’état, c’est moi'), 23 | ('QR CODE (¡filenáme törture test! 😉).png', 'QR_CODE', 'This should be QR_CODE'), 24 | ('QR_CODE-png-but-wrong-extension.bmp', 'QR_CODE', 'This should be QR_CODE'), 25 | ('QR_CODE-fun-with-whitespace.png', 'QR_CODE', '\n\r\t\r\r\r\n '), 26 | ('QR_CODE-screen_scraping_torture_test.png', 'QR_CODE', '\n\\n¡Atención ☹! UTF-8 characters,\n\r embedded newlines,\r &&am&p;& trailing whitespace\t \r '), 27 | ] 28 | 29 | test_non_barcodes = [ 30 | ('empty.png', None, None), 31 | ] 32 | 33 | test_valid_images = test_barcodes + test_non_barcodes 34 | 35 | test_reader = None 36 | 37 | 38 | def setup_reader(): 39 | global test_reader 40 | if test_reader is None: 41 | test_reader = zxing.BarCodeReader() 42 | 43 | 44 | @with_setup(setup_reader) 45 | def test_version(): 46 | global test_reader 47 | assert test_reader.zxing_version is not None 48 | assert '.'.join(map(str, test_reader.zxing_version_info)) == test_reader.zxing_version 49 | 50 | 51 | @with_setup(setup_reader) 52 | def _check_decoding(filename, expected_format, expected_raw, extra={}, as_Image=False): 53 | global test_reader 54 | if (3, 5, 0) <= test_reader.zxing_version_info < (3, 5, 3) and expected_format == 'PDF_417': 55 | # See https://github.com/zxing/zxing/issues/1682 and https://github.com/zxing/zxing/issues/1683 56 | raise unittest.SkipTest("ZXing v{} CommandLineRunner is broken for combination of {} barcode format and --raw option".format( 57 | test_reader.zxing_version, expected_format)) 58 | path = os.path.join(test_barcode_dir, filename) 59 | what = Image.open(path) if as_Image else path 60 | logging.debug('Trying to parse {}, expecting {!r}.'.format(path, expected_raw)) 61 | dec = test_reader.decode(what, pure_barcode=True, **extra) 62 | if expected_raw is None: 63 | assert dec.raw is None, ( 64 | 'Expected failure, but got result in {} format'.format(dec.format)) 65 | else: 66 | assert dec.raw == expected_raw, ( 67 | 'Expected {!r} but got {!r}'.format(expected_raw, dec.raw)) 68 | assert dec.format == expected_format, ( 69 | 'Expected {!r} but got {!r}'.format(expected_format, dec.format)) 70 | if as_Image: 71 | assert not os.path.exists(dec.path), ( 72 | 'Expected temporary file {!r} to be deleted, but it still exists'.format(dec.path)) 73 | 74 | 75 | def test_decoding(): 76 | global test_reader 77 | yield from ((_check_decoding, filename, expected_format, expected_raw) for filename, expected_format, expected_raw in test_valid_images) 78 | 79 | 80 | def test_decoding_from_Image(): 81 | global test_reader 82 | yield from ((_check_decoding, filename, expected_format, expected_raw, {}, True) for filename, expected_format, expected_raw in test_valid_images) 83 | 84 | 85 | def test_possible_formats(): 86 | yield from ((_check_decoding, filename, expected_format, expected_raw, dict(possible_formats=('CODE_93', expected_format, 'DATA_MATRIX'))) 87 | for filename, expected_format, expected_raw in test_barcodes) 88 | 89 | 90 | @with_setup(setup_reader) 91 | def test_decoding_multiple(): 92 | global test_reader 93 | # See https://github.com/zxing/zxing/issues/1682 and https://github.com/zxing/zxing/issues/1683 94 | _tvi = [x for x in test_valid_images if not ((3, 5, 0) <= test_reader.zxing_version_info < (3, 5, 3) and x[1] == 'PDF_417')] 95 | filenames = [os.path.join(test_barcode_dir, filename) for filename, expected_format, expected_raw in _tvi] 96 | for dec, (filename, expected_format, expected_raw) in zip(test_reader.decode(filenames, pure_barcode=True), _tvi): 97 | assert dec.raw == expected_raw, ( 98 | '{}: Expected {!r} but got {!r}'.format(filename, expected_raw, dec.parsed)) 99 | assert dec.format == expected_format, ( 100 | '{}: Expected {!r} but got {!r}'.format(filename, expected_format, dec.format)) 101 | 102 | 103 | @params(*product((False, True), repeat=2)) 104 | def test_parsing(with_raw_bits, with_netloc): 105 | stdout = (""" 106 | file://""") + ("NETWORK_SHARE" if with_netloc else "") + ("""/tmp/default%20file.png (format: FAKE_DATA, type: TEXT): 107 | Raw result: 108 | Élan|\tthe barcode is taking off 109 | Parsed result: 110 | Élan 111 | \tthe barcode is taking off""") + (""" 112 | Raw bits: 113 | f00f00cafe""" if with_raw_bits else "") + (""" 114 | Found 4 result points: 115 | Point 0: (24.0,18.0) 116 | Point 1: (21.0,196.0) 117 | Point 2: (201.0,198.0) 118 | Point 3: (205.23952,21.0) 119 | """) 120 | dec = zxing.BarCode.parse(stdout.encode()) 121 | assert dec.uri == 'file://' + ("NETWORK_SHARE" if with_netloc else "") + '/tmp/default%20file.png' 122 | assert dec.path == (None if with_netloc else '/tmp/default file.png') 123 | assert dec.format == 'FAKE_DATA' 124 | assert dec.type == 'TEXT' 125 | assert dec.raw == 'Élan|\tthe barcode is taking off' 126 | assert dec.raw_bits == (bytes.fromhex('f00f00cafe') if with_raw_bits else b'') 127 | assert dec.parsed == 'Élan\n\tthe barcode is taking off' 128 | assert dec.points == [(24.0, 18.0), (21.0, 196.0), (201.0, 198.0), (205.23952, 21.0)] 129 | r = repr(dec) 130 | assert r.startswith('BarCode(') and r.endswith(')') 131 | 132 | 133 | def test_parsing_not_found(): 134 | stdout = "file:///tmp/some%5ffile%5fwithout%5fbarcode.png: No barcode found\n" 135 | dec = zxing.BarCode.parse(stdout.encode()) 136 | assert dec.uri == 'file:///tmp/some%5ffile%5fwithout%5fbarcode.png' 137 | assert dec.path == '/tmp/some_file_without_barcode.png' 138 | assert dec.format is None 139 | assert dec.type is None 140 | assert dec.raw is None 141 | assert dec.raw_bits is None 142 | assert dec.parsed is None 143 | assert dec.points is None 144 | assert bool(dec) is False 145 | r = repr(dec) 146 | assert r.startswith('BarCode(') and r.endswith(')') 147 | 148 | 149 | def test_wrong_formats(): 150 | all_test_formats = {fmt for fn, fmt, raw in test_barcodes} 151 | yield from ((_check_decoding, filename, expected_format, None, dict(possible_formats=all_test_formats - {expected_format})) 152 | for filename, expected_format, expected_raw in test_barcodes) 153 | 154 | 155 | def test_bad_java(): 156 | test_reader = zxing.BarCodeReader(java=os.devnull) 157 | with helper.assertRaises(zxing.BarCodeReaderException): 158 | test_reader.decode(test_barcodes[0][0]) 159 | 160 | 161 | def test_bad_classpath(): 162 | with helper.assertRaises(zxing.BarCodeReaderException): 163 | test_reader = zxing.BarCodeReader(classpath=mkdtemp()) 164 | 165 | 166 | def test_wrong_JAVA_HOME(): 167 | saved_JAVA_HOME = os.environ.get('JAVA_HOME') 168 | try: 169 | os.environ['JAVA_HOME'] = '/non-existent/path/to/java/stuff' 170 | test_reader = zxing.BarCodeReader() 171 | with helper.assertRaises(zxing.BarCodeReaderException): 172 | test_reader.decode(test_barcodes[0][0]) 173 | finally: 174 | if saved_JAVA_HOME is not None: 175 | os.environ['JAVA_HOME'] = saved_JAVA_HOME 176 | 177 | 178 | @with_setup(setup_reader) 179 | def test_nonexistent_file_error(): 180 | global test_reader 181 | with helper.assertRaises(zxing.BarCodeReaderException): 182 | test_reader.decode(os.path.join(test_barcode_dir, 'nonexistent.png')) 183 | 184 | 185 | @with_setup(setup_reader) 186 | def test_bad_file_format_error(): 187 | global test_reader 188 | with helper.assertRaises(zxing.BarCodeReaderException): 189 | test_reader.decode(os.path.join(test_barcode_dir, 'bad_format.png')) 190 | 191 | 192 | def test_data_uris(): 193 | def _check_data_uri(uri, contents, suffix): 194 | fobj = zxing.data_uri_to_fobj(uri) 195 | assert fobj.getvalue() == contents 196 | assert fobj.name.endswith(suffix) 197 | 198 | yield from ((_check_data_uri, uri, contents, suffix) for (uri, contents, suffix) in ( 199 | ('data:image/png,ABCD', b'ABCD', '.png'), 200 | ('data:image/jpeg;base64,3q2+7w==', bytes.fromhex('deadbeef'), '.jpeg'), 201 | ('data:application/binary,%f1%f2%f3', bytes.fromhex('f1f2f3'), '.binary'), 202 | )) 203 | -------------------------------------------------------------------------------- /zxing/__init__.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # 3 | # zxing.py -- a quick and dirty wrapper for zxing for python 4 | # 5 | # this allows you to send images and get back data from the ZXing 6 | # library: http://code.google.com/p/zxing/ 7 | # 8 | 9 | import glob 10 | import os 11 | import pathlib 12 | import re 13 | import subprocess as sp 14 | import sys 15 | import urllib.parse 16 | import zipfile 17 | from base64 import b64decode 18 | from enum import Enum 19 | from io import BytesIO, IOBase 20 | from itertools import chain 21 | 22 | try: 23 | from PIL.Image import Image 24 | from tempfile import NamedTemporaryFile 25 | have_pil = True 26 | except ImportError: 27 | have_pil = None 28 | 29 | from .version import __version__ # noqa: F401 30 | 31 | 32 | def file_uri_to_path(s): 33 | uri = urllib.parse.urlparse(s) 34 | if (uri.scheme, uri.netloc, uri.query, uri.fragment) != ('file', '', '', ''): 35 | raise ValueError(uri) 36 | return urllib.parse.unquote_plus(uri.path) 37 | 38 | 39 | def data_uri_to_fobj(s): 40 | r = urllib.parse.urlparse(s) 41 | if r.scheme == 'data' and not r.netloc: 42 | mime, *rest = r.path.split(',', 1) 43 | if rest: 44 | if mime.endswith(';base64') and rest: 45 | mime = mime[:-7] 46 | data = b64decode(rest[0]) 47 | else: 48 | data = urllib.parse.unquote_to_bytes(rest[0]) 49 | ff = BytesIO(data) 50 | ff.name = f'data_uri_{len(data)}_bytes.{mime.split("/")[-1]}' 51 | return ff 52 | raise ValueError("Cannot handle URIs other than data:MIMETYPE[;base64],DATA") 53 | 54 | 55 | class BarCodeReaderException(Exception): 56 | def __init__(self, message, filename=None): 57 | self.message, self.filename = message, filename 58 | super().__init__(message, filename) 59 | 60 | 61 | class BarCodeReader(object): 62 | cls = "com.google.zxing.client.j2se.CommandLineRunner" 63 | classpath_sep = ';' if os.name == 'nt' else ':' # https://stackoverflow.com/a/60211688 64 | 65 | def __init__(self, classpath=None, java=None): 66 | self.java = java or 'java' 67 | self.zxing_version = self.zxing_version_info = None 68 | if classpath: 69 | self.classpath = classpath if isinstance(classpath, str) else self.classpath_sep.join(classpath) 70 | elif "ZXING_CLASSPATH" in os.environ: 71 | self.classpath = os.environ.get("ZXING_CLASSPATH", "") 72 | else: 73 | self.classpath = os.path.join(os.path.dirname(__file__), 'java', '*') 74 | 75 | for fn in chain.from_iterable(glob.glob(cp) for cp in self.classpath.split(self.classpath_sep)): 76 | if os.path.basename(fn) == 'core.jar': 77 | self.core_jar = fn 78 | with zipfile.ZipFile(self.core_jar) as c: 79 | for line in c.open('META-INF/MANIFEST.MF'): 80 | if line.startswith(b'Bundle-Version: '): 81 | self.zxing_version = line.split(b' ', 1)[1].strip().decode() 82 | self.zxing_version_info = tuple(int(n) for n in self.zxing_version.split('.')) 83 | break 84 | return 85 | raise BarCodeReaderException("Java JARs not found in classpath (%s)" % self.classpath, self.classpath) 86 | 87 | def decode(self, filenames, try_harder=False, possible_formats=None, pure_barcode=False, products_only=False): 88 | possible_formats = (possible_formats,) if isinstance(possible_formats, str) else possible_formats 89 | 90 | if isinstance(filenames, (str, IOBase, Image) if have_pil else (str, IOBase)): 91 | one_file = True 92 | filenames = filenames, 93 | else: 94 | one_file = False 95 | 96 | file_uris = [] 97 | temp_files = [] 98 | for fn_or_im in filenames: 99 | if have_pil and isinstance(fn_or_im, Image): 100 | tf = NamedTemporaryFile(prefix='PIL_image_', suffix='.png') 101 | temp_files.append(tf) 102 | fn_or_im.save(tf, compresslevel=0) 103 | tf.flush() 104 | fn = tf.name 105 | elif isinstance(fn_or_im, IOBase): 106 | tf = NamedTemporaryFile(prefix='temp_', suffix=os.path.splitext(getattr(fn_or_im, 'name', ''))[1]) 107 | temp_files.append(tf) 108 | tf.write(fn_or_im.read()) 109 | tf.flush() 110 | fn = tf.name 111 | else: 112 | fn = fn_or_im 113 | file_uris.append(pathlib.Path(fn).absolute().as_uri()) 114 | 115 | cmd = [self.java, '-Djava.awt.headless=true', '-cp', self.classpath, self.cls] + file_uris 116 | if self.zxing_version_info and self.zxing_version_info >= (3, 5, 3): 117 | # The --raw option was added in 3.5.0, but broken for certain barcode types (PDF_417 and maybe others) until 3.5.3 118 | # See https://github.com/zxing/zxing/issues/1682 and https://github.com/zxing/zxing/issues/1683 119 | cmd.append('--raw') 120 | if try_harder: 121 | cmd.append('--try_harder') 122 | if pure_barcode: 123 | cmd.append('--pure_barcode') 124 | if products_only: 125 | cmd.append('--products_only') 126 | if possible_formats: 127 | for pf in possible_formats: 128 | cmd += ['--possible_formats', pf] 129 | 130 | try: 131 | p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.STDOUT, universal_newlines=False) 132 | except OSError as e: 133 | raise BarCodeReaderException("Could not execute specified Java binary", self.java) from e 134 | else: 135 | stdout, stderr = p.communicate() 136 | finally: 137 | for tf in temp_files: 138 | tf.close() 139 | 140 | if stdout.startswith((b'Error: Could not find or load main class com.google.zxing.client.j2se.CommandLineRunner', 141 | b'Exception in thread "main" java.lang.NoClassDefFoundError:')): 142 | raise BarCodeReaderException("Java JARs not found in classpath (%s)" % self.classpath, self.classpath) 143 | elif stdout.startswith((b'''Exception in thread "main" javax.imageio.IIOException: Can't get input stream from URL!''', 144 | b'''Exception in thread "main" java.util.concurrent.ExecutionException: javax.imageio.IIOException: Can't get input stream from URL!''')): # noqa: E501 145 | # Find the line that looks like: "Caused by: java.io.FileNotFoundException: $FILENAME ({No such file or directory,Permission denied,*)" 146 | fn, err = next((map(bytes.decode, l[42:].rsplit(b' (', 1)) for l in stdout.splitlines() 147 | if l.startswith(b"Caused by: java.io.FileNotFoundException: ")), ('', '')) 148 | if err == 'No such file or directory)': 149 | err = FileNotFoundError(fn) 150 | elif err == 'Permission denied)': 151 | err = PermissionError(fn) 152 | else: 153 | err = OSError(err[:-1]) 154 | raise BarCodeReaderException("Java library could not read image", fn) from err 155 | elif stdout.startswith(b'''Exception in thread "main" java.io.IOException: Could not load '''): 156 | # First line ends with file:// URI 157 | fn = file_uri_to_path(stdout.splitlines()[0][63:].decode()) 158 | raise BarCodeReaderException("Java library could not read image (is it in a supported format?)", fn) 159 | elif stdout.startswith(b'''Exception '''): 160 | raise BarCodeReaderException("Unknown Java exception", self.java) from sp.CalledProcessError(0, cmd, stdout) 161 | elif stdout.startswith(b'''The operation couldn't be completed. Unable to locate a Java Runtime.'''): 162 | raise BarCodeReaderException("Unable to locate Java runtime (check JAVA_HOME variable and other configuration)", self.java) from sp.CalledProcessError(p.returncode, cmd, stdout) 163 | elif p.returncode: 164 | raise BarCodeReaderException("Unexpected Java subprocess return code", self.java) from sp.CalledProcessError(p.returncode, cmd, stdout) 165 | 166 | file_results = [] 167 | for line in stdout.splitlines(True): 168 | if line.startswith((b'file://', b'Exception')): 169 | file_results.append(line) 170 | else: 171 | file_results[-1] += line 172 | codes = [BarCode.parse(result) for result in file_results] 173 | 174 | if one_file: 175 | return codes[0] 176 | else: 177 | # zxing (insanely) randomly reorders the output blocks, so we have to put them back in the 178 | # expected order, based on their URIs 179 | d = {c.uri: c for c in codes} 180 | return [d[f] for f in file_uris] 181 | 182 | 183 | class CLROutputBlock(Enum): 184 | UNKNOWN = 0 185 | RAW = 1 186 | PARSED = 2 187 | POINTS = 3 188 | RAW_BITS = 4 189 | 190 | 191 | class BarCode(object): 192 | @classmethod 193 | def parse(cls, zxing_output): 194 | block = CLROutputBlock.UNKNOWN 195 | uri = format = type = None 196 | raw = parsed = raw_bits = b'' 197 | points = [] 198 | 199 | for l in zxing_output.splitlines(True): 200 | if block == CLROutputBlock.UNKNOWN: 201 | if l.strip().endswith(b': No barcode found'): 202 | return cls(l.rsplit(b':', 1)[0].decode(), None, None, None, None, None) 203 | m = re.match(rb"(\S+) \(format:\s*([^,]+),\s*type:\s*([^)]+)\)", l) 204 | if m: 205 | uri, format, type = m.group(1).decode(), m.group(2).decode(), m.group(3).decode() 206 | elif l.startswith(b"Raw result:"): 207 | block = CLROutputBlock.RAW 208 | elif block == CLROutputBlock.RAW: 209 | if l.startswith(b"Parsed result:"): 210 | block = CLROutputBlock.PARSED 211 | else: 212 | raw += l 213 | elif block == CLROutputBlock.PARSED: 214 | if l.startswith(b"Raw bits:"): 215 | block = CLROutputBlock.RAW_BITS 216 | elif re.match(rb"Found\s+\d+\s+result\s+points?", l): 217 | block = CLROutputBlock.POINTS 218 | else: 219 | parsed += l 220 | elif block == CLROutputBlock.RAW_BITS: 221 | if re.match(rb"Found\s+\d+\s+result\s+points?", l): 222 | block = CLROutputBlock.POINTS 223 | else: 224 | raw_bits += l 225 | elif block == CLROutputBlock.POINTS: 226 | m = re.match(rb"\s*Point\s*\d+:\s*\(([\d.]+),([\d.]+)\)", l) 227 | if m: 228 | points.append((float(m.group(1)), float(m.group(2)))) 229 | 230 | parsed = parsed[:-1].decode() 231 | raw = raw[:-1].decode() 232 | raw_bits = bytes.fromhex(raw_bits[:-1].decode()) 233 | return cls(uri, format, type, raw, parsed, raw_bits, points) 234 | 235 | def __bool__(self): 236 | return bool(self.raw) 237 | 238 | def __init__(self, uri, format=None, type=None, raw=None, parsed=None, raw_bits=None, points=None): 239 | self.raw = raw 240 | self.parsed = parsed 241 | self.raw_bits = raw_bits 242 | self.uri = uri 243 | self.format = format 244 | self.type = type 245 | self.points = points 246 | 247 | @property 248 | def path(self): 249 | try: 250 | return file_uri_to_path(self.uri) 251 | except ValueError: 252 | pass 253 | 254 | def __repr__(self): 255 | return '{}(raw={!r}, parsed={!r}, raw_bits={!r}, {}={!r}, format={!r}, type={!r}, points={!r})'.format( 256 | self.__class__.__name__, self.raw, self.parsed, self.raw_bits.hex() if self.raw_bits else None, 257 | 'path' if self.path else 'uri', self.path or self.uri, 258 | self.format, self.type, self.points) 259 | -------------------------------------------------------------------------------- /zxing/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import csv 3 | from sys import stdout, stdin 4 | 5 | from . import BarCodeReader, BarCodeReaderException, data_uri_to_fobj 6 | from .version import __version__ 7 | 8 | 9 | class ErrorDeferredArgumentParser(argparse.ArgumentParser): 10 | def __init__(self, *args, **kwargs): 11 | self._errors = [] 12 | super().__init__(*args, **kwargs) 13 | 14 | def error(self, message): 15 | self._errors.append(message) 16 | 17 | def handle_errors(self): 18 | for e in self._errors: 19 | super().error(e) 20 | 21 | 22 | def main(): 23 | p = ErrorDeferredArgumentParser() 24 | p.add_argument('-c', '--csv', action='store_true') 25 | p.add_argument('--try-harder', action='store_true') 26 | p.add_argument('--pure-barcode', action='store_true') 27 | p.add_argument('image', nargs='+', help='File path or data: URI of an image containing a barcode') 28 | p.add_argument('-P', '--classpath', help=argparse.SUPPRESS) 29 | p.add_argument('-J', '--java', help=argparse.SUPPRESS) 30 | p.add_argument('-V', '--version', action='store_true') 31 | args = p.parse_args() 32 | if p._errors and not args.version: 33 | p.handle_errors() 34 | 35 | bcr = BarCodeReader(args.classpath, args.java) 36 | 37 | if args.version: 38 | p.exit(0, '%s v%s\n' 39 | 'using Java ZXing library version v%s\n' % (p.prog, __version__, bcr.zxing_version)) 40 | 41 | if args.csv: 42 | wr = csv.writer(stdout) 43 | wr.writerow(('Filename', 'Format', 'Type', 'Raw', 'Parsed')) 44 | 45 | for fn in args.image: 46 | if fn == '-': 47 | ff = stdin.buffer 48 | fn = ff.name 49 | elif ':' in fn: 50 | try: 51 | ff = data_uri_to_fobj(fn) 52 | fn = ff.name 53 | except ValueError as exc: 54 | p.error(exc.args[0]) 55 | else: 56 | ff = fn 57 | 58 | bc = None 59 | try: 60 | bc = bcr.decode(ff, try_harder=args.try_harder, pure_barcode=args.pure_barcode) 61 | except BarCodeReaderException as e: 62 | p.error(e.message + ((': ' + e.filename) if e.filename else '') + (('\n\tCaused by: ' + repr(e.__cause__) if e.__cause__ else ''))) 63 | 64 | if args.csv: 65 | wr.writerow((fn, bc.format, bc.type, bc.raw, bc.parsed) if bc else (fn, 'ERROR', None, None, None)) 66 | else: 67 | print("%s\n%s" % (fn, '=' * len(fn))) 68 | if not bc: 69 | print(" ERROR: Failed to decode barcode (using Java ZXing library v%s)." % bcr.zxing_version) 70 | else: 71 | print(" Decoded %s barcode in %s format." % (bc.type, bc.format)) 72 | print(" Raw text: %r" % bc.raw) 73 | print(" Parsed text: %r" % bc.parsed) 74 | print(" Raw bits: %r\n" % bc.raw_bits.hex()) 75 | 76 | 77 | if __name__=='__main__': 78 | main() 79 | -------------------------------------------------------------------------------- /zxing/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.3" 2 | --------------------------------------------------------------------------------