update pyzbar shim

This commit is contained in:
github-actions[bot]
2026-03-28 05:52:58 +00:00
commit f98575b114
8 changed files with 773 additions and 0 deletions

20
pyzbar/pyproject.toml Normal file
View File

@@ -0,0 +1,20 @@
[build-system]
requires = ["setuptools>=64", "wheel", 'tomli; python_version < "3.11"']
build-backend = "setuptools.build_meta"
[project]
name = "pyzbar"
version = "0.1.9"
description = "pyzbar barcode reader with bundled zbar library (pre-built)"
requires-python = ">=3.8"
[tool.setuptools.packages.find]
include = ["pyzbar*"]
[tool.setuptools.package-data]
pyzbar = ["install/**/*", "*.so"]
[tool.shim]
repo_url = "https://github.com/greatgitsby/op-dependencies"
tag = "pyzbar/v0.1.9"
datadir = "install"

22
pyzbar/pyzbar/__init__.py Normal file
View File

@@ -0,0 +1,22 @@
"""Read one-dimensional barcodes and QR codes from Python 2 and 3."""
import os
__version__ = '0.1.9'
DIR = os.path.join(os.path.dirname(__file__), "install")
LIB_DIR = os.path.join(DIR, "lib")
def smoketest():
import platform
if platform.system() == "Darwin":
lib_name = "libzbar.dylib"
else:
lib_name = "libzbar.so"
lib_path = os.path.join(LIB_DIR, lib_name)
assert os.path.isfile(lib_path), f"{lib_name} not found at {lib_path}"
from .pyzbar import decode
result = decode((b'\x00', 1, 1))
assert isinstance(result, list), "decode() did not return a list"

View File

@@ -0,0 +1,70 @@
from collections import namedtuple
from itertools import chain
from operator import itemgetter
__all__ = ['bounding_box', 'convex_hull', 'Point', 'Rect']
Point = namedtuple('Point', ['x', 'y'])
Rect = namedtuple('Rect', ['left', 'top', 'width', 'height'])
def bounding_box(locations):
"""Computes the bounding box of an iterable of (x, y) coordinates.
Args:
locations: iterable of (x, y) tuples.
Returns:
`Rect`: Coordinates of the bounding box.
"""
x_values = list(map(itemgetter(0), locations))
x_min, x_max = min(x_values), max(x_values)
y_values = list(map(itemgetter(1), locations))
y_min, y_max = min(y_values), max(y_values)
return Rect(x_min, y_min, x_max - x_min, y_max - y_min)
def convex_hull(points):
"""Computes the convex hull of an iterable of (x, y) coordinates.
Args:
points: iterable of (x, y) tuples.
Returns:
`list`: instances of `Point` - vertices of the convex hull in
counter-clockwise order, starting from the vertex with the
lexicographically smallest coordinates.
Andrew's monotone chain algorithm. O(n log n) complexity.
https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
"""
def is_not_clockwise(p0, p1, p2):
return 0 <= (
(p1[0] - p0[0]) * (p2[1] - p0[1]) -
(p1[1] - p0[1]) * (p2[0] - p0[0])
)
def go(points_):
res = []
for p in points_:
while 1 < len(res) and is_not_clockwise(res[-2], res[-1], p):
res.pop()
res.append(p)
# The last point in each list is the first point in the other list
res.pop()
return res
# Discard duplicates and sort by x then y
points = sorted(set(points))
# Algorithm needs at least two points
hull = (
points if len(points) < 2 else chain(go(points), go(reversed(points)))
)
return list(map(Point._make, hull))

236
pyzbar/pyzbar/pyzbar.py Normal file
View File

