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.
Files changed (25) hide show
  1. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/PKG-INFO +2 -2
  2. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/README.rst +1 -1
  3. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/__init__.py +1 -1
  4. pybarcodes-1.2.2/pybarcodes/barcode.py +322 -0
  5. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/codes.py +76 -67
  6. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/ean.py +47 -41
  7. pybarcodes-1.2.2/pybarcodes/py.typed +1 -0
  8. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes.egg-info/PKG-INFO +2 -2
  9. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes.egg-info/SOURCES.txt +4 -2
  10. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pyproject.toml +6 -2
  11. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/tests/test_codes.py +12 -5
  12. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/tests/test_ean.py +38 -20
  13. pybarcodes-1.2.2/tests/test_images.py +101 -0
  14. pybarcodes-1.2.2/tests/test_spec_fixtures.py +55 -0
  15. pybarcodes-1.0.3/pybarcodes/barcode.py +0 -159
  16. pybarcodes-1.0.3/pybarcodes/fonts/arial.ttf +0 -0
  17. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/LICENSE +0 -0
  18. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/codings/__init__.py +0 -0
  19. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/codings/codex.py +0 -0
  20. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/codings/ean.py +0 -0
  21. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes/exceptions.py +0 -0
  22. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes.egg-info/dependency_links.txt +0 -0
  23. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes.egg-info/requires.txt +0 -0
  24. {pybarcodes-1.0.3 → pybarcodes-1.2.2}/pybarcodes.egg-info/top_level.txt +0 -0
  25. {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.0.3
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 python package to create and read barcodes
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 python package to create and read barcodes
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
 
@@ -7,7 +7,7 @@ __title__ = "pybarcodes"
7
7
  __author__ = "atbuy"
8
8
  __license__ = "MIT"
9
9
  __copyright__ = "Copyright 2021-present atbuy"
10
- __version__ = (1, 0, 3)
10
+ __version__ = (1, 2, 2)
11
11
 
12
12
  VersionInfo = namedtuple("VersionInfo", "major minor patch")
13
13
 
@@ -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 pathlib import Path
3
- from typing import Union
2
+ from typing import Optional, Union
4
3
 
5
- from PIL import Image, ImageDraw, ImageFont
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: Union[str, int]):
14
+ def __init__(self, barcode: BarcodeInput):
16
15
  super().__init__(barcode)
17
16
 
18
- self.code = self._clean_code()
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 or self.code:
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
- barcode = barcode.upper()
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(self) -> Image.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
- # Get the padding around the barcode
115
- padding = self.BARCODE_PADDING
116
-
117
- # Get the image's width and height
118
- selected_size, font_size = self.BARCODE_SIZE, self.BARCODE_FONT_SIZE
119
-
120
- # This is for the whitespace around the barcode
121
- padded_size = (
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
- (column_size * self.BARCODE_COLUMN_NUMBER, selected_size[1]),
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", (column_size, img.height), (255, 255, 255))
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 = round(column_size * 3)
174
+ column_width = module_width * 3
155
175
  elif digit == " ":
156
- column_width = round(column_size * 3)
176
+ column_width = module_width * 3
157
177
  color = (255, 255, 255)
158
178
  else:
159
- column_width = column_size
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, (base_center.x - img_center.x, base_center.y - img_center.y))
204
+ base.paste(img, (quiet_zone, text_padding // 2))
180
205
 
181
- # Write the digits at the bottom
182
- cwd = Path(__file__).parent
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 = ImageFont.truetype(font_path, font_size)
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 = base.height - (base.height - img.height) // 2
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
- for char in self.code:
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: Union[str, int]):
286
- super().__init__(barcode.upper())
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: Union[str, int]):
18
+ def __init__(self, barcode: BarcodeInput):
19
19
  super().__init__(barcode)
20
20
 
21
- if not self.code.isdigit():
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
- # Do some error checking
25
- if isinstance(self.code, str):
26
- if len(self.code) < self.BARCODE_LENGTH:
27
- classname = self.__class__.__name__
28
- error = f"{classname} should be at least {self.BARCODE_LENGTH} digits long, not {len(self.code)}."
29
- raise IncorrectFormat(error)
30
- else:
31
- self.code = self._clean_code()
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
- if len(barcode) >= cls.BARCODE_LENGTH:
116
- barcode = barcode[: cls.BARCODE_LENGTH]
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
- # Calculate the checksum
125
- checksum = (
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
- # Find the closest multiple of 10, that is equal to
133
- # or higher than the checksum and return the difference
134
- closest10 = ((checksum // 10) * 10) + 10
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
- raise IncorrectFormat(
138
- f"Barcode should be at least {cls.BARCODE_LENGTH} digits long."
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
- code = self.code[: self.BARCODE_LENGTH]
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: Union[str, int]):
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: Union[str, int]):
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: Union[str, int]):
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
- def __init__(self, barcode: Union[str, int]):
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.0.3
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 python package to create and read barcodes
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.0.3"
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 = ["fonts/*.ttf"]
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
- try:
20
- code = "^"
21
- barcode = CODE39(code)
22
- except IncorrectFormat:
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
- try:
36
- code = "1"
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
- try:
76
- code = "1"
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
- try:
113
- code = "1"
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
- try:
158
- code = "1"
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