pybarcodes 1.0.3__tar.gz → 1.2.2__tar.gz
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.
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/PKG-INFO +2 -2
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/README.rst +1 -1
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/__init__.py +1 -1
- pybarcodes-1.2.2/pybarcodes/barcode.py +322 -0
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/codes.py +76 -67
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/ean.py +47 -41
- pybarcodes-1.2.2/pybarcodes/py.typed +1 -0
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes.egg-info/PKG-INFO +2 -2
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes.egg-info/SOURCES.txt +4 -2
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pyproject.toml +6 -2
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/tests/test_codes.py +12 -5
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/tests/test_ean.py +38 -20
- pybarcodes-1.2.2/tests/test_images.py +101 -0
- pybarcodes-1.2.2/tests/test_spec_fixtures.py +55 -0
- pybarcodes-1.0.3/pybarcodes/barcode.py +0 -159
- pybarcodes-1.0.3/pybarcodes/fonts/arial.ttf +0 -0
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/LICENSE +0 -0
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/codings/__init__.py +0 -0
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/codings/codex.py +0 -0
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/codings/ean.py +0 -0
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/exceptions.py +0 -0
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes.egg-info/dependency_links.txt +0 -0
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes.egg-info/requires.txt +0 -0
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes.egg-info/top_level.txt +0 -0
- {pybarcodes-1.0.3 → pybarcodes-1.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pybarcodes
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: A Python package for barcode generation
|
|
5
5
|
Author-email: atbuy <buy@atbuy.dev>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -48,7 +48,7 @@ pybarcodes
|
|
|
48
48
|
:alt: PyPI supported Python versions
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
This is a
|
|
51
|
+
This is a Python package to create barcodes.
|
|
52
52
|
You can create file-like objects, text files and images from just a barcode number.
|
|
53
53
|
Image generation is fast so it can be used to create images in bulk.
|
|
54
54
|
|
|
@@ -16,7 +16,7 @@ pybarcodes
|
|
|
16
16
|
:alt: PyPI supported Python versions
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
This is a
|
|
19
|
+
This is a Python package to create barcodes.
|
|
20
20
|
You can create file-like objects, text files and images from just a barcode number.
|
|
21
21
|
Image generation is fast so it can be used to create images in bulk.
|
|
22
22
|
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
from collections import namedtuple
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from os import PathLike
|
|
4
|
+
from typing import Any, Optional, Union
|
|
5
|
+
|
|
6
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
7
|
+
|
|
8
|
+
BarcodeInput = Union[str, int]
|
|
9
|
+
PathInput = Union[str, PathLike[str]]
|
|
10
|
+
RenderSize = tuple[int, int]
|
|
11
|
+
RenderOptions = tuple[int, int, int, int, int]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Barcode:
|
|
15
|
+
"""A base class for all barcode types"""
|
|
16
|
+
|
|
17
|
+
BARCODE_SIZE: RenderSize
|
|
18
|
+
BARCODE_FONT_SIZE: int
|
|
19
|
+
BARCODE_COLUMN_NUMBER: int
|
|
20
|
+
BARCODE_PADDING: Any
|
|
21
|
+
|
|
22
|
+
def __init__(self, barcode: BarcodeInput):
|
|
23
|
+
self.code = self.normalize(barcode)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def validate(cls, barcode: BarcodeInput) -> None:
|
|
27
|
+
"""Validate barcode input."""
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def normalize(cls, barcode: BarcodeInput) -> str:
|
|
31
|
+
"""Return the normalized barcode value used by the instance."""
|
|
32
|
+
|
|
33
|
+
cls.validate(barcode)
|
|
34
|
+
return str(barcode)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def image(self) -> Image.Image:
|
|
38
|
+
"""Retrieves and returns the PIL.Image object with the barcode
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
PIl.Image.Image:
|
|
43
|
+
The barcode image
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
return self.render()
|
|
47
|
+
|
|
48
|
+
def render(
|
|
49
|
+
self,
|
|
50
|
+
size: Optional[RenderSize] = None,
|
|
51
|
+
module_width: Optional[int] = None,
|
|
52
|
+
bar_height: Optional[int] = None,
|
|
53
|
+
quiet_zone: Optional[int] = None,
|
|
54
|
+
font_size: Optional[int] = None,
|
|
55
|
+
draw_text: bool = True,
|
|
56
|
+
) -> Image.Image:
|
|
57
|
+
"""Create a PIL Image object for the barcode."""
|
|
58
|
+
|
|
59
|
+
img = self._get_barcode_image(
|
|
60
|
+
module_width=module_width,
|
|
61
|
+
bar_height=bar_height,
|
|
62
|
+
quiet_zone=quiet_zone,
|
|
63
|
+
font_size=font_size,
|
|
64
|
+
draw_text=draw_text,
|
|
65
|
+
)
|
|
66
|
+
if size is not None:
|
|
67
|
+
resampling = getattr(Image, "Resampling", Image)
|
|
68
|
+
img = img.resize(size, resampling.NEAREST)
|
|
69
|
+
return img
|
|
70
|
+
|
|
71
|
+
def save(
|
|
72
|
+
self,
|
|
73
|
+
path: PathInput,
|
|
74
|
+
size: Optional[RenderSize] = None,
|
|
75
|
+
module_width: Optional[int] = None,
|
|
76
|
+
bar_height: Optional[int] = None,
|
|
77
|
+
quiet_zone: Optional[int] = None,
|
|
78
|
+
font_size: Optional[int] = None,
|
|
79
|
+
draw_text: bool = True,
|
|
80
|
+
**save_kwargs: Any,
|
|
81
|
+
) -> Image.Image:
|
|
82
|
+
"""Create a PIL Image object and save it to the path given.
|
|
83
|
+
|
|
84
|
+
It also returns that image object to the caller.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
path: str
|
|
89
|
+
The path to save the image to
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
Returns a PIL Image object to the caller
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
img = self.render(
|
|
97
|
+
size=size,
|
|
98
|
+
module_width=module_width,
|
|
99
|
+
bar_height=bar_height,
|
|
100
|
+
quiet_zone=quiet_zone,
|
|
101
|
+
font_size=font_size,
|
|
102
|
+
draw_text=draw_text,
|
|
103
|
+
)
|
|
104
|
+
img.save(path, **save_kwargs)
|
|
105
|
+
return img
|
|
106
|
+
|
|
107
|
+
def show(self) -> None:
|
|
108
|
+
"""Shows the barcode image"""
|
|
109
|
+
|
|
110
|
+
self.render().show()
|
|
111
|
+
|
|
112
|
+
def to_text_bytes(self, encoding: str = "ascii") -> bytes:
|
|
113
|
+
"""Return the normalized barcode text as bytes."""
|
|
114
|
+
|
|
115
|
+
return self.code.encode(encoding)
|
|
116
|
+
|
|
117
|
+
def to_text_bytesio(self, encoding: str = "ascii") -> BytesIO:
|
|
118
|
+
"""
|
|
119
|
+
Write the barcode to a BytesIO object
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
Returns the BytesIO object created
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
obj = BytesIO(self.to_text_bytes(encoding=encoding))
|
|
127
|
+
obj.seek(0)
|
|
128
|
+
return obj
|
|
129
|
+
|
|
130
|
+
def to_bytesio(self, encoding: str = "ascii") -> BytesIO:
|
|
131
|
+
"""Return the normalized barcode text in a BytesIO object."""
|
|
132
|
+
|
|
133
|
+
return self.to_text_bytesio(encoding=encoding)
|
|
134
|
+
|
|
135
|
+
def to_image_bytesio(
|
|
136
|
+
self,
|
|
137
|
+
format: str = "PNG",
|
|
138
|
+
size: Optional[RenderSize] = None,
|
|
139
|
+
module_width: Optional[int] = None,
|
|
140
|
+
bar_height: Optional[int] = None,
|
|
141
|
+
quiet_zone: Optional[int] = None,
|
|
142
|
+
font_size: Optional[int] = None,
|
|
143
|
+
draw_text: bool = True,
|
|
144
|
+
**save_kwargs: Any,
|
|
145
|
+
) -> BytesIO:
|
|
146
|
+
"""Return the rendered barcode image in a BytesIO object."""
|
|
147
|
+
|
|
148
|
+
obj = BytesIO()
|
|
149
|
+
self.render(
|
|
150
|
+
size=size,
|
|
151
|
+
module_width=module_width,
|
|
152
|
+
bar_height=bar_height,
|
|
153
|
+
quiet_zone=quiet_zone,
|
|
154
|
+
font_size=font_size,
|
|
155
|
+
draw_text=draw_text,
|
|
156
|
+
).save(obj, format=format, **save_kwargs)
|
|
157
|
+
obj.seek(0)
|
|
158
|
+
return obj
|
|
159
|
+
|
|
160
|
+
def to_image_bytes(
|
|
161
|
+
self,
|
|
162
|
+
format: str = "PNG",
|
|
163
|
+
size: Optional[RenderSize] = None,
|
|
164
|
+
module_width: Optional[int] = None,
|
|
165
|
+
bar_height: Optional[int] = None,
|
|
166
|
+
quiet_zone: Optional[int] = None,
|
|
167
|
+
font_size: Optional[int] = None,
|
|
168
|
+
draw_text: bool = True,
|
|
169
|
+
**save_kwargs: Any,
|
|
170
|
+
) -> bytes:
|
|
171
|
+
"""Return the rendered barcode image as bytes."""
|
|
172
|
+
|
|
173
|
+
return self.to_image_bytesio(
|
|
174
|
+
format=format,
|
|
175
|
+
size=size,
|
|
176
|
+
module_width=module_width,
|
|
177
|
+
bar_height=bar_height,
|
|
178
|
+
quiet_zone=quiet_zone,
|
|
179
|
+
font_size=font_size,
|
|
180
|
+
draw_text=draw_text,
|
|
181
|
+
**save_kwargs,
|
|
182
|
+
).getvalue()
|
|
183
|
+
|
|
184
|
+
def write(self, path: PathInput, encoding: str = "ascii") -> None:
|
|
185
|
+
"""
|
|
186
|
+
Tries to save the barcode to a text file
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
path: str
|
|
191
|
+
The path of the file
|
|
192
|
+
"""
|
|
193
|
+
with open(path, "w", encoding=encoding) as file:
|
|
194
|
+
file.write(self.code)
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def _positive_int(value: int, name: str) -> int:
|
|
198
|
+
value = int(value)
|
|
199
|
+
if value <= 0:
|
|
200
|
+
raise ValueError(f"{name} must be greater than 0.")
|
|
201
|
+
return value
|
|
202
|
+
|
|
203
|
+
def _get_render_options(
|
|
204
|
+
self,
|
|
205
|
+
module_width: Optional[int] = None,
|
|
206
|
+
bar_height: Optional[int] = None,
|
|
207
|
+
quiet_zone: Optional[int] = None,
|
|
208
|
+
font_size: Optional[int] = None,
|
|
209
|
+
draw_text: bool = True,
|
|
210
|
+
) -> RenderOptions:
|
|
211
|
+
padding = self.BARCODE_PADDING
|
|
212
|
+
selected_size, default_font_size = self.BARCODE_SIZE, self.BARCODE_FONT_SIZE
|
|
213
|
+
|
|
214
|
+
module_width = (
|
|
215
|
+
self._get_column_size()
|
|
216
|
+
if module_width is None
|
|
217
|
+
else self._positive_int(module_width, "module_width")
|
|
218
|
+
)
|
|
219
|
+
bar_height = (
|
|
220
|
+
selected_size[1]
|
|
221
|
+
if bar_height is None
|
|
222
|
+
else self._positive_int(bar_height, "bar_height")
|
|
223
|
+
)
|
|
224
|
+
quiet_zone = (
|
|
225
|
+
padding.width // 2
|
|
226
|
+
if quiet_zone is None
|
|
227
|
+
else self._positive_int(quiet_zone, "quiet_zone")
|
|
228
|
+
)
|
|
229
|
+
font_size = (
|
|
230
|
+
default_font_size
|
|
231
|
+
if font_size is None
|
|
232
|
+
else self._positive_int(font_size, "font_size")
|
|
233
|
+
)
|
|
234
|
+
text_padding = padding.height if draw_text else 0
|
|
235
|
+
|
|
236
|
+
return module_width, bar_height, quiet_zone, font_size, text_padding
|
|
237
|
+
|
|
238
|
+
def _get_default_font(self, font_size: int) -> ImageFont.ImageFont:
|
|
239
|
+
try:
|
|
240
|
+
return ImageFont.load_default(size=font_size)
|
|
241
|
+
except TypeError:
|
|
242
|
+
return ImageFont.load_default()
|
|
243
|
+
|
|
244
|
+
def _get_barcode_image(
|
|
245
|
+
self,
|
|
246
|
+
module_width: Optional[int] = None,
|
|
247
|
+
bar_height: Optional[int] = None,
|
|
248
|
+
quiet_zone: Optional[int] = None,
|
|
249
|
+
font_size: Optional[int] = None,
|
|
250
|
+
draw_text: bool = True,
|
|
251
|
+
) -> Image.Image:
|
|
252
|
+
"""Creates a PIL Image from the binary string of the barcode
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
A PIL Image with the barcode is returned to the caller.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
module_width, bar_height, quiet_zone, font_size, text_padding = (
|
|
260
|
+
self._get_render_options(
|
|
261
|
+
module_width=module_width,
|
|
262
|
+
bar_height=bar_height,
|
|
263
|
+
quiet_zone=quiet_zone,
|
|
264
|
+
font_size=font_size,
|
|
265
|
+
draw_text=draw_text,
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
binary_string = self.get_binary_string
|
|
270
|
+
|
|
271
|
+
# Create the image for the barcode
|
|
272
|
+
img = Image.new(
|
|
273
|
+
"RGB",
|
|
274
|
+
(module_width * len(binary_string), bar_height),
|
|
275
|
+
(255, 255, 255),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
index = 0
|
|
279
|
+
for digit in binary_string:
|
|
280
|
+
color = (0, 0, 0) if digit == "1" else (255, 255, 255)
|
|
281
|
+
column = Image.new("RGB", (module_width, img.height), color)
|
|
282
|
+
img.paste(column, (index, 0))
|
|
283
|
+
index += module_width
|
|
284
|
+
|
|
285
|
+
base = Image.new(
|
|
286
|
+
"RGB",
|
|
287
|
+
(img.width + quiet_zone * 2, bar_height + text_padding),
|
|
288
|
+
(255, 255, 255),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Paste the barcode on the center of the padded base
|
|
292
|
+
Point = namedtuple("Point", "x y")
|
|
293
|
+
base_center = Point(base.width // 2, base.height // 2)
|
|
294
|
+
|
|
295
|
+
base.paste(img, (quiet_zone, text_padding // 2))
|
|
296
|
+
|
|
297
|
+
if not draw_text:
|
|
298
|
+
return base
|
|
299
|
+
|
|
300
|
+
draw = ImageDraw.Draw(base)
|
|
301
|
+
font = self._get_default_font(font_size)
|
|
302
|
+
|
|
303
|
+
text_width = draw.textlength(self.code, font)
|
|
304
|
+
x = base_center.x - text_width // 2
|
|
305
|
+
y = text_padding // 2 + img.height
|
|
306
|
+
|
|
307
|
+
draw.text((x, y), self.code, (0, 0, 0), font=font)
|
|
308
|
+
return base
|
|
309
|
+
|
|
310
|
+
def __eq__(self, other: object) -> bool:
|
|
311
|
+
if isinstance(other, self.__class__):
|
|
312
|
+
return self.code == other.code
|
|
313
|
+
elif isinstance(other, str):
|
|
314
|
+
return self.code == other
|
|
315
|
+
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
def __str__(self) -> str:
|
|
319
|
+
return f"<{self.__class__.__name__}(code={self.code})>"
|
|
320
|
+
|
|
321
|
+
def __repr__(self) -> str:
|
|
322
|
+
return self.__str__()
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
from collections import namedtuple
|
|
2
|
-
from
|
|
3
|
-
from typing import Union
|
|
2
|
+
from typing import Optional, Union
|
|
4
3
|
|
|
5
|
-
from PIL import Image, ImageDraw
|
|
4
|
+
from PIL import Image, ImageDraw
|
|
6
5
|
|
|
7
|
-
from .barcode import Barcode
|
|
6
|
+
from .barcode import Barcode, BarcodeInput
|
|
8
7
|
from .codings import codex as CODEXCoding
|
|
9
8
|
from .exceptions import IncorrectFormat
|
|
10
9
|
|
|
@@ -12,10 +11,10 @@ Size = namedtuple("Size", "width height")
|
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
class Code(Barcode):
|
|
15
|
-
def __init__(self, barcode:
|
|
14
|
+
def __init__(self, barcode: BarcodeInput):
|
|
16
15
|
super().__init__(barcode)
|
|
17
16
|
|
|
18
|
-
self.
|
|
17
|
+
self.checksum = self.code[-1]
|
|
19
18
|
|
|
20
19
|
# Calculate the variable width of the barcod
|
|
21
20
|
# 6 pixels for each character
|
|
@@ -36,6 +35,39 @@ class Code(Barcode):
|
|
|
36
35
|
# Calculate how many character will have to be written
|
|
37
36
|
self.BARCODE_COLUMN_NUMBER = column_size
|
|
38
37
|
|
|
38
|
+
@classmethod
|
|
39
|
+
def validate(cls, barcode: BarcodeInput) -> None:
|
|
40
|
+
code = str(barcode).upper()
|
|
41
|
+
|
|
42
|
+
for char in code:
|
|
43
|
+
if char not in CODEXCoding.CODES or char == "_":
|
|
44
|
+
raise IncorrectFormat(
|
|
45
|
+
f"Character {char} is not supported by {cls.__name__}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def normalize(cls, barcode: BarcodeInput) -> str:
|
|
50
|
+
cls.validate(barcode)
|
|
51
|
+
code = str(barcode).upper()
|
|
52
|
+
reference = cls._calculate_checksum(code)
|
|
53
|
+
checkchar = CODEXCoding.REFERENCE_NUMBERS[reference]
|
|
54
|
+
return code + checkchar
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def _calculate_checksum(cls, barcode: str) -> int:
|
|
58
|
+
code = barcode.upper()
|
|
59
|
+
for char in code:
|
|
60
|
+
if char not in CODEXCoding.REFERENCE_DIGITS:
|
|
61
|
+
raise IncorrectFormat(
|
|
62
|
+
f"Character {char} is not supported by {cls.__name__}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if len(code) == 1:
|
|
66
|
+
return CODEXCoding.REFERENCE_DIGITS[code]
|
|
67
|
+
|
|
68
|
+
numbers = [CODEXCoding.REFERENCE_DIGITS[char] for char in list(code)]
|
|
69
|
+
return sum(numbers) % 43
|
|
70
|
+
|
|
39
71
|
@property
|
|
40
72
|
def get_binary_string(self) -> str:
|
|
41
73
|
"""Converts the code to the binary string that it produces.
|
|
@@ -61,7 +93,7 @@ class Code(Barcode):
|
|
|
61
93
|
|
|
62
94
|
return self._convert_to_binary(code)
|
|
63
95
|
|
|
64
|
-
def calculate_checksum(self, barcode: Union[str, "CODE39"] = None) -> int:
|
|
96
|
+
def calculate_checksum(self, barcode: Optional[Union[str, "CODE39"]] = None) -> int:
|
|
65
97
|
"""Calculate the checksum of the barcode
|
|
66
98
|
|
|
67
99
|
Parameters
|
|
@@ -85,25 +117,21 @@ class Code(Barcode):
|
|
|
85
117
|
barcode = barcode.code
|
|
86
118
|
elif isinstance(barcode, str):
|
|
87
119
|
pass
|
|
88
|
-
elif barcode is None
|
|
120
|
+
elif barcode is None:
|
|
89
121
|
return CODEXCoding.REFERENCE_DIGITS[self.checksum]
|
|
90
122
|
else:
|
|
91
123
|
raise TypeError(f"Can't accept type {type(barcode)}")
|
|
92
124
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# If the barcode is only 1 character long
|
|
96
|
-
# Then the check sum will be that same character
|
|
97
|
-
if len(barcode) == 1:
|
|
98
|
-
return CODEXCoding.REFERENCE_DIGITS[barcode]
|
|
99
|
-
|
|
100
|
-
# Find the number value of each digit
|
|
101
|
-
numbers = [CODEXCoding.REFERENCE_DIGITS[char] for char in list(barcode)]
|
|
102
|
-
|
|
103
|
-
# Calculate the modulo 43 of the sum
|
|
104
|
-
return sum(numbers) % 43
|
|
125
|
+
return self._calculate_checksum(barcode)
|
|
105
126
|
|
|
106
|
-
def _get_barcode_image(
|
|
127
|
+
def _get_barcode_image(
|
|
128
|
+
self,
|
|
129
|
+
module_width: Optional[int] = None,
|
|
130
|
+
bar_height: Optional[int] = None,
|
|
131
|
+
quiet_zone: Optional[int] = None,
|
|
132
|
+
font_size: Optional[int] = None,
|
|
133
|
+
draw_text: bool = True,
|
|
134
|
+
) -> Image.Image:
|
|
107
135
|
"""Creates a PIL Image from the binary string of the barcode.
|
|
108
136
|
|
|
109
137
|
Returns
|
|
@@ -111,33 +139,25 @@ class Code(Barcode):
|
|
|
111
139
|
A PIL Image with the barcode is returned to the caller.
|
|
112
140
|
"""
|
|
113
141
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
selected_size[0] + padding.width,
|
|
123
|
-
selected_size[1] + padding.height,
|
|
142
|
+
module_width, bar_height, quiet_zone, font_size, text_padding = (
|
|
143
|
+
self._get_render_options(
|
|
144
|
+
module_width=module_width,
|
|
145
|
+
bar_height=bar_height,
|
|
146
|
+
quiet_zone=quiet_zone,
|
|
147
|
+
font_size=font_size,
|
|
148
|
+
draw_text=draw_text,
|
|
149
|
+
)
|
|
124
150
|
)
|
|
125
151
|
|
|
126
|
-
# Get each column's width and height
|
|
127
|
-
column_size = self._get_column_size()
|
|
128
|
-
|
|
129
|
-
# Create a white base image to paste the barcode on
|
|
130
|
-
base = Image.new("RGB", padded_size, (255, 255, 255))
|
|
131
|
-
|
|
132
152
|
# Create the image to write the columns
|
|
133
153
|
img = Image.new(
|
|
134
154
|
"RGB",
|
|
135
|
-
(
|
|
155
|
+
(module_width * self.BARCODE_COLUMN_NUMBER, bar_height),
|
|
136
156
|
(255, 255, 255),
|
|
137
157
|
)
|
|
138
158
|
|
|
139
159
|
# This is the spacing we are going to add after each digit
|
|
140
|
-
space = Image.new("RGB", (
|
|
160
|
+
space = Image.new("RGB", (module_width, img.height), (255, 255, 255))
|
|
141
161
|
|
|
142
162
|
# Create a binary string representation of the barcode digits
|
|
143
163
|
binary_string = self.get_binary_string
|
|
@@ -151,12 +171,12 @@ class Code(Barcode):
|
|
|
151
171
|
# If the character is a ` ` (space) then we write 3 times the size on the spacing
|
|
152
172
|
# We also need to add a single width column of spacing after each digit
|
|
153
173
|
if digit == "1":
|
|
154
|
-
column_width =
|
|
174
|
+
column_width = module_width * 3
|
|
155
175
|
elif digit == " ":
|
|
156
|
-
column_width =
|
|
176
|
+
column_width = module_width * 3
|
|
157
177
|
color = (255, 255, 255)
|
|
158
178
|
else:
|
|
159
|
-
column_width =
|
|
179
|
+
column_width = module_width
|
|
160
180
|
|
|
161
181
|
# First paste the column
|
|
162
182
|
column = Image.new("RGB", (column_width, img.height), color)
|
|
@@ -171,31 +191,30 @@ class Code(Barcode):
|
|
|
171
191
|
# Crop redundant whitespace after barcode
|
|
172
192
|
img = img.crop((0, 0, index, img.height))
|
|
173
193
|
|
|
194
|
+
base = Image.new(
|
|
195
|
+
"RGB",
|
|
196
|
+
(img.width + quiet_zone * 2, bar_height + text_padding),
|
|
197
|
+
(255, 255, 255),
|
|
198
|
+
)
|
|
199
|
+
|
|
174
200
|
# Paste the barcode on the center of the padded base
|
|
175
201
|
Point = namedtuple("Point", "x y")
|
|
176
202
|
base_center = Point(base.width // 2, base.height // 2)
|
|
177
|
-
img_center = Point(img.width // 2, img.height // 2)
|
|
178
203
|
|
|
179
|
-
base.paste(img, (
|
|
204
|
+
base.paste(img, (quiet_zone, text_padding // 2))
|
|
180
205
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
font_path = str(cwd.joinpath(cwd, "fonts", "arial.ttf"))
|
|
206
|
+
if not draw_text:
|
|
207
|
+
return base
|
|
184
208
|
|
|
185
209
|
draw = ImageDraw.Draw(base)
|
|
186
|
-
font =
|
|
210
|
+
font = self._get_default_font(font_size)
|
|
187
211
|
|
|
188
212
|
text_width = draw.textlength(self.code, font)
|
|
189
213
|
x = base_center.x - text_width // 2
|
|
190
|
-
y =
|
|
214
|
+
y = text_padding // 2 + img.height
|
|
191
215
|
|
|
192
216
|
draw.text((x, y), self.code, (0, 0, 0), font=font)
|
|
193
217
|
|
|
194
|
-
# And now we crop spare white space around base image
|
|
195
|
-
start_index = (base_center.x - img_center.x) - padding.width // 2
|
|
196
|
-
end_index = (base_center.x - img_center.x) + padding.width // 2
|
|
197
|
-
|
|
198
|
-
base.crop((start_index, 0, end_index, base.height))
|
|
199
218
|
return base
|
|
200
219
|
|
|
201
220
|
def _convert_to_binary(self, string: str) -> str:
|
|
@@ -225,17 +244,7 @@ class Code(Barcode):
|
|
|
225
244
|
and the check digit is calculated if not given
|
|
226
245
|
"""
|
|
227
246
|
|
|
228
|
-
|
|
229
|
-
if char not in CODEXCoding.CODES or char == "_":
|
|
230
|
-
raise IncorrectFormat(
|
|
231
|
-
f"Character {char} is not supported by {self.__class__.__name__}"
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
reference = self.calculate_checksum(self.code)
|
|
235
|
-
checkchar = CODEXCoding.REFERENCE_NUMBERS[reference]
|
|
236
|
-
|
|
237
|
-
self.checksum = checkchar
|
|
238
|
-
return self.code + str(checkchar)
|
|
247
|
+
return self.normalize(self.code)
|
|
239
248
|
|
|
240
249
|
def _trim(self, code: str) -> str:
|
|
241
250
|
"""Removes the start and stop characters from the barcode
|
|
@@ -282,5 +291,5 @@ class CODE39(Code):
|
|
|
282
291
|
BARCODE_FONT_SIZE = 30
|
|
283
292
|
BARCODE_PADDING = Size(50, 100)
|
|
284
293
|
|
|
285
|
-
def __init__(self, barcode:
|
|
286
|
-
super().__init__(barcode
|
|
294
|
+
def __init__(self, barcode: BarcodeInput):
|
|
295
|
+
super().__init__(barcode)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from collections import namedtuple
|
|
2
2
|
from typing import Union
|
|
3
3
|
|
|
4
|
-
from .barcode import Barcode
|
|
4
|
+
from .barcode import Barcode, BarcodeInput
|
|
5
5
|
from .codings import ean as EANCoding
|
|
6
6
|
from .exceptions import IncorrectFormat
|
|
7
7
|
|
|
@@ -15,20 +15,30 @@ class EAN(Barcode):
|
|
|
15
15
|
Shouldn't be used directly and it's subclasses are preferred
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
def __init__(self, barcode:
|
|
18
|
+
def __init__(self, barcode: BarcodeInput):
|
|
19
19
|
super().__init__(barcode)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
@classmethod
|
|
22
|
+
def validate(cls, barcode: BarcodeInput) -> None:
|
|
23
|
+
code = str(barcode)
|
|
24
|
+
|
|
25
|
+
if not code.isdigit():
|
|
22
26
|
raise IncorrectFormat("Barcode can't contain non-digit characters.")
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
classname
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
if len(code) < cls.BARCODE_LENGTH:
|
|
29
|
+
classname = cls.__name__
|
|
30
|
+
error = (
|
|
31
|
+
f"{classname} should be at least {cls.BARCODE_LENGTH} digits long, "
|
|
32
|
+
f"not {len(code)}."
|
|
33
|
+
)
|
|
34
|
+
raise IncorrectFormat(error)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def normalize(cls, barcode: BarcodeInput) -> str:
|
|
38
|
+
cls.validate(barcode)
|
|
39
|
+
code = str(barcode)[: cls.BARCODE_LENGTH]
|
|
40
|
+
check_digit = cls.calculate_checksum(code)
|
|
41
|
+
return code + str(check_digit)
|
|
32
42
|
|
|
33
43
|
@property
|
|
34
44
|
def get_binary_string(self) -> str:
|
|
@@ -112,31 +122,21 @@ class EAN(Barcode):
|
|
|
112
122
|
else:
|
|
113
123
|
raise TypeError(f"Can't accept type {type(barcode)}")
|
|
114
124
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# Here there is no check digit so it's calculated
|
|
118
|
-
digits = list(map(int, list(barcode)))
|
|
119
|
-
|
|
120
|
-
# Get even and odd indeces of the digits
|
|
121
|
-
weighted_odd = digits[1::2]
|
|
122
|
-
weighted_even = digits[::2]
|
|
125
|
+
cls.validate(barcode)
|
|
126
|
+
barcode = barcode[: cls.BARCODE_LENGTH]
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
sum(weighted_odd) * cls.WEIGHTS.ODD
|
|
127
|
-
+ sum(weighted_even) * cls.WEIGHTS.EVEN
|
|
128
|
-
)
|
|
129
|
-
if checksum % 10 == 0:
|
|
130
|
-
return 0
|
|
128
|
+
# Here there is no check digit so it's calculated
|
|
129
|
+
digits = list(map(int, list(barcode)))
|
|
131
130
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return closest10 % checksum
|
|
131
|
+
# Get even and odd indeces of the digits
|
|
132
|
+
weighted_odd = digits[1::2]
|
|
133
|
+
weighted_even = digits[::2]
|
|
136
134
|
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
# Calculate the checksum
|
|
136
|
+
checksum = (
|
|
137
|
+
sum(weighted_odd) * cls.WEIGHTS.ODD + sum(weighted_even) * cls.WEIGHTS.EVEN
|
|
139
138
|
)
|
|
139
|
+
return (10 - checksum % 10) % 10
|
|
140
140
|
|
|
141
141
|
def _get_column_size(self) -> int:
|
|
142
142
|
"""Finds and returns what the width of each column should be
|
|
@@ -157,11 +157,7 @@ class EAN(Barcode):
|
|
|
157
157
|
and the check digit is calculated if not given
|
|
158
158
|
"""
|
|
159
159
|
if len(self.code) >= self.BARCODE_LENGTH:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
# Calculate the checksum digit
|
|
163
|
-
check_digit = self.calculate_checksum(code)
|
|
164
|
-
return code + str(check_digit)
|
|
160
|
+
return self.normalize(self.code)
|
|
165
161
|
|
|
166
162
|
|
|
167
163
|
class EAN14(EAN):
|
|
@@ -191,7 +187,7 @@ class EAN14(EAN):
|
|
|
191
187
|
WEIGHTS = Weights(1, 3)
|
|
192
188
|
HAS_STRUCTURE = True
|
|
193
189
|
|
|
194
|
-
def __init__(self, barcode:
|
|
190
|
+
def __init__(self, barcode: BarcodeInput):
|
|
195
191
|
super().__init__(barcode)
|
|
196
192
|
|
|
197
193
|
|
|
@@ -222,7 +218,7 @@ class EAN13(EAN):
|
|
|
222
218
|
WEIGHTS = Weights(3, 1)
|
|
223
219
|
HAS_STRUCTURE = True
|
|
224
220
|
|
|
225
|
-
def __init__(self, barcode:
|
|
221
|
+
def __init__(self, barcode: BarcodeInput):
|
|
226
222
|
super().__init__(barcode)
|
|
227
223
|
|
|
228
224
|
|
|
@@ -253,7 +249,7 @@ class EAN8(EAN):
|
|
|
253
249
|
WEIGHTS = Weights(1, 3)
|
|
254
250
|
HAS_STRUCTURE = False
|
|
255
251
|
|
|
256
|
-
def __init__(self, barcode:
|
|
252
|
+
def __init__(self, barcode: BarcodeInput):
|
|
257
253
|
super().__init__(barcode)
|
|
258
254
|
|
|
259
255
|
|
|
@@ -274,7 +270,17 @@ class JAN(EAN13, EAN):
|
|
|
274
270
|
The padding around the actual barcode
|
|
275
271
|
"""
|
|
276
272
|
|
|
277
|
-
|
|
273
|
+
@classmethod
|
|
274
|
+
def validate(cls, barcode: BarcodeInput) -> None:
|
|
275
|
+
super().validate(barcode)
|
|
276
|
+
|
|
277
|
+
code = str(barcode)
|
|
278
|
+
if code[:2] not in ("45", "49"):
|
|
279
|
+
raise IncorrectFormat(
|
|
280
|
+
"JAN type barcodes need to start with country code 45 or 49."
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def __init__(self, barcode: BarcodeInput):
|
|
278
284
|
super().__init__(barcode)
|
|
279
285
|
|
|
280
286
|
if self.code[:2] not in ("45", "49"):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561 typed packages.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pybarcodes
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: A Python package for barcode generation
|
|
5
5
|
Author-email: atbuy <buy@atbuy.dev>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -48,7 +48,7 @@ pybarcodes
|
|
|
48
48
|
:alt: PyPI supported Python versions
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
This is a
|
|
51
|
+
This is a Python package to create barcodes.
|
|
52
52
|
You can create file-like objects, text files and images from just a barcode number.
|
|
53
53
|
Image generation is fast so it can be used to create images in bulk.
|
|
54
54
|
|
|
@@ -6,6 +6,7 @@ pybarcodes/barcode.py
|
|
|
6
6
|
pybarcodes/codes.py
|
|
7
7
|
pybarcodes/ean.py
|
|
8
8
|
pybarcodes/exceptions.py
|
|
9
|
+
pybarcodes/py.typed
|
|
9
10
|
pybarcodes.egg-info/PKG-INFO
|
|
10
11
|
pybarcodes.egg-info/SOURCES.txt
|
|
11
12
|
pybarcodes.egg-info/dependency_links.txt
|
|
@@ -14,6 +15,7 @@ pybarcodes.egg-info/top_level.txt
|
|
|
14
15
|
pybarcodes/codings/__init__.py
|
|
15
16
|
pybarcodes/codings/codex.py
|
|
16
17
|
pybarcodes/codings/ean.py
|
|
17
|
-
pybarcodes/fonts/arial.ttf
|
|
18
18
|
tests/test_codes.py
|
|
19
|
-
tests/test_ean.py
|
|
19
|
+
tests/test_ean.py
|
|
20
|
+
tests/test_images.py
|
|
21
|
+
tests/test_spec_fixtures.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pybarcodes"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.2"
|
|
4
4
|
description = "A Python package for barcode generation"
|
|
5
5
|
authors = [{ name = "atbuy", email = "buy@atbuy.dev" }]
|
|
6
6
|
license = "MIT"
|
|
@@ -35,6 +35,7 @@ dependencies = ["pillow>8,<13"]
|
|
|
35
35
|
|
|
36
36
|
[dependency-groups]
|
|
37
37
|
dev = ["pre-commit==4.6.0", "ruff==0.15.12"]
|
|
38
|
+
docs = ["numpydoc==1.8.0", "sphinx-rtd-theme==3.0.2"]
|
|
38
39
|
tests = ["pytest==9.0.3", "pytest-cov==7.1.0"]
|
|
39
40
|
|
|
40
41
|
[project.urls]
|
|
@@ -44,11 +45,14 @@ Repository = "https://github.com/atbuy/pybarcodes"
|
|
|
44
45
|
"Source Code" = "https://github.com/atbuy/pybarcodes"
|
|
45
46
|
"CI/CD" = "https://github.com/atbuy/pybarcodes/actions"
|
|
46
47
|
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
addopts = "--cov-fail-under=90"
|
|
50
|
+
|
|
47
51
|
[tool.setuptools.packages.find]
|
|
48
52
|
include = ["pybarcodes*"]
|
|
49
53
|
|
|
50
54
|
[tool.setuptools.package-data]
|
|
51
|
-
pybarcodes = ["
|
|
55
|
+
pybarcodes = ["py.typed"]
|
|
52
56
|
|
|
53
57
|
[build-system]
|
|
54
58
|
requires = ["setuptools"]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
1
3
|
from pybarcodes import CODE39
|
|
2
4
|
from pybarcodes.exceptions import IncorrectFormat
|
|
3
5
|
|
|
@@ -15,12 +17,17 @@ def test_code39():
|
|
|
15
17
|
|
|
16
18
|
assert barcode.calculate_checksum(code) == 40
|
|
17
19
|
assert barcode.calculate_checksum() == 40
|
|
20
|
+
assert CODE39.normalize("abc123") == CODE39("abc123").code
|
|
21
|
+
assert CODE39(123).code == CODE39.normalize(123)
|
|
22
|
+
assert CODE39.validate("abc123") is None
|
|
23
|
+
|
|
24
|
+
with pytest.raises(IncorrectFormat):
|
|
25
|
+
CODE39("^")
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
pass
|
|
27
|
+
with pytest.raises(IncorrectFormat):
|
|
28
|
+
barcode.calculate_checksum("^")
|
|
29
|
+
with pytest.raises(IncorrectFormat):
|
|
30
|
+
CODE39.validate("^")
|
|
24
31
|
|
|
25
32
|
binary_string = barcode.get_binary_string
|
|
26
33
|
start_char = binary_string[:6]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
1
3
|
from pybarcodes import EAN8, EAN13, EAN14, JAN
|
|
2
4
|
from pybarcodes.exceptions import IncorrectFormat
|
|
3
5
|
|
|
@@ -20,6 +22,8 @@ def test_ean13():
|
|
|
20
22
|
|
|
21
23
|
assert barcode == code + "1"
|
|
22
24
|
assert barcode == barcode2
|
|
25
|
+
assert EAN13.normalize(code) == code + "1"
|
|
26
|
+
assert EAN13.validate(code) is None
|
|
23
27
|
|
|
24
28
|
# Check if the checksum digit is calculated correctly
|
|
25
29
|
# The check digit should be `1`
|
|
@@ -32,11 +36,8 @@ def test_ean13():
|
|
|
32
36
|
# The check digit is calculated when instansiating
|
|
33
37
|
assert barcode == "4006381333931"
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
barcode = EAN13(code)
|
|
38
|
-
except IncorrectFormat:
|
|
39
|
-
pass
|
|
39
|
+
with pytest.raises(IncorrectFormat):
|
|
40
|
+
EAN13("1")
|
|
40
41
|
|
|
41
42
|
# Check if guards are in the correct positions
|
|
42
43
|
binary_string = barcode.get_binary_string
|
|
@@ -48,6 +49,11 @@ def test_ean13():
|
|
|
48
49
|
assert center_guard == "01010"
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
def test_ean13_checksum_edge_cases():
|
|
53
|
+
assert EAN13.calculate_checksum("000000000001") == 7
|
|
54
|
+
assert EAN13("000000000001") == "0000000000017"
|
|
55
|
+
|
|
56
|
+
|
|
51
57
|
def test_ean8():
|
|
52
58
|
code = "0123456"
|
|
53
59
|
barcode = EAN8(code)
|
|
@@ -72,11 +78,8 @@ def test_ean8():
|
|
|
72
78
|
# The check digit is calculated when instansiating
|
|
73
79
|
assert barcode == "01234565"
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
barcode = EAN8(code)
|
|
78
|
-
except IncorrectFormat:
|
|
79
|
-
pass
|
|
81
|
+
with pytest.raises(IncorrectFormat):
|
|
82
|
+
EAN8("1")
|
|
80
83
|
|
|
81
84
|
# Check if guards in correct positions
|
|
82
85
|
binary_string = barcode.get_binary_string
|
|
@@ -88,6 +91,11 @@ def test_ean8():
|
|
|
88
91
|
assert center_guard == "01010"
|
|
89
92
|
|
|
90
93
|
|
|
94
|
+
def test_ean8_checksum_edge_cases():
|
|
95
|
+
assert EAN8.calculate_checksum("0000001") == 7
|
|
96
|
+
assert EAN8("0000001") == "00000017"
|
|
97
|
+
|
|
98
|
+
|
|
91
99
|
def test_ean14():
|
|
92
100
|
code = "4070071967072013242346"
|
|
93
101
|
barcode = EAN14(code)
|
|
@@ -109,11 +117,8 @@ def test_ean14():
|
|
|
109
117
|
# The check digit is calculated when instansiating
|
|
110
118
|
assert barcode == "40700719670720"
|
|
111
119
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
barcode = EAN14(code)
|
|
115
|
-
except IncorrectFormat:
|
|
116
|
-
pass
|
|
120
|
+
with pytest.raises(IncorrectFormat):
|
|
121
|
+
EAN14("1")
|
|
117
122
|
|
|
118
123
|
# Check if guards in correct positions
|
|
119
124
|
binary_string = barcode.get_binary_string
|
|
@@ -125,6 +130,11 @@ def test_ean14():
|
|
|
125
130
|
assert center_guard == "01010"
|
|
126
131
|
|
|
127
132
|
|
|
133
|
+
def test_ean14_checksum_edge_cases():
|
|
134
|
+
assert EAN14.calculate_checksum("0000000000001") == 7
|
|
135
|
+
assert EAN14("0000000000001") == "00000000000017"
|
|
136
|
+
|
|
137
|
+
|
|
128
138
|
def test_jan():
|
|
129
139
|
code = "450638133393"
|
|
130
140
|
barcode = JAN(code)
|
|
@@ -143,6 +153,8 @@ def test_jan():
|
|
|
143
153
|
|
|
144
154
|
assert barcode == code + "6"
|
|
145
155
|
assert barcode == barcode2
|
|
156
|
+
assert JAN.normalize(code) == code + "6"
|
|
157
|
+
assert JAN.validate(code) is None
|
|
146
158
|
|
|
147
159
|
# Check if the checksum digit is calculated correctly
|
|
148
160
|
# The check digit should be `1`
|
|
@@ -154,11 +166,8 @@ def test_jan():
|
|
|
154
166
|
barcode = JAN(code)
|
|
155
167
|
# The check digit is calculated when instansiating
|
|
156
168
|
assert barcode == "4506381333936"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
barcode = JAN(code)
|
|
160
|
-
except IncorrectFormat:
|
|
161
|
-
pass
|
|
169
|
+
with pytest.raises(IncorrectFormat):
|
|
170
|
+
JAN("1")
|
|
162
171
|
|
|
163
172
|
# Check if guards are in the correct positions
|
|
164
173
|
binary_string = barcode.get_binary_string
|
|
@@ -168,3 +177,12 @@ def test_jan():
|
|
|
168
177
|
assert left_guard == "101"
|
|
169
178
|
assert right_guard == "101"
|
|
170
179
|
assert center_guard == "01010"
|
|
180
|
+
|
|
181
|
+
with pytest.raises(IncorrectFormat):
|
|
182
|
+
JAN.validate("120638133393")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@pytest.mark.parametrize("barcode_type", [EAN8, EAN13, EAN14])
|
|
186
|
+
def test_ean_calculate_checksum_rejects_non_digits(barcode_type):
|
|
187
|
+
with pytest.raises(IncorrectFormat):
|
|
188
|
+
barcode_type.calculate_checksum("x" * barcode_type.BARCODE_LENGTH)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from PIL import Image
|
|
6
|
+
|
|
7
|
+
from pybarcodes import CODE39, EAN13
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def assert_barcode_image(image: Image.Image) -> None:
|
|
11
|
+
assert image.mode == "RGB"
|
|
12
|
+
assert image.width > 0
|
|
13
|
+
assert image.height > 0
|
|
14
|
+
assert image.getbbox() == (0, 0, image.width, image.height)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_ean_image_generation_and_save(tmp_path: Path):
|
|
18
|
+
barcode = EAN13("400638133393")
|
|
19
|
+
|
|
20
|
+
image = barcode.render()
|
|
21
|
+
assert_barcode_image(image)
|
|
22
|
+
assert_barcode_image(barcode.image)
|
|
23
|
+
|
|
24
|
+
output_path = tmp_path / "ean13.png"
|
|
25
|
+
saved_image = barcode.save(
|
|
26
|
+
str(output_path), module_width=3, bar_height=80, quiet_zone=20, draw_text=False
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
assert output_path.exists()
|
|
30
|
+
assert saved_image.size == (len(barcode.get_binary_string) * 3 + 40, 80)
|
|
31
|
+
with Image.open(output_path) as image_file:
|
|
32
|
+
assert image_file.size == saved_image.size
|
|
33
|
+
assert image_file.mode == "RGB"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_code39_image_generation():
|
|
37
|
+
barcode = CODE39("ABC123")
|
|
38
|
+
|
|
39
|
+
image = barcode.image
|
|
40
|
+
|
|
41
|
+
assert_barcode_image(image)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_renderer_controls():
|
|
45
|
+
barcode = EAN13("400638133393")
|
|
46
|
+
|
|
47
|
+
image = barcode.render(
|
|
48
|
+
module_width=4,
|
|
49
|
+
bar_height=120,
|
|
50
|
+
quiet_zone=12,
|
|
51
|
+
font_size=16,
|
|
52
|
+
draw_text=False,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert image.size == (len(barcode.get_binary_string) * 4 + 24, 120)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_renderer_rejects_invalid_controls():
|
|
59
|
+
barcode = EAN13("400638133393")
|
|
60
|
+
|
|
61
|
+
with pytest.raises(ValueError):
|
|
62
|
+
barcode.render(module_width=0)
|
|
63
|
+
with pytest.raises(ValueError):
|
|
64
|
+
barcode.render(bar_height=0)
|
|
65
|
+
with pytest.raises(ValueError):
|
|
66
|
+
barcode.render(quiet_zone=0)
|
|
67
|
+
with pytest.raises(ValueError):
|
|
68
|
+
barcode.render(font_size=0)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_text_outputs(tmp_path: Path):
|
|
72
|
+
barcode = EAN13("400638133393")
|
|
73
|
+
|
|
74
|
+
output_path = tmp_path / "ean13.txt"
|
|
75
|
+
barcode.write(str(output_path))
|
|
76
|
+
|
|
77
|
+
assert output_path.read_text() == "4006381333931"
|
|
78
|
+
assert barcode.to_text_bytes() == b"4006381333931"
|
|
79
|
+
assert isinstance(barcode.to_text_bytesio(), BytesIO)
|
|
80
|
+
assert barcode.to_text_bytesio().read() == b"4006381333931"
|
|
81
|
+
assert isinstance(barcode.to_bytesio(), BytesIO)
|
|
82
|
+
assert barcode.to_bytesio().read() == b"4006381333931"
|
|
83
|
+
assert str(barcode) == "<EAN13(code=4006381333931)>"
|
|
84
|
+
assert repr(barcode) == "<EAN13(code=4006381333931)>"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_image_bytes_outputs():
|
|
88
|
+
barcode = EAN13("400638133393")
|
|
89
|
+
|
|
90
|
+
image_bytes = barcode.to_image_bytes(
|
|
91
|
+
module_width=3, bar_height=80, quiet_zone=20, draw_text=False
|
|
92
|
+
)
|
|
93
|
+
assert image_bytes.startswith(b"\x89PNG")
|
|
94
|
+
|
|
95
|
+
image_buffer = barcode.to_image_bytesio(
|
|
96
|
+
module_width=3, bar_height=80, quiet_zone=20, draw_text=False
|
|
97
|
+
)
|
|
98
|
+
assert isinstance(image_buffer, BytesIO)
|
|
99
|
+
with Image.open(image_buffer) as image_file:
|
|
100
|
+
assert image_file.size == (len(barcode.get_binary_string) * 3 + 40, 80)
|
|
101
|
+
assert image_file.mode == "RGB"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from pybarcodes import CODE39, EAN8, EAN13, EAN14
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# GS1 check digit algorithm reference:
|
|
7
|
+
# https://www.gs1.org/services/how-calculate-check-digit-manually
|
|
8
|
+
@pytest.mark.parametrize(
|
|
9
|
+
("barcode_type", "payload", "expected_code"),
|
|
10
|
+
[
|
|
11
|
+
(EAN8, "9638507", "96385074"),
|
|
12
|
+
(EAN13, "629104150021", "6291041500213"),
|
|
13
|
+
(EAN14, "1061414145678", "10614141456786"),
|
|
14
|
+
],
|
|
15
|
+
)
|
|
16
|
+
def test_gs1_check_digit_fixtures(barcode_type, payload, expected_code):
|
|
17
|
+
assert barcode_type.normalize(payload) == expected_code
|
|
18
|
+
assert barcode_type(payload).code == expected_code
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_ean8_binary_pattern_fixture():
|
|
22
|
+
barcode = EAN8("9638507")
|
|
23
|
+
|
|
24
|
+
assert barcode.get_binary_string == (
|
|
25
|
+
"1010001011010111101111010110111010101001110111001010001001011100101"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_ean13_binary_pattern_fixture():
|
|
30
|
+
barcode = EAN13("629104150021")
|
|
31
|
+
|
|
32
|
+
assert barcode.get_binary_string == (
|
|
33
|
+
"101"
|
|
34
|
+
"0010011"
|
|
35
|
+
"0010111"
|
|
36
|
+
"0110011"
|
|
37
|
+
"0100111"
|
|
38
|
+
"0100011"
|
|
39
|
+
"0011001"
|
|
40
|
+
"01010"
|
|
41
|
+
"1001110"
|
|
42
|
+
"1110010"
|
|
43
|
+
"1110010"
|
|
44
|
+
"1101100"
|
|
45
|
+
"1100110"
|
|
46
|
+
"1000010"
|
|
47
|
+
"101"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Code 39 MOD43 reference:
|
|
52
|
+
# https://docshield.tungstenautomation.com/ControlSuite/en_US/1.5.0-uh5vow9jo0/help/QCP/Dita/ACSCChecksum.html
|
|
53
|
+
def test_code39_mod43_check_digit_fixture():
|
|
54
|
+
assert CODE39.normalize("+1110001111") == "+11100011115"
|
|
55
|
+
assert CODE39("+1110001111").calculate_checksum("+1110001111") == 5
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
from collections import namedtuple
|
|
2
|
-
from io import BytesIO
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Union
|
|
5
|
-
|
|
6
|
-
from PIL import Image, ImageDraw, ImageFont
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Barcode:
|
|
10
|
-
"""A base class for all barcode types"""
|
|
11
|
-
|
|
12
|
-
def __init__(self, barcode: Union[str, int]):
|
|
13
|
-
self.code = str(barcode)
|
|
14
|
-
|
|
15
|
-
@property
|
|
16
|
-
def image(self) -> Image.Image:
|
|
17
|
-
"""Retrieves and returns the PIL.Image object with the barcode
|
|
18
|
-
|
|
19
|
-
Returns
|
|
20
|
-
-------
|
|
21
|
-
PIl.Image.Image:
|
|
22
|
-
The barcode image
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
return self._get_barcode_image()
|
|
26
|
-
|
|
27
|
-
def save(self, path: str, size: tuple = None) -> Image.Image:
|
|
28
|
-
"""Create a PIL Image object and save it to the path given.
|
|
29
|
-
|
|
30
|
-
It also returns that image object to the caller.
|
|
31
|
-
|
|
32
|
-
Parameters
|
|
33
|
-
----------
|
|
34
|
-
path: str
|
|
35
|
-
The path to save the image to
|
|
36
|
-
|
|
37
|
-
Returns
|
|
38
|
-
-------
|
|
39
|
-
Returns a PIL Image object to the caller
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
img = self._get_barcode_image()
|
|
43
|
-
|
|
44
|
-
if size is not None:
|
|
45
|
-
img = img.resize(size)
|
|
46
|
-
img.save(path)
|
|
47
|
-
|
|
48
|
-
def show(self) -> None:
|
|
49
|
-
"""Shows the barcode image"""
|
|
50
|
-
|
|
51
|
-
img = self._get_barcode_image()
|
|
52
|
-
img.show()
|
|
53
|
-
|
|
54
|
-
def to_bytesio(self) -> BytesIO:
|
|
55
|
-
"""
|
|
56
|
-
Write the barcode to a BytesIO object
|
|
57
|
-
|
|
58
|
-
Returns
|
|
59
|
-
-------
|
|
60
|
-
Returns the BytesIO object created
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
obj = BytesIO()
|
|
64
|
-
obj.write(self.code.encode("ascii"))
|
|
65
|
-
obj.seek(0)
|
|
66
|
-
return obj
|
|
67
|
-
|
|
68
|
-
def write(self, path: str) -> None:
|
|
69
|
-
"""
|
|
70
|
-
Tries to save the barcode to a text file
|
|
71
|
-
|
|
72
|
-
Parameters
|
|
73
|
-
----------
|
|
74
|
-
path: str
|
|
75
|
-
The path of the file
|
|
76
|
-
"""
|
|
77
|
-
with open(path, "w") as file:
|
|
78
|
-
file.write(self.code)
|
|
79
|
-
|
|
80
|
-
def _get_barcode_image(self) -> Image.Image:
|
|
81
|
-
"""Creates a PIL Image from the binary string of the barcode
|
|
82
|
-
|
|
83
|
-
Returns
|
|
84
|
-
-------
|
|
85
|
-
A PIL Image with the barcode is returned to the caller.
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
|
-
# Get the padding around the barcode
|
|
89
|
-
padding = self.BARCODE_PADDING
|
|
90
|
-
|
|
91
|
-
# Get the final image's width and height
|
|
92
|
-
selected_size, font_size = self.BARCODE_SIZE, self.BARCODE_FONT_SIZE
|
|
93
|
-
|
|
94
|
-
# This is for the white space around the barcode
|
|
95
|
-
padded_size = (
|
|
96
|
-
selected_size[0] + padding.width,
|
|
97
|
-
selected_size[1] + padding.height,
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
# Get each column's width and height
|
|
101
|
-
column_size = self._get_column_size()
|
|
102
|
-
|
|
103
|
-
# Create a white base image to write columns
|
|
104
|
-
base = Image.new("RGB", padded_size, (255, 255, 255))
|
|
105
|
-
|
|
106
|
-
# Create the image for the barcode
|
|
107
|
-
img = Image.new(
|
|
108
|
-
"RGB",
|
|
109
|
-
(column_size * self.BARCODE_COLUMN_NUMBER, selected_size[1]),
|
|
110
|
-
(255, 255, 255),
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
# Get the binary string representation of the barcode digits
|
|
114
|
-
binary_string = self.get_binary_string
|
|
115
|
-
|
|
116
|
-
index = 0
|
|
117
|
-
for digit in binary_string:
|
|
118
|
-
color = (0, 0, 0) if digit == "1" else (255, 255, 255)
|
|
119
|
-
column = Image.new("RGB", (column_size, img.height), color)
|
|
120
|
-
img.paste(column, (index, 0))
|
|
121
|
-
index += column_size
|
|
122
|
-
|
|
123
|
-
# Crop redundant whitespace after barcode
|
|
124
|
-
img = img.crop((0, 0, index, img.height))
|
|
125
|
-
|
|
126
|
-
# Paste the barcode on the center of the padded base
|
|
127
|
-
Point = namedtuple("Point", "x y")
|
|
128
|
-
base_center = Point(base.width // 2, base.height // 2)
|
|
129
|
-
img_center = Point(img.width // 2, img.height // 2)
|
|
130
|
-
|
|
131
|
-
base.paste(img, (base_center.x - img_center.x, base_center.y - img_center.y))
|
|
132
|
-
|
|
133
|
-
# Write the digits at the bottom
|
|
134
|
-
cwd = Path(__file__).parent
|
|
135
|
-
font_path = str(cwd.joinpath(cwd, "fonts", "arial.ttf"))
|
|
136
|
-
|
|
137
|
-
draw = ImageDraw.Draw(base)
|
|
138
|
-
font = ImageFont.truetype(font_path, font_size)
|
|
139
|
-
|
|
140
|
-
text_width = draw.textlength(self.code, font)
|
|
141
|
-
x = base_center.x - text_width // 2
|
|
142
|
-
y = base.height - (base.height - img.height) // 2
|
|
143
|
-
|
|
144
|
-
draw.text((x, y), self.code, (0, 0, 0), font=font)
|
|
145
|
-
return base
|
|
146
|
-
|
|
147
|
-
def __eq__(self, other):
|
|
148
|
-
if isinstance(other, self.__class__):
|
|
149
|
-
return self.code == other.code
|
|
150
|
-
elif isinstance(other, str):
|
|
151
|
-
return self.code == other
|
|
152
|
-
|
|
153
|
-
return False
|
|
154
|
-
|
|
155
|
-
def __str__(self) -> str:
|
|
156
|
-
return f"<{self.__class__.__name__}(code={self.code})>"
|
|
157
|
-
|
|
158
|
-
def __repr__(self):
|
|
159
|
-
return self.__str__()
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|