@@ -0,0 +1,236 @@
from collections import namedtuple
from contextlib import contextmanager
from ctypes import cast, c_void_p, string_at
from .locations import bounding_box, convex_hull, Point, Rect
from .pyzbar_error import PyZbarError
from .wrapper import (
zbar_image_scanner_set_config,
zbar_image_scanner_create, zbar_image_scanner_destroy,
zbar_image_create, zbar_image_destroy, zbar_image_set_format,
zbar_image_set_size, zbar_image_set_data, zbar_scan_image,
zbar_image_first_symbol, zbar_symbol_get_data_length,
zbar_symbol_get_data, zbar_symbol_get_orientation,
zbar_symbol_get_loc_size, zbar_symbol_get_loc_x, zbar_symbol_get_loc_y,
zbar_symbol_get_quality, zbar_symbol_next, ZBarConfig, ZBarOrientation,
ZBarSymbol, EXTERNAL_DEPENDENCIES,
)
__all__ = [
'decode', 'Point', 'Rect', 'Decoded', 'ZBarSymbol', 'EXTERNAL_DEPENDENCIES', 'ORIENTATION_AVAILABLE'
]
ORIENTATION_AVAILABLE = zbar_symbol_get_orientation is not None
Decoded = namedtuple('Decoded', 'data type rect polygon quality orientation')
# ZBar's magic 'fourcc' numbers that represent image formats
_FOURCC = {
'L800': 808466521,
'GRAY': 1497715271
}
_RANGEFN = getattr(globals(), 'xrange', range)
@contextmanager
def _image():
"""A context manager for `zbar_image`, created and destoyed by
`zbar_image_create` and `zbar_image_destroy`.
Yields:
POINTER(zbar_image): The created image
Raises:
PyZbarError: If the image could not be created.
"""
image = zbar_image_create()
if not image:
raise PyZbarError('Could not create zbar image')
else:
try:
yield image
finally:
zbar_image_destroy(image)
@contextmanager
def _image_scanner():
"""A context manager for `zbar_image_scanner`, created and destroyed by
`zbar_image_scanner_create` and `zbar_image_scanner_destroy`.
Yields:
POINTER(zbar_image_scanner): The created scanner
Raises:
PyZbarError: If the decoder could not be created.
"""
scanner = zbar_image_scanner_create()
if not scanner:
raise PyZbarError('Could not create image scanner')
else:
try:
yield scanner
finally:
zbar_image_scanner_destroy(scanner)
def _symbols_for_image(image):
"""Generator of symbols.
Args:
image: `zbar_image`
Yields:
POINTER(zbar_symbol): Symbol
"""
symbol = zbar_image_first_symbol(image)
while symbol:
yield symbol
symbol = zbar_symbol_next(symbol)
def _decode_symbols(symbols):
"""Generator of decoded symbol information.
Args:
symbols: iterable of instances of `POINTER(zbar_symbol)`
Yields:
Decoded: decoded symbol
"""
for symbol in symbols:
data = string_at(
zbar_symbol_get_data(symbol),
zbar_symbol_get_data_length(symbol)
)
# The 'type' int should be a value in the ZBarSymbol enumeration
try:
symbol_type = ZBarSymbol(symbol.contents.type)
except ValueError:
# This release of zbar supports a type that pyzbar does not know about
symbol_type = "Unrecognised type [{0}]".format(symbol.contents.type)
else:
symbol_type = symbol_type.name
quality = zbar_symbol_get_quality(symbol)
polygon = convex_hull(
(
zbar_symbol_get_loc_x(symbol, index),
zbar_symbol_get_loc_y(symbol, index)
)
for index in _RANGEFN(zbar_symbol_get_loc_size(symbol))
)
if zbar_symbol_get_orientation:
orientation = ZBarOrientation(zbar_symbol_get_orientation(symbol)).name
else:
orientation = None
yield Decoded(
data=data,
type=symbol_type,
rect=bounding_box(polygon),
polygon=polygon,
orientation=orientation,
quality=quality,
)
def _pixel_data(image):
"""Returns (pixels, width, height)
Returns:
:obj: `tuple` (pixels, width, height)
"""
# Test for PIL.Image, numpy.ndarray, and imageio.core.util without
# requiring that cv2, PIL, or imageio are installed.
image_type = str(type(image))
if 'PIL.' in image_type:
if 'L' != image.mode:
image = image.convert('L')
pixels = image.tobytes()
width, height = image.size
elif 'numpy.ndarray' in image_type or 'imageio.core.util' in image_type:
# Different versions of imageio use a subclass of numpy.ndarray
# called either imageio.core.util.Image or imageio.core.util.Array.
if 3 == len(image.shape):
# Take just the first channel
image = image[:, :, 0]
if 'uint8' != str(image.dtype):
image = image.astype('uint8')
try:
pixels = image.tobytes()
except AttributeError:
# `numpy.ndarray.tobytes()` introduced in `numpy` 1.9.0 - use the
# older `tostring` method.
pixels = image.tostring()
height, width = image.shape[:2]
else:
# image should be a tuple (pixels, width, height)
pixels, width, height = image
# Check dimensions
if 0 != len(pixels) % (width * height):
raise PyZbarError(
(
'Inconsistent dimensions: image data of {0} bytes is not '
'divisible by (width x height = {1})'
).format(len(pixels), (width * height))
)
# Compute bits-per-pixel
bpp = 8 * len(pixels) // (width * height)
if 8 != bpp:
raise PyZbarError(
'Unsupported bits-per-pixel [{0}]. Only [8] is supported.'.format(
bpp
)
)
return pixels, width, height
def decode(image, symbols=None):
"""Decodes datamatrix barcodes in `image`.
Args:
image: `numpy.ndarray`, `PIL.Image` or tuple (pixels, width, height)
symbols: iter(ZBarSymbol) the symbol types to decode; if `None`, uses
`zbar`'s default behaviour, which is to decode all symbol types.
Returns:
:obj:`list` of :obj:`Decoded`: The values decoded from barcodes.
"""
pixels, width, height = _pixel_data(image)
results = []
with _image_scanner() as scanner:
if symbols:
# Disable all but the symbols of interest
disable = set(ZBarSymbol).difference(symbols)
for symbol in disable:
zbar_image_scanner_set_config(
scanner, symbol, ZBarConfig.CFG_ENABLE, 0
)
# I think it likely that zbar will detect all symbol types by
# default, in which case enabling the types of interest is
# redundant but it seems sensible to be over-cautious and enable
# them.
for symbol in symbols:
zbar_image_scanner_set_config(
scanner, symbol, ZBarConfig.CFG_ENABLE, 1
)
with _image() as img:
zbar_image_set_format(img, _FOURCC['L800'])
zbar_image_set_size(img, width, height)
zbar_image_set_data(img, cast(pixels, c_void_p), len(pixels), None)
decoded = zbar_scan_image(scanner, img)
if decoded < 0:
raise PyZbarError('Unsupported image format')
else:
results.extend(_decode_symbols(_symbols_for_image(img)))
return results

View File

@@ -0,0 +1,5 @@
__all__ = ["PyZbarError"]
class PyZbarError(Exception):
pass

295
pyzbar/pyzbar/wrapper.py Normal file
View File

@@ -0,0 +1,295 @@
"""Low-level wrapper around zbar's interface
"""
from ctypes import (
c_ubyte, c_char_p, c_int, c_uint, c_ulong, c_void_p, Structure,
CFUNCTYPE, POINTER
)
from enum import IntEnum, unique
from . import zbar_library
__all__ = [
'EXTERNAL_DEPENDENCIES', 'LIBZBAR', 'ZBarConfig', 'ZBarSymbol', 'ZBarOrientation',
'zbar_image_create', 'zbar_image_destroy', 'zbar_image_first_symbol',
'zbar_image_scanner_create', 'zbar_image_scanner_destroy',
'zbar_image_scanner_set_config', 'zbar_image_set_data',
'zbar_image_set_format', 'zbar_image_set_size', 'zbar_scan_image',
'zbar_symbol_get_data_length', 'zbar_symbol_get_data',
'zbar_symbol_get_loc_size', 'zbar_symbol_get_loc_x',
'zbar_symbol_get_loc_y', 'zbar_symbol_next',
'zbar_symbol_get_orientation', 'zbar_symbol_get_quality',
]
# Globals populated in load_libzbar
LIBZBAR = None
"""ctypes.CDLL
"""
EXTERNAL_DEPENDENCIES = []
"""List of instances of ctypes.CDLL. Helpful when freezing.
"""
# Types
c_ubyte_p = POINTER(c_ubyte)
c_uint_p = POINTER(c_uint)
c_ulong_p = POINTER(c_ulong)
"""unsigned char* type
"""
# Defines and enums
@unique
class ZBarSymbol(IntEnum):
NONE = 0 # /**< no symbol decoded */
PARTIAL = 1 # /**< intermediate status */
EAN2 = 2 # /**< GS1 2-digit add-on */
EAN5 = 5 # /**< GS1 5-digit add-on */
EAN8 = 8 # /**< EAN-8 */
UPCE = 9 # /**< UPC-E */
ISBN10 = 10 # /**< ISBN-10 (from EAN-13). @since 0.4 */
UPCA = 12 # /**< UPC-A */
EAN13 = 13 # /**< EAN-13 */
ISBN13 = 14 # /**< ISBN-13 (from EAN-13). @since 0.4 */
COMPOSITE = 15 # /**< EAN/UPC composite */
I25 = 25 # /**< Interleaved 2 of 5. @since 0.4 */
DATABAR = 34 # /**< GS1 DataBar (RSS). @since 0.11 */
DATABAR_EXP = 35 # /**< GS1 DataBar Expanded. @since 0.11 */
CODABAR = 38 # /**< Codabar. @since 0.11 */
CODE39 = 39 # /**< Code 39. @since 0.4 */
PDF417 = 57 # /**< PDF417. @since 0.6 */
QRCODE = 64 # /**< QR Code. @since 0.10 */
SQCODE = 80 # /**< SQ Code. @since 0.20.1 */
CODE93 = 93 # /**< Code 93. @since 0.11 */
CODE128 = 128 # /**< Code 128 */
@unique
class ZBarConfig(IntEnum):
CFG_ENABLE = 0 # /**< enable symbology/feature */
CFG_ADD_CHECK = 1 # /**< enable check digit when optional */
CFG_EMIT_CHECK = 2 # /**< return check digit when present */
CFG_ASCII = 3 # /**< enable full ASCII character set */
CFG_NUM = 4 # /**< number of boolean decoder configs */
CFG_MIN_LEN = 0x20 # /**< minimum data length for valid decode */
CFG_MAX_LEN = 0x21 # /**< maximum data length for valid decode */
CFG_UNCERTAINTY = 0x40 # /**< required video consistency frames */
CFG_POSITION = 0x80 # /**< enable scanner to collect position data */
CFG_X_DENSITY = 0x100 # /**< image scanner vertical scan density */
CFG_Y_DENSITY = 0x101 # /**< image scanner horizontal scan density */
@unique
class ZBarOrientation(IntEnum):
UNKNOWN = -1 # /**< unable to determine orientation */
UP = 0 # /**< upright, read left to right */
RIGHT = 1 # /**< sideways, read top to bottom */
DOWN = 2 # /**< upside-down, read right to left */
LEFT = 3 # /**< sideways, read bottom to top */
# Structs
class zbar_image_scanner(Structure):
"""Opaque C++ class with private implementation
"""
pass
class zbar_image(Structure):
"""Opaque C++ class with private implementation
"""
pass
class zbar_symbol(Structure):
"""Opaque C++ class with private implementation
The first item in the structure is an integeger value in the ZBarSymbol
enumeration.
"""
_fields_ = [
('type', c_int),
]
def load_libzbar():
"""Loads the zbar shared library and its dependencies.
Populates the globals LIBZBAR and EXTERNAL_DEPENDENCIES.
"""
global LIBZBAR
global EXTERNAL_DEPENDENCIES
if not LIBZBAR:
libzbar, dependencies = zbar_library.load()
LIBZBAR = libzbar
EXTERNAL_DEPENDENCIES = [LIBZBAR] + dependencies
return LIBZBAR
# Function signatures
def zbar_function(fname, restype, *args):
"""Returns a foreign function exported by `zbar`.
Args:
fname (:obj:`str`): Name of the exported function as string.
restype (:obj:): Return type - one of the `ctypes` primitive C data
types.
*args: Arguments - a sequence of `ctypes` primitive C data types.
Returns:
cddl.CFunctionType: A wrapper around the function.
"""
prototype = CFUNCTYPE(restype, *args)
return prototype((fname, load_libzbar()))
zbar_version = zbar_function(
'zbar_version',
c_int,
c_uint_p, # major,
c_uint_p, # minor
)
zbar_set_verbosity = zbar_function(
'zbar_set_verbosity',
None,
c_int
)
zbar_image_scanner_create = zbar_function(
'zbar_image_scanner_create',
POINTER(zbar_image_scanner)
)
zbar_image_scanner_destroy = zbar_function(
'zbar_image_scanner_destroy',
None,
POINTER(zbar_image_scanner)
)
zbar_parse_config = zbar_function(
'zbar_parse_config',
c_int,
c_char_p, # config_string,
POINTER(c_int), # symbology - values in ZBarSymbol
POINTER(c_int), # config - values in ZBarConfig
POINTER(c_int), # value
)
zbar_image_scanner_set_config = zbar_function(
'zbar_image_scanner_set_config',
c_int,
POINTER(zbar_image_scanner), # scanner
c_int, # symbology - values in ZBarSymbol
c_int, # config - values in ZBarConfig
c_int # value
)
zbar_image_create = zbar_function(
'zbar_image_create',
POINTER(zbar_image)
)
zbar_image_destroy = zbar_function(
'zbar_image_destroy',
None,
POINTER(zbar_image)
)
zbar_image_set_format = zbar_function(
'zbar_image_set_format',
None,
POINTER(zbar_image),
c_uint
)
zbar_image_set_size = zbar_function(
'zbar_image_set_size',
None,
POINTER(zbar_image),
c_uint, # width
c_uint # height
)
zbar_image_set_data = zbar_function(
'zbar_image_set_data',
None,
POINTER(zbar_image),
c_void_p, # data
c_ulong, # raw_image_data_length
c_void_p # A function pointer(!)
)
zbar_scan_image = zbar_function(
'zbar_scan_image',
c_int,
POINTER(zbar_image_scanner),
POINTER(zbar_image)
)
zbar_image_first_symbol = zbar_function(
'zbar_image_first_symbol',
POINTER(zbar_symbol),
POINTER(zbar_image)
)
zbar_symbol_get_data_length = zbar_function(
'zbar_symbol_get_data_length',
c_uint,
POINTER(zbar_symbol)
)
zbar_symbol_get_data = zbar_function(
'zbar_symbol_get_data',
c_ubyte_p,
POINTER(zbar_symbol)
)
zbar_symbol_get_loc_size = zbar_function(
'zbar_symbol_get_loc_size',
c_uint,
POINTER(zbar_symbol)
)
zbar_symbol_get_loc_x = zbar_function(
'zbar_symbol_get_loc_x',
c_int,
POINTER(zbar_symbol),
c_uint
)
zbar_symbol_get_loc_y = zbar_function(
'zbar_symbol_get_loc_y',
c_int,
POINTER(zbar_symbol),
c_uint
)
try:
zbar_symbol_get_orientation = zbar_function(
'zbar_symbol_get_orientation',
c_uint,
POINTER(zbar_symbol)
)
except AttributeError:
# This function not present in the original pre-20
zbar_symbol_get_orientation = None
zbar_symbol_next = zbar_function(
'zbar_symbol_next',
POINTER(zbar_symbol),
POINTER(zbar_symbol)
)
zbar_symbol_get_quality = zbar_function(
'zbar_symbol_get_quality',
c_int,
POINTER(zbar_symbol)
)

View File

@@ -0,0 +1,22 @@
"""Loads the zbar shared library bundled with this package."""
import platform
from ctypes import cdll
from pathlib import Path
def load():
lib_dir = Path(__file__).parent / 'install' / 'lib'
if platform.system() == 'Darwin':
lib_name = 'libzbar.dylib'
else:
lib_name = 'libzbar.so'
lib_path = lib_dir / lib_name
if not lib_path.exists():
raise ImportError(
f'Bundled zbar shared library not found at {lib_path}'
)
return cdll.LoadLibrary(str(lib_path)), []

103
pyzbar/setup.py Normal file
View File

@@ -0,0 +1,103 @@
"""Shim setup.py: downloads pre-built wheels from GitHub Releases at install time."""
import os
import platform
import time
import zipfile
from io import BytesIO
from urllib.error import URLError
from urllib.request import urlopen
try:
import tomllib
except ImportError:
import tomli as tomllib
from setuptools import setup
from setuptools.command.build_py import build_py
_HERE = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(_HERE, "pyproject.toml"), "rb") as _f:
_cfg = tomllib.load(_f)
REPO_URL = _cfg["tool"]["shim"]["repo_url"]
TAG = _cfg["tool"]["shim"]["tag"]
DATADIR = _cfg["tool"]["shim"]["datadir"]
VERSION = _cfg["project"]["version"]
MODULE = _cfg["project"]["name"].replace("-", "_")
PLATFORM_MAP = {
("Linux", "x86_64"): "linux_x86_64",
("Linux", "aarch64"): "linux_aarch64",
("Darwin", "arm64"): "macosx_11_0_arm64",
}
class InstallPrebuilt(build_py):
def run(self):
module_dir = os.path.join(_HERE, MODULE)
data_dir = os.path.join(module_dir, DATADIR)
if not os.path.exists(os.path.join(data_dir, "bin")):
key = (platform.system(), platform.machine())
plat = PLATFORM_MAP.get(key)
if plat is None:
raise RuntimeError(f"unsupported platform: {key}")
whl_name = f"{MODULE}-{VERSION}-py3-none-{plat}.whl"
url = f"{REPO_URL}/releases/download/{TAG}/{whl_name}"
print(f"Downloading {url} ...")
for attempt in range(3):
try:
raw = urlopen(url, timeout=60).read()
break
except (URLError, OSError) as e:
if attempt == 2:
raise
wait = 2 ** attempt
print(f"Download failed ({e}), retrying in {wait}s ...")
time.sleep(wait)
print(f"Extracting {DATADIR} ...")
with zipfile.ZipFile(BytesIO(raw)) as zf:
prefix = f"{MODULE}/{DATADIR}/"
alt_prefix = f"{MODULE}-{VERSION}.data/purelib/{MODULE}/{DATADIR}/"
for info in zf.infolist():
for p in (prefix, alt_prefix):
if info.filename.startswith(p):
rel = info.filename[len(p):]
if not rel:
continue
dest = os.path.join(data_dir, rel)
if info.is_dir():
os.makedirs(dest, exist_ok=True)
else:
os.makedirs(os.path.dirname(dest), exist_ok=True)
with open(dest, "wb") as f:
f.write(zf.read(info))
if info.external_attr >> 16 & 0o111:
os.chmod(dest, 0o755)
break
# Also extract compiled extension modules (.so)
ext_prefix = f"{MODULE}/"
ext_alt_prefix = f"{MODULE}-{VERSION}.data/purelib/{MODULE}/"
for info in zf.infolist():
if not info.filename.endswith('.so'):
continue
for p in (ext_prefix, ext_alt_prefix):
if info.filename.startswith(p):
rel = info.filename[len(p):]
if rel and '/' not in rel:
dest = os.path.join(module_dir, rel)
os.makedirs(os.path.dirname(dest), exist_ok=True)
with open(dest, "wb") as f:
f.write(zf.read(info))
if info.external_attr >> 16 & 0o111:
os.chmod(dest, 0o755)
break
super().run()
setup(cmdclass={"build_py": InstallPrebuilt})