barcodekit 0.1.2__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- barcodekit/__init__.py +201 -0
- barcodekit/_bin/.gitkeep +1 -0
- barcodekit/_bin/barcode-rest.exe +0 -0
- barcodekit/_binary.py +85 -0
- barcodekit/_core.py +478 -0
- barcodekit/_errors.py +64 -0
- barcodekit/_version.py +4 -0
- barcodekit/py.typed +1 -0
- barcodekit-0.1.2.dist-info/METADATA +194 -0
- barcodekit-0.1.2.dist-info/RECORD +16 -0
- barcodekit-0.1.2.dist-info/WHEEL +5 -0
- barcodekit-0.1.2.dist-info/licenses/LICENSE +22 -0
- barcodekit-0.1.2.dist-info/licenses/THIRD_PARTY_NOTICES.md +15 -0
- barcodekit-0.1.2.dist-info/licenses/licenses/barcode-rest-MIT.txt +22 -0
- barcodekit-0.1.2.dist-info/licenses/licenses/boombuler-barcode-MIT.txt +22 -0
- barcodekit-0.1.2.dist-info/licenses/licenses/go-and-x-image-BSD-3-Clause.txt +28 -0
barcodekit/__init__.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Generate barcode PNGs with the local barcode-rest CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._core import BarcodeImage, BarcodeKit
|
|
6
|
+
from ._errors import (
|
|
7
|
+
BarcodeKitBinaryNotFound,
|
|
8
|
+
BarcodeKitCommandError,
|
|
9
|
+
BarcodeKitError,
|
|
10
|
+
BarcodeKitTimeout,
|
|
11
|
+
BarcodeKitUnsupportedPlatform,
|
|
12
|
+
)
|
|
13
|
+
from ._version import __version__
|
|
14
|
+
|
|
15
|
+
_default_kit = BarcodeKit()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate(symbology: str, text: str, **options: object) -> BarcodeImage:
|
|
19
|
+
return _default_kit.generate(symbology, text, **options)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def datamatrix(
|
|
23
|
+
text: str,
|
|
24
|
+
*,
|
|
25
|
+
size: int | None = 256,
|
|
26
|
+
module: int | None = None,
|
|
27
|
+
quiet: int | None = None,
|
|
28
|
+
) -> BarcodeImage:
|
|
29
|
+
return _default_kit.datamatrix(text, size=size, module=module, quiet=quiet)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def qr(
|
|
33
|
+
text: str,
|
|
34
|
+
*,
|
|
35
|
+
size: int | None = 512,
|
|
36
|
+
module: int | None = None,
|
|
37
|
+
quiet: int | None = None,
|
|
38
|
+
level: str = "M",
|
|
39
|
+
) -> BarcodeImage:
|
|
40
|
+
return _default_kit.qr(text, size=size, module=module, quiet=quiet, level=level)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def aztec(
|
|
44
|
+
text: str,
|
|
45
|
+
*,
|
|
46
|
+
size: int | None = 512,
|
|
47
|
+
module: int | None = None,
|
|
48
|
+
quiet: int | None = None,
|
|
49
|
+
) -> BarcodeImage:
|
|
50
|
+
return _default_kit.aztec(text, size=size, module=module, quiet=quiet)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pdf417(
|
|
54
|
+
text: str,
|
|
55
|
+
*,
|
|
56
|
+
module: int | None = None,
|
|
57
|
+
quiet: int | None = None,
|
|
58
|
+
level: int | str = 2,
|
|
59
|
+
) -> BarcodeImage:
|
|
60
|
+
return _default_kit.pdf417(text, module=module, quiet=quiet, level=level)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def code128(
|
|
64
|
+
text: str,
|
|
65
|
+
*,
|
|
66
|
+
module: int | None = None,
|
|
67
|
+
height: int | None = 80,
|
|
68
|
+
quiet: int | None = None,
|
|
69
|
+
label: bool = False,
|
|
70
|
+
) -> BarcodeImage:
|
|
71
|
+
return _default_kit.code128(
|
|
72
|
+
text, module=module, height=height, quiet=quiet, label=label
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def code39(
|
|
77
|
+
text: str,
|
|
78
|
+
*,
|
|
79
|
+
module: int | None = None,
|
|
80
|
+
height: int | None = 80,
|
|
81
|
+
quiet: int | None = None,
|
|
82
|
+
label: bool = False,
|
|
83
|
+
fullascii: bool = False,
|
|
84
|
+
) -> BarcodeImage:
|
|
85
|
+
return _default_kit.code39(
|
|
86
|
+
text,
|
|
87
|
+
module=module,
|
|
88
|
+
height=height,
|
|
89
|
+
quiet=quiet,
|
|
90
|
+
label=label,
|
|
91
|
+
fullascii=fullascii,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def code93(
|
|
96
|
+
text: str,
|
|
97
|
+
*,
|
|
98
|
+
module: int | None = None,
|
|
99
|
+
height: int | None = 80,
|
|
100
|
+
quiet: int | None = None,
|
|
101
|
+
label: bool = False,
|
|
102
|
+
fullascii: bool = False,
|
|
103
|
+
) -> BarcodeImage:
|
|
104
|
+
return _default_kit.code93(
|
|
105
|
+
text,
|
|
106
|
+
module=module,
|
|
107
|
+
height=height,
|
|
108
|
+
quiet=quiet,
|
|
109
|
+
label=label,
|
|
110
|
+
fullascii=fullascii,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def codabar(
|
|
115
|
+
text: str,
|
|
116
|
+
*,
|
|
117
|
+
module: int | None = None,
|
|
118
|
+
height: int | None = 80,
|
|
119
|
+
quiet: int | None = None,
|
|
120
|
+
label: bool = False,
|
|
121
|
+
) -> BarcodeImage:
|
|
122
|
+
return _default_kit.codabar(
|
|
123
|
+
text, module=module, height=height, quiet=quiet, label=label
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def itf(
|
|
128
|
+
text: str,
|
|
129
|
+
*,
|
|
130
|
+
module: int | None = None,
|
|
131
|
+
height: int | None = 80,
|
|
132
|
+
quiet: int | None = None,
|
|
133
|
+
label: bool = False,
|
|
134
|
+
) -> BarcodeImage:
|
|
135
|
+
return _default_kit.itf(
|
|
136
|
+
text, module=module, height=height, quiet=quiet, label=label
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def code25(
|
|
141
|
+
text: str,
|
|
142
|
+
*,
|
|
143
|
+
module: int | None = None,
|
|
144
|
+
height: int | None = 80,
|
|
145
|
+
quiet: int | None = None,
|
|
146
|
+
label: bool = False,
|
|
147
|
+
) -> BarcodeImage:
|
|
148
|
+
return _default_kit.code25(
|
|
149
|
+
text, module=module, height=height, quiet=quiet, label=label
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def ean13(
|
|
154
|
+
text: str,
|
|
155
|
+
*,
|
|
156
|
+
module: int | None = None,
|
|
157
|
+
height: int | None = 80,
|
|
158
|
+
quiet: int | None = None,
|
|
159
|
+
label: bool = False,
|
|
160
|
+
) -> BarcodeImage:
|
|
161
|
+
return _default_kit.ean13(
|
|
162
|
+
text, module=module, height=height, quiet=quiet, label=label
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def ean8(
|
|
167
|
+
text: str,
|
|
168
|
+
*,
|
|
169
|
+
module: int | None = None,
|
|
170
|
+
height: int | None = 80,
|
|
171
|
+
quiet: int | None = None,
|
|
172
|
+
label: bool = False,
|
|
173
|
+
) -> BarcodeImage:
|
|
174
|
+
return _default_kit.ean8(
|
|
175
|
+
text, module=module, height=height, quiet=quiet, label=label
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
__all__ = [
|
|
180
|
+
"BarcodeImage",
|
|
181
|
+
"BarcodeKit",
|
|
182
|
+
"BarcodeKitBinaryNotFound",
|
|
183
|
+
"BarcodeKitCommandError",
|
|
184
|
+
"BarcodeKitError",
|
|
185
|
+
"BarcodeKitTimeout",
|
|
186
|
+
"BarcodeKitUnsupportedPlatform",
|
|
187
|
+
"__version__",
|
|
188
|
+
"aztec",
|
|
189
|
+
"codabar",
|
|
190
|
+
"code25",
|
|
191
|
+
"code39",
|
|
192
|
+
"code93",
|
|
193
|
+
"code128",
|
|
194
|
+
"datamatrix",
|
|
195
|
+
"ean8",
|
|
196
|
+
"ean13",
|
|
197
|
+
"generate",
|
|
198
|
+
"itf",
|
|
199
|
+
"pdf417",
|
|
200
|
+
"qr",
|
|
201
|
+
]
|
barcodekit/_bin/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
Binary file
|
barcodekit/_binary.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Resolve the barcode-rest executable for the current platform."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import stat
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ._errors import BarcodeKitBinaryNotFound, BarcodeKitUnsupportedPlatform
|
|
11
|
+
|
|
12
|
+
_BINARY_ENV = "BARCODEKIT_BINARY"
|
|
13
|
+
_BINARY_DIR = Path(__file__).parent / "_bin"
|
|
14
|
+
_MACHINE_ALIASES = {
|
|
15
|
+
"amd64": "amd64",
|
|
16
|
+
"x86_64": "amd64",
|
|
17
|
+
"arm64": "arm64",
|
|
18
|
+
"aarch64": "arm64",
|
|
19
|
+
}
|
|
20
|
+
_SUPPORTED_PLATFORMS = {
|
|
21
|
+
("windows", "amd64"): "barcode-rest.exe",
|
|
22
|
+
("linux", "amd64"): "barcode-rest",
|
|
23
|
+
("linux", "arm64"): "barcode-rest",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _current_platform() -> tuple[str, str]:
|
|
28
|
+
system = platform.system().lower()
|
|
29
|
+
raw_machine = platform.machine().lower()
|
|
30
|
+
return system, _MACHINE_ALIASES.get(raw_machine, raw_machine)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _bundled_binary_name() -> tuple[str, str]:
|
|
34
|
+
system, machine = _current_platform()
|
|
35
|
+
name = _SUPPORTED_PLATFORMS.get((system, machine))
|
|
36
|
+
if name is None:
|
|
37
|
+
supported = ", ".join(f"{os_name}/{arch}" for os_name, arch in _SUPPORTED_PLATFORMS)
|
|
38
|
+
raise BarcodeKitUnsupportedPlatform(
|
|
39
|
+
f"Unsupported platform {system}/{machine}. Supported platforms: {supported}."
|
|
40
|
+
)
|
|
41
|
+
return system, name
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _require_file(value: str | Path, source: str) -> Path:
|
|
45
|
+
path = Path(value).expanduser()
|
|
46
|
+
if not path.is_file():
|
|
47
|
+
raise BarcodeKitBinaryNotFound(f"{source} does not point to a file: {path}")
|
|
48
|
+
return path
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _ensure_bundled_executable(path: Path, system: str) -> None:
|
|
52
|
+
if system == "windows":
|
|
53
|
+
return
|
|
54
|
+
mode = path.stat().st_mode
|
|
55
|
+
if mode & stat.S_IXUSR:
|
|
56
|
+
return
|
|
57
|
+
try:
|
|
58
|
+
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
59
|
+
except OSError as exc:
|
|
60
|
+
raise BarcodeKitBinaryNotFound(
|
|
61
|
+
f"Bundled binary is not executable and its permissions could not be changed: {path}"
|
|
62
|
+
) from exc
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def resolve_binary(executable: str | Path | None = None) -> Path:
|
|
66
|
+
"""Resolve an explicit, environment-provided, or bundled executable."""
|
|
67
|
+
system, bundled_name = _bundled_binary_name()
|
|
68
|
+
|
|
69
|
+
if executable is not None:
|
|
70
|
+
return _require_file(executable, "The executable argument")
|
|
71
|
+
|
|
72
|
+
environment_path = os.environ.get(_BINARY_ENV)
|
|
73
|
+
if environment_path:
|
|
74
|
+
return _require_file(environment_path, _BINARY_ENV)
|
|
75
|
+
|
|
76
|
+
bundled = _BINARY_DIR / bundled_name
|
|
77
|
+
if not bundled.is_file():
|
|
78
|
+
raise BarcodeKitBinaryNotFound(
|
|
79
|
+
f"Bundled barcode-rest binary was not found at {bundled}. "
|
|
80
|
+
f"Install the platform-specific wheel, set {_BINARY_ENV}, "
|
|
81
|
+
"or pass executable= to BarcodeKit."
|
|
82
|
+
)
|
|
83
|
+
_ensure_bundled_executable(bundled, system)
|
|
84
|
+
return bundled
|
|
85
|
+
|
barcodekit/_core.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""Core barcodekit API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ._binary import resolve_binary
|
|
13
|
+
from ._errors import (
|
|
14
|
+
BarcodeKitBinaryNotFound,
|
|
15
|
+
BarcodeKitCommandError,
|
|
16
|
+
BarcodeKitTimeout,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
|
20
|
+
_TWO_DIMENSIONAL_LIMITS = {
|
|
21
|
+
"datamatrix": 128,
|
|
22
|
+
"qr": 256,
|
|
23
|
+
"aztec": 256,
|
|
24
|
+
"pdf417": 256,
|
|
25
|
+
}
|
|
26
|
+
_ONE_DIMENSIONAL = {
|
|
27
|
+
"code128",
|
|
28
|
+
"code39",
|
|
29
|
+
"code93",
|
|
30
|
+
"codabar",
|
|
31
|
+
"itf",
|
|
32
|
+
"code25",
|
|
33
|
+
"ean13",
|
|
34
|
+
"ean8",
|
|
35
|
+
}
|
|
36
|
+
_SUPPORTED_SYMBOLOGIES = frozenset(_TWO_DIMENSIONAL_LIMITS) | _ONE_DIMENSIONAL
|
|
37
|
+
_COMMON_2D_OPTIONS = frozenset({"module", "quiet", "size"})
|
|
38
|
+
_COMMON_1D_OPTIONS = frozenset({"module", "height", "quiet", "label"})
|
|
39
|
+
_ALLOWED_OPTIONS = {
|
|
40
|
+
"datamatrix": _COMMON_2D_OPTIONS,
|
|
41
|
+
"qr": _COMMON_2D_OPTIONS | {"level"},
|
|
42
|
+
"aztec": _COMMON_2D_OPTIONS,
|
|
43
|
+
"pdf417": {"module", "quiet", "level"},
|
|
44
|
+
"code128": _COMMON_1D_OPTIONS,
|
|
45
|
+
"code39": _COMMON_1D_OPTIONS | {"fullascii"},
|
|
46
|
+
"code93": _COMMON_1D_OPTIONS | {"fullascii"},
|
|
47
|
+
"codabar": _COMMON_1D_OPTIONS,
|
|
48
|
+
"itf": _COMMON_1D_OPTIONS,
|
|
49
|
+
"code25": _COMMON_1D_OPTIONS,
|
|
50
|
+
"ean13": _COMMON_1D_OPTIONS,
|
|
51
|
+
"ean8": _COMMON_1D_OPTIONS,
|
|
52
|
+
}
|
|
53
|
+
_BASIC_CODE39_CHARACTERS = frozenset("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%")
|
|
54
|
+
_CODABAR_PATTERN = re.compile(r"^[A-D][0-9\-$:/.+]+[A-D]$")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class BarcodeImage:
|
|
59
|
+
"""A generated PNG image."""
|
|
60
|
+
|
|
61
|
+
png: bytes
|
|
62
|
+
|
|
63
|
+
def save(self, path: str | Path) -> Path:
|
|
64
|
+
"""Write the PNG bytes to path and return the resulting Path."""
|
|
65
|
+
destination = Path(path)
|
|
66
|
+
destination.write_bytes(self.png)
|
|
67
|
+
return destination
|
|
68
|
+
|
|
69
|
+
def to_bytes(self) -> bytes:
|
|
70
|
+
"""Return the raw PNG bytes."""
|
|
71
|
+
return self.png
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _validate_int(value: object, name: str, minimum: int, maximum: int) -> int:
|
|
75
|
+
if type(value) is not int:
|
|
76
|
+
raise TypeError(f"{name} must be an integer")
|
|
77
|
+
integer = value
|
|
78
|
+
if not minimum <= integer <= maximum:
|
|
79
|
+
raise ValueError(f"{name} must be between {minimum} and {maximum}")
|
|
80
|
+
return integer
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _validate_bool(value: object, name: str) -> bool:
|
|
84
|
+
if type(value) is not bool:
|
|
85
|
+
raise TypeError(f"{name} must be a boolean")
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _validate_qr_level(value: object) -> str:
|
|
90
|
+
if not isinstance(value, str):
|
|
91
|
+
raise TypeError("level must be a string for QR codes")
|
|
92
|
+
level = value.upper()
|
|
93
|
+
if level not in {"L", "M", "Q", "H"}:
|
|
94
|
+
raise ValueError("QR level must be one of L, M, Q, or H")
|
|
95
|
+
return level
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _validate_pdf417_level(value: object) -> int:
|
|
99
|
+
if isinstance(value, bool):
|
|
100
|
+
raise TypeError("level must be an integer or numeric string for PDF417")
|
|
101
|
+
if isinstance(value, str):
|
|
102
|
+
if not value.isdigit():
|
|
103
|
+
raise ValueError("PDF417 level must be between 0 and 8")
|
|
104
|
+
value = int(value)
|
|
105
|
+
if not isinstance(value, int):
|
|
106
|
+
raise TypeError("level must be an integer or numeric string for PDF417")
|
|
107
|
+
if not 0 <= value <= 8:
|
|
108
|
+
raise ValueError("PDF417 level must be between 0 and 8")
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _validate_options(symbology: str, options: Mapping[str, object]) -> dict[str, object]:
|
|
113
|
+
unknown = set(options) - _ALLOWED_OPTIONS[symbology]
|
|
114
|
+
if unknown:
|
|
115
|
+
names = ", ".join(sorted(unknown))
|
|
116
|
+
raise ValueError(f"Unsupported option(s) for {symbology}: {names}")
|
|
117
|
+
|
|
118
|
+
if options.get("size") is not None and options.get("module") is not None:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
"size and module cannot be specified together; set size=None to use module"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
validated: dict[str, object] = {}
|
|
124
|
+
for name, value in options.items():
|
|
125
|
+
if value is None:
|
|
126
|
+
continue
|
|
127
|
+
if name == "module":
|
|
128
|
+
validated[name] = _validate_int(value, name, 2, 32)
|
|
129
|
+
elif name == "quiet":
|
|
130
|
+
validated[name] = _validate_int(value, name, 0, 16)
|
|
131
|
+
elif name == "size":
|
|
132
|
+
validated[name] = _validate_int(value, name, 16, 2048)
|
|
133
|
+
elif name == "height":
|
|
134
|
+
validated[name] = _validate_int(value, name, 20, 600)
|
|
135
|
+
elif name in {"label", "fullascii"}:
|
|
136
|
+
validated[name] = _validate_bool(value, name)
|
|
137
|
+
elif name == "level" and symbology == "qr":
|
|
138
|
+
validated[name] = _validate_qr_level(value)
|
|
139
|
+
elif name == "level" and symbology == "pdf417":
|
|
140
|
+
validated[name] = _validate_pdf417_level(value)
|
|
141
|
+
return validated
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _utf8_bytes(text: str) -> bytes:
|
|
145
|
+
try:
|
|
146
|
+
return text.encode("utf-8")
|
|
147
|
+
except UnicodeEncodeError as exc:
|
|
148
|
+
raise ValueError("text must be valid UTF-8") from exc
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _validate_ascii(text: str, symbology: str, maximum: int) -> None:
|
|
152
|
+
if not text.isascii():
|
|
153
|
+
raise ValueError(f"{symbology} text must contain ASCII characters only")
|
|
154
|
+
if len(text) > maximum:
|
|
155
|
+
raise ValueError(f"{symbology} text must be at most {maximum} characters")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _ean_checksum(body: str) -> str:
|
|
159
|
+
total = sum(
|
|
160
|
+
int(digit) * (3 if index % 2 == 0 else 1)
|
|
161
|
+
for index, digit in enumerate(reversed(body))
|
|
162
|
+
)
|
|
163
|
+
return str((10 - total % 10) % 10)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _validate_text(symbology: str, text: str, options: Mapping[str, object]) -> None:
|
|
167
|
+
if not isinstance(text, str):
|
|
168
|
+
raise TypeError("text must be a string")
|
|
169
|
+
if not text:
|
|
170
|
+
raise ValueError("text must not be empty")
|
|
171
|
+
if "\x00" in text:
|
|
172
|
+
raise ValueError("text must not contain NUL characters")
|
|
173
|
+
|
|
174
|
+
if symbology in _TWO_DIMENSIONAL_LIMITS:
|
|
175
|
+
maximum = _TWO_DIMENSIONAL_LIMITS[symbology]
|
|
176
|
+
if len(_utf8_bytes(text)) > maximum:
|
|
177
|
+
raise ValueError(f"{symbology} text must be at most {maximum} UTF-8 bytes")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
if symbology == "code128":
|
|
181
|
+
_validate_ascii(text, symbology, 80)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if symbology in {"code39", "code93"}:
|
|
185
|
+
_validate_ascii(text, symbology, 128)
|
|
186
|
+
if not options.get("fullascii", False) and not set(text) <= _BASIC_CODE39_CHARACTERS:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"{symbology} text contains characters that require fullascii=True"
|
|
189
|
+
)
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
if symbology == "codabar":
|
|
193
|
+
_validate_ascii(text, symbology, 128)
|
|
194
|
+
if _CODABAR_PATTERN.fullmatch(text) is None:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
"codabar text must start and end with A-D and contain valid data characters"
|
|
197
|
+
)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if symbology in {"itf", "code25"}:
|
|
201
|
+
_validate_ascii(text, symbology, 128)
|
|
202
|
+
if not text.isdigit():
|
|
203
|
+
raise ValueError(f"{symbology} text must contain digits only")
|
|
204
|
+
if symbology == "itf" and len(text) % 2:
|
|
205
|
+
raise ValueError("itf text must contain an even number of digits")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
required_lengths = (12, 13) if symbology == "ean13" else (7, 8)
|
|
209
|
+
if not text.isascii() or not text.isdigit() or len(text) not in required_lengths:
|
|
210
|
+
lengths = " or ".join(str(length) for length in required_lengths)
|
|
211
|
+
raise ValueError(f"{symbology} text must contain exactly {lengths} digits")
|
|
212
|
+
if len(text) == required_lengths[1] and text[-1] != _ean_checksum(text[:-1]):
|
|
213
|
+
raise ValueError(f"{symbology} text has an invalid check digit")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _option_flag(name: str) -> str:
|
|
217
|
+
return f"--{name.replace('_', '-')}"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _build_option_arguments(options: Mapping[str, object]) -> list[str]:
|
|
221
|
+
arguments: list[str] = []
|
|
222
|
+
for name, value in options.items():
|
|
223
|
+
flag = _option_flag(name)
|
|
224
|
+
if isinstance(value, bool):
|
|
225
|
+
if value:
|
|
226
|
+
arguments.append(flag)
|
|
227
|
+
else:
|
|
228
|
+
arguments.extend((flag, str(value)))
|
|
229
|
+
return arguments
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _redact_stderr(stderr: str, text: str) -> str:
|
|
233
|
+
return stderr.replace(text, "<redacted>") if text else stderr
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _is_png(data: bytes) -> bool:
|
|
237
|
+
if not data.startswith(_PNG_SIGNATURE):
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
offset = len(_PNG_SIGNATURE)
|
|
241
|
+
first_chunk = True
|
|
242
|
+
while offset + 12 <= len(data):
|
|
243
|
+
length = int.from_bytes(data[offset : offset + 4], "big")
|
|
244
|
+
chunk_type = data[offset + 4 : offset + 8]
|
|
245
|
+
chunk_end = offset + 12 + length
|
|
246
|
+
if chunk_end > len(data):
|
|
247
|
+
return False
|
|
248
|
+
if first_chunk and (chunk_type != b"IHDR" or length != 13):
|
|
249
|
+
return False
|
|
250
|
+
first_chunk = False
|
|
251
|
+
if chunk_type == b"IEND":
|
|
252
|
+
return length == 0 and chunk_end == len(data)
|
|
253
|
+
offset = chunk_end
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class BarcodeKit:
|
|
258
|
+
"""Generate barcode PNGs by invoking barcode-rest in one-shot CLI mode."""
|
|
259
|
+
|
|
260
|
+
def __init__(
|
|
261
|
+
self,
|
|
262
|
+
executable: str | Path | None = None,
|
|
263
|
+
timeout: float = 10.0,
|
|
264
|
+
) -> None:
|
|
265
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
|
|
266
|
+
raise TypeError("timeout must be a number")
|
|
267
|
+
if not math.isfinite(timeout) or timeout <= 0:
|
|
268
|
+
raise ValueError("timeout must be a finite number greater than zero")
|
|
269
|
+
self._executable = executable
|
|
270
|
+
self.timeout = float(timeout)
|
|
271
|
+
|
|
272
|
+
def generate(self, symbology: str, text: str, **options: object) -> BarcodeImage:
|
|
273
|
+
"""Generate a barcode using a supported symbology."""
|
|
274
|
+
if not isinstance(symbology, str):
|
|
275
|
+
raise TypeError("symbology must be a string")
|
|
276
|
+
if symbology not in _SUPPORTED_SYMBOLOGIES:
|
|
277
|
+
raise ValueError(f"Unsupported symbology: {symbology}")
|
|
278
|
+
|
|
279
|
+
validated_options = _validate_options(symbology, options)
|
|
280
|
+
_validate_text(symbology, text, validated_options)
|
|
281
|
+
executable = resolve_binary(self._executable)
|
|
282
|
+
command = [
|
|
283
|
+
str(executable),
|
|
284
|
+
"generate",
|
|
285
|
+
symbology,
|
|
286
|
+
"--text",
|
|
287
|
+
text,
|
|
288
|
+
"--output",
|
|
289
|
+
"-",
|
|
290
|
+
*_build_option_arguments(validated_options),
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
completed = subprocess.run( # noqa: UP022 - explicit PIPEs are part of the contract
|
|
295
|
+
command,
|
|
296
|
+
stdout=subprocess.PIPE,
|
|
297
|
+
stderr=subprocess.PIPE,
|
|
298
|
+
timeout=self.timeout,
|
|
299
|
+
check=False,
|
|
300
|
+
shell=False,
|
|
301
|
+
)
|
|
302
|
+
except subprocess.TimeoutExpired as exc:
|
|
303
|
+
raise BarcodeKitTimeout(self.timeout, command) from exc
|
|
304
|
+
except FileNotFoundError as exc:
|
|
305
|
+
raise BarcodeKitBinaryNotFound(
|
|
306
|
+
f"barcode-rest executable disappeared before it could be run: {executable}"
|
|
307
|
+
) from exc
|
|
308
|
+
except OSError as exc:
|
|
309
|
+
message = _redact_stderr(str(exc), text)
|
|
310
|
+
raise BarcodeKitCommandError(None, command, message) from exc
|
|
311
|
+
|
|
312
|
+
stderr = _redact_stderr(completed.stderr.decode("utf-8", errors="replace"), text)
|
|
313
|
+
if completed.returncode != 0:
|
|
314
|
+
raise BarcodeKitCommandError(completed.returncode, command, stderr)
|
|
315
|
+
if not _is_png(completed.stdout):
|
|
316
|
+
raise BarcodeKitCommandError(0, command, "barcode-rest produced invalid PNG output")
|
|
317
|
+
return BarcodeImage(completed.stdout)
|
|
318
|
+
|
|
319
|
+
def datamatrix(
|
|
320
|
+
self,
|
|
321
|
+
text: str,
|
|
322
|
+
*,
|
|
323
|
+
size: int | None = 256,
|
|
324
|
+
module: int | None = None,
|
|
325
|
+
quiet: int | None = None,
|
|
326
|
+
) -> BarcodeImage:
|
|
327
|
+
return self.generate("datamatrix", text, size=size, module=module, quiet=quiet)
|
|
328
|
+
|
|
329
|
+
def qr(
|
|
330
|
+
self,
|
|
331
|
+
text: str,
|
|
332
|
+
*,
|
|
333
|
+
size: int | None = 512,
|
|
334
|
+
module: int | None = None,
|
|
335
|
+
quiet: int | None = None,
|
|
336
|
+
level: str = "M",
|
|
337
|
+
) -> BarcodeImage:
|
|
338
|
+
return self.generate(
|
|
339
|
+
"qr", text, size=size, module=module, quiet=quiet, level=level
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def aztec(
|
|
343
|
+
self,
|
|
344
|
+
text: str,
|
|
345
|
+
*,
|
|
346
|
+
size: int | None = 512,
|
|
347
|
+
module: int | None = None,
|
|
348
|
+
quiet: int | None = None,
|
|
349
|
+
) -> BarcodeImage:
|
|
350
|
+
return self.generate("aztec", text, size=size, module=module, quiet=quiet)
|
|
351
|
+
|
|
352
|
+
def pdf417(
|
|
353
|
+
self,
|
|
354
|
+
text: str,
|
|
355
|
+
*,
|
|
356
|
+
module: int | None = None,
|
|
357
|
+
quiet: int | None = None,
|
|
358
|
+
level: int | str = 2,
|
|
359
|
+
) -> BarcodeImage:
|
|
360
|
+
return self.generate("pdf417", text, module=module, quiet=quiet, level=level)
|
|
361
|
+
|
|
362
|
+
def code128(
|
|
363
|
+
self,
|
|
364
|
+
text: str,
|
|
365
|
+
*,
|
|
366
|
+
module: int | None = None,
|
|
367
|
+
height: int | None = 80,
|
|
368
|
+
quiet: int | None = None,
|
|
369
|
+
label: bool = False,
|
|
370
|
+
) -> BarcodeImage:
|
|
371
|
+
return self.generate(
|
|
372
|
+
"code128", text, module=module, height=height, quiet=quiet, label=label
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def code39(
|
|
376
|
+
self,
|
|
377
|
+
text: str,
|
|
378
|
+
*,
|
|
379
|
+
module: int | None = None,
|
|
380
|
+
height: int | None = 80,
|
|
381
|
+
quiet: int | None = None,
|
|
382
|
+
label: bool = False,
|
|
383
|
+
fullascii: bool = False,
|
|
384
|
+
) -> BarcodeImage:
|
|
385
|
+
return self.generate(
|
|
386
|
+
"code39",
|
|
387
|
+
text,
|
|
388
|
+
module=module,
|
|
389
|
+
height=height,
|
|
390
|
+
quiet=quiet,
|
|
391
|
+
label=label,
|
|
392
|
+
fullascii=fullascii,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def code93(
|
|
396
|
+
self,
|
|
397
|
+
text: str,
|
|
398
|
+
*,
|
|
399
|
+
module: int | None = None,
|
|
400
|
+
height: int | None = 80,
|
|
401
|
+
quiet: int | None = None,
|
|
402
|
+
label: bool = False,
|
|
403
|
+
fullascii: bool = False,
|
|
404
|
+
) -> BarcodeImage:
|
|
405
|
+
return self.generate(
|
|
406
|
+
"code93",
|
|
407
|
+
text,
|
|
408
|
+
module=module,
|
|
409
|
+
height=height,
|
|
410
|
+
quiet=quiet,
|
|
411
|
+
label=label,
|
|
412
|
+
fullascii=fullascii,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def codabar(
|
|
416
|
+
self,
|
|
417
|
+
text: str,
|
|
418
|
+
*,
|
|
419
|
+
module: int | None = None,
|
|
420
|
+
height: int | None = 80,
|
|
421
|
+
quiet: int | None = None,
|
|
422
|
+
label: bool = False,
|
|
423
|
+
) -> BarcodeImage:
|
|
424
|
+
return self.generate(
|
|
425
|
+
"codabar", text, module=module, height=height, quiet=quiet, label=label
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def itf(
|
|
429
|
+
self,
|
|
430
|
+
text: str,
|
|
431
|
+
*,
|
|
432
|
+
module: int | None = None,
|
|
433
|
+
height: int | None = 80,
|
|
434
|
+
quiet: int | None = None,
|
|
435
|
+
label: bool = False,
|
|
436
|
+
) -> BarcodeImage:
|
|
437
|
+
return self.generate(
|
|
438
|
+
"itf", text, module=module, height=height, quiet=quiet, label=label
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def code25(
|
|
442
|
+
self,
|
|
443
|
+
text: str,
|
|
444
|
+
*,
|
|
445
|
+
module: int | None = None,
|
|
446
|
+
height: int | None = 80,
|
|
447
|
+
quiet: int | None = None,
|
|
448
|
+
label: bool = False,
|
|
449
|
+
) -> BarcodeImage:
|
|
450
|
+
return self.generate(
|
|
451
|
+
"code25", text, module=module, height=height, quiet=quiet, label=label
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def ean13(
|
|
455
|
+
self,
|
|
456
|
+
text: str,
|
|
457
|
+
*,
|
|
458
|
+
module: int | None = None,
|
|
459
|
+
height: int | None = 80,
|
|
460
|
+
quiet: int | None = None,
|
|
461
|
+
label: bool = False,
|
|
462
|
+
) -> BarcodeImage:
|
|
463
|
+
return self.generate(
|
|
464
|
+
"ean13", text, module=module, height=height, quiet=quiet, label=label
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def ean8(
|
|
468
|
+
self,
|
|
469
|
+
text: str,
|
|
470
|
+
*,
|
|
471
|
+
module: int | None = None,
|
|
472
|
+
height: int | None = 80,
|
|
473
|
+
quiet: int | None = None,
|
|
474
|
+
label: bool = False,
|
|
475
|
+
) -> BarcodeImage:
|
|
476
|
+
return self.generate(
|
|
477
|
+
"ean8", text, module=module, height=height, quiet=quiet, label=label
|
|
478
|
+
)
|
barcodekit/_errors.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Public exceptions raised by barcodekit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _redact_command(command: Sequence[str]) -> tuple[str, ...]:
|
|
12
|
+
"""Return command arguments with the barcode text removed."""
|
|
13
|
+
redacted = list(command)
|
|
14
|
+
for index, argument in enumerate(redacted[:-1]):
|
|
15
|
+
if argument == "--text":
|
|
16
|
+
redacted[index + 1] = "<redacted>"
|
|
17
|
+
return tuple(redacted)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _display_command(command: Sequence[str]) -> str:
|
|
21
|
+
redacted = _redact_command(command)
|
|
22
|
+
if os.name == "nt":
|
|
23
|
+
return subprocess.list2cmdline(redacted)
|
|
24
|
+
return shlex.join(redacted)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BarcodeKitError(Exception):
|
|
28
|
+
"""Base class for barcodekit errors."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BarcodeKitBinaryNotFound(BarcodeKitError):
|
|
32
|
+
"""Raised when the configured or bundled executable cannot be found."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BarcodeKitUnsupportedPlatform(BarcodeKitError):
|
|
36
|
+
"""Raised when barcodekit does not support the current platform."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BarcodeKitCommandError(BarcodeKitError):
|
|
40
|
+
"""Raised when barcode-rest fails or returns unusable output."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
returncode: int | None,
|
|
45
|
+
command: Sequence[str],
|
|
46
|
+
stderr: str,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.returncode = returncode
|
|
49
|
+
self.command = _redact_command(command)
|
|
50
|
+
self.stderr = stderr
|
|
51
|
+
code = "could not be started" if returncode is None else f"exited with code {returncode}"
|
|
52
|
+
detail = stderr.strip() or "no error details"
|
|
53
|
+
super().__init__(f"barcode-rest {code}: {_display_command(command)}; {detail}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BarcodeKitTimeout(BarcodeKitError):
|
|
57
|
+
"""Raised when barcode-rest does not finish before the timeout."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, timeout: float, command: Sequence[str]) -> None:
|
|
60
|
+
self.timeout = timeout
|
|
61
|
+
self.command = _redact_command(command)
|
|
62
|
+
super().__init__(
|
|
63
|
+
f"barcode-rest timed out after {timeout:g} seconds: {_display_command(command)}"
|
|
64
|
+
)
|
barcodekit/_version.py
ADDED
barcodekit/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: barcodekit
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Generate barcode PNGs with the local barcode-rest command-line tool.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Moge800/barcodekit
|
|
6
|
+
Project-URL: Upstream CLI, https://github.com/Moge800/barcode-rest
|
|
7
|
+
Author: Moge800
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
11
|
+
License-File: licenses/barcode-rest-MIT.txt
|
|
12
|
+
License-File: licenses/boombuler-barcode-MIT.txt
|
|
13
|
+
License-File: licenses/go-and-x-image-BSD-3-Clause.txt
|
|
14
|
+
Keywords: barcode,barcode-rest,code128,datamatrix,qr
|
|
15
|
+
Classifier: Development Status :: 3 - Alpha
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.14; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
28
|
+
Provides-Extra: release
|
|
29
|
+
Requires-Dist: wheel<1,>=0.45; extra == 'release'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# barcodekit
|
|
33
|
+
|
|
34
|
+
[日本語](https://github.com/Moge800/barcodekit/blob/main/README_JP.md)
|
|
35
|
+
|
|
36
|
+
`barcodekit` generates barcode PNG images from Python by invoking the local
|
|
37
|
+
[`barcode-rest`](https://github.com/Moge800/barcode-rest) executable in its
|
|
38
|
+
one-shot CLI mode.
|
|
39
|
+
|
|
40
|
+
Despite the upstream executable's name, **barcodekit does not start the REST
|
|
41
|
+
server and does not use HTTP**. Each operation runs only:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
barcode-rest generate <symbology> --text <text> --output -
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
PNG bytes are read from standard output and returned directly to Python.
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
Install the platform-specific wheel, then:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from barcodekit import code128, datamatrix, qr
|
|
55
|
+
|
|
56
|
+
datamatrix("ABC123", size=256).save("dm.png")
|
|
57
|
+
qr("https://example.com", size=512, level="Q").save("qr.png")
|
|
58
|
+
code128("ABC-123456", label=True).save("c128.png")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
An explicit engine object provides the same methods:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from barcodekit import BarcodeKit
|
|
65
|
+
|
|
66
|
+
kit = BarcodeKit(timeout=10)
|
|
67
|
+
image = kit.datamatrix("ABC123", size=256)
|
|
68
|
+
|
|
69
|
+
raw_png = image.to_bytes()
|
|
70
|
+
image.save("dm.png")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For `datamatrix`, `qr`, and `aztec`, `size` and `module` are mutually exclusive.
|
|
74
|
+
Set `size=None` when selecting the module size directly:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
datamatrix("ABC123", size=None, module=8)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Executable resolution
|
|
81
|
+
|
|
82
|
+
`barcodekit` resolves the executable when an image is generated, in this order:
|
|
83
|
+
|
|
84
|
+
1. The file path passed as `BarcodeKit(executable=...)`.
|
|
85
|
+
2. The file path in `BARCODEKIT_BINARY`.
|
|
86
|
+
3. The executable bundled in the installed platform wheel.
|
|
87
|
+
|
|
88
|
+
An explicit development binary can be used without building a wheel:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
kit = BarcodeKit(executable=r"C:\tools\barcode-rest.exe")
|
|
92
|
+
kit.datamatrix("ABC123").save("dm.png")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or with an environment variable:
|
|
96
|
+
|
|
97
|
+
```powershell
|
|
98
|
+
$env:BARCODEKIT_BINARY = "C:\tools\barcode-rest.exe"
|
|
99
|
+
uv run python example.py
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
BARCODEKIT_BINARY=/opt/barcode-rest uv run python example.py
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
These values must be file paths; barcodekit does not search `PATH`.
|
|
107
|
+
|
|
108
|
+
## Bundled binary wheels
|
|
109
|
+
|
|
110
|
+
Each released wheel is intended to contain exactly one matching
|
|
111
|
+
`barcode-rest` executable. It must not contain a collection of executables for
|
|
112
|
+
other operating systems or CPU architectures.
|
|
113
|
+
|
|
114
|
+
Supported bundled targets:
|
|
115
|
+
|
|
116
|
+
- Windows amd64
|
|
117
|
+
- Linux amd64 using glibc 2.34 or newer, including Ubuntu 22.04 or newer
|
|
118
|
+
- Linux arm64 using glibc, including 64-bit Ubuntu and 64-bit Raspberry Pi OS
|
|
119
|
+
|
|
120
|
+
Unsupported targets:
|
|
121
|
+
|
|
122
|
+
- macOS
|
|
123
|
+
- Windows arm64
|
|
124
|
+
- 32-bit Linux and 32-bit Raspberry Pi OS
|
|
125
|
+
- musl-based Linux distributions such as Alpine Linux
|
|
126
|
+
|
|
127
|
+
Binary-free source distributions are not intended for release. On the
|
|
128
|
+
supported operating systems and CPU architectures listed above, development
|
|
129
|
+
from a source checkout remains supported with `BARCODEKIT_BINARY` or
|
|
130
|
+
`BarcodeKit(executable=...)`.
|
|
131
|
+
|
|
132
|
+
Release builds currently pin
|
|
133
|
+
[`barcode-rest` v0.2.0](https://github.com/Moge800/barcode-rest/releases/tag/v0.2.0).
|
|
134
|
+
The expected SHA-256 values are committed in `checksums/v0.2.0.sha256`.
|
|
135
|
+
|
|
136
|
+
## Supported symbologies
|
|
137
|
+
|
|
138
|
+
Two-dimensional:
|
|
139
|
+
|
|
140
|
+
- Data Matrix (`datamatrix`)
|
|
141
|
+
- QR Code (`qr`)
|
|
142
|
+
- Aztec (`aztec`)
|
|
143
|
+
- PDF417 (`pdf417`)
|
|
144
|
+
|
|
145
|
+
One-dimensional:
|
|
146
|
+
|
|
147
|
+
- Code 128 (`code128`)
|
|
148
|
+
- Code 39 (`code39`)
|
|
149
|
+
- Code 93 (`code93`)
|
|
150
|
+
- Codabar (`codabar`)
|
|
151
|
+
- Interleaved 2 of 5 (`itf`)
|
|
152
|
+
- Standard 2 of 5 (`code25`)
|
|
153
|
+
- EAN-13 / JAN (`ean13`)
|
|
154
|
+
- EAN-8 (`ean8`)
|
|
155
|
+
|
|
156
|
+
`barcodekit` validates supported options, numeric ranges, text limits, basic
|
|
157
|
+
character sets, and check digits before starting the executable. Encoding
|
|
158
|
+
constraints that depend on the generated symbol remain the responsibility of
|
|
159
|
+
`barcode-rest`.
|
|
160
|
+
|
|
161
|
+
## Security and privacy
|
|
162
|
+
|
|
163
|
+
- No executable or other data is downloaded at runtime.
|
|
164
|
+
- No server is started.
|
|
165
|
+
- The REST API and HTTP are not used.
|
|
166
|
+
- No outbound network connection is made by the wrapper.
|
|
167
|
+
- Barcode text is passed only to the local `barcode-rest` executable.
|
|
168
|
+
- The wrapper does not log barcode text.
|
|
169
|
+
- Commands shown by wrapper exceptions replace the value after `--text` with
|
|
170
|
+
`<redacted>`. Matching text returned on stderr is also redacted.
|
|
171
|
+
|
|
172
|
+
The text is necessarily passed through the local process command line because
|
|
173
|
+
that is the upstream CLI interface. It may therefore be temporarily visible to
|
|
174
|
+
users or tools with permission to inspect local process arguments.
|
|
175
|
+
|
|
176
|
+
## Development with uv
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
uv sync --extra dev
|
|
180
|
+
uv run pytest
|
|
181
|
+
uv run ruff check .
|
|
182
|
+
uv run mypy src/barcodekit
|
|
183
|
+
uv build
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Unit tests mock `subprocess.run` and do not need the Go executable. If
|
|
187
|
+
`BARCODEKIT_BINARY` is set, the optional integration test generates a real
|
|
188
|
+
Data Matrix image and checks its PNG output.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
`barcodekit` is MIT licensed. Platform wheels also include the notices and
|
|
193
|
+
license texts listed in
|
|
194
|
+
[THIRD_PARTY_NOTICES.md](https://github.com/Moge800/barcodekit/blob/v0.1.2/THIRD_PARTY_NOTICES.md).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
barcodekit/__init__.py,sha256=4KLH7Uiauip6QARkFz3jxe4Y4OR4LcRz4NqZgf5wxzI,4409
|
|
2
|
+
barcodekit/_binary.py,sha256=ybYPXUsesaDM3JLbmkfddANsUhDUtMGHCUU0y7l0BJA,2788
|
|
3
|
+
barcodekit/_core.py,sha256=71Isg1W-Aurk1vPSvN0pEQ6qGEndf3WJTSKQZDRCC3Y,15645
|
|
4
|
+
barcodekit/_errors.py,sha256=VzAUUSkhRrTCipUoVeK7Wq6aYplp8Ulj8feAuWMR66g,2082
|
|
5
|
+
barcodekit/_version.py,sha256=e1Iz5EfvRhaCoTklG4I54svyD-fQ1ujsrPto2dSw1cg,51
|
|
6
|
+
barcodekit/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
7
|
+
barcodekit/_bin/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
8
|
+
barcodekit/_bin/barcode-rest.exe,sha256=Qn-mC-IO7VPBILxsdyZc0s_PjOLaMQR4hcuVikjaWsg,9929216
|
|
9
|
+
barcodekit-0.1.2.dist-info/METADATA,sha256=9KKyLb47foSB3Dj-Pl2SEHL1XW6LrQU5q9pU9jNV4qI,6001
|
|
10
|
+
barcodekit-0.1.2.dist-info/WHEEL,sha256=pp_iTzO0EBcCvlZZV2dJYp6L2tdDWBsn1ceo50wEt8Q,94
|
|
11
|
+
barcodekit-0.1.2.dist-info/licenses/LICENSE,sha256=9BmdgGze2TgObLNWUVMlYMK3n_Q76jWnm_bHkdpBvg4,1087
|
|
12
|
+
barcodekit-0.1.2.dist-info/licenses/THIRD_PARTY_NOTICES.md,sha256=sYLLB_bLSGwDMEcQaMu6_UdTt3C-ggFLt0XELYQE5Sw,691
|
|
13
|
+
barcodekit-0.1.2.dist-info/licenses/licenses/barcode-rest-MIT.txt,sha256=9BmdgGze2TgObLNWUVMlYMK3n_Q76jWnm_bHkdpBvg4,1087
|
|
14
|
+
barcodekit-0.1.2.dist-info/licenses/licenses/boombuler-barcode-MIT.txt,sha256=KxADQwMxUj_V8zUg3vQ1VkxxPLZVNLLYMbpQkhcwUe0,1108
|
|
15
|
+
barcodekit-0.1.2.dist-info/licenses/licenses/go-and-x-image-BSD-3-Clause.txt,sha256=JyHS_WMLrAVQohVdq64G2DWGcuUpCauv_SsstFNktYc,1482
|
|
16
|
+
barcodekit-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moge800
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, 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,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
Platform-specific `barcodekit` wheels bundle the `barcode-rest` executable.
|
|
4
|
+
That executable contains the following software:
|
|
5
|
+
|
|
6
|
+
| Component | Version | License |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| [barcode-rest](https://github.com/Moge800/barcode-rest) | v0.2.0 | MIT |
|
|
9
|
+
| [github.com/boombuler/barcode](https://github.com/boombuler/barcode) | v1.0.2 | MIT |
|
|
10
|
+
| [Go](https://go.dev/) runtime and standard library | 1.26 | BSD-3-Clause |
|
|
11
|
+
| [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) | v0.43.0 | BSD-3-Clause |
|
|
12
|
+
|
|
13
|
+
The corresponding license texts are included in the `licenses` directory.
|
|
14
|
+
`barcodekit` itself is licensed under the MIT License in `LICENSE`.
|
|
15
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moge800
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, 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,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014 Florian Sundermann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, 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,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Copyright 2009 The Go Authors.
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
|
4
|
+
modification, are permitted provided that the following conditions are
|
|
5
|
+
met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright
|
|
8
|
+
notice, this list of conditions and the following disclaimer.
|
|
9
|
+
* Redistributions in binary form must reproduce the above
|
|
10
|
+
copyright notice, this list of conditions and the following disclaimer
|
|
11
|
+
in the documentation and/or other materials provided with the
|
|
12
|
+
distribution.
|
|
13
|
+
* Neither the name of Google LLC nor the names of its
|
|
14
|
+
contributors may be used to endorse or promote products derived from
|
|
15
|
+
this software without specific prior written permission.
|
|
16
|
+
|
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
18
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
19
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
20
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
21
|
+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
22
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
23
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
24
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
25
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
26
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
28
|
+
|