pybarcodes 1.0.2__tar.gz → 1.2.1__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.
@@ -1,15 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybarcodes
3
- Version: 1.0.2
3
+ Version: 1.2.1
4
4
  Summary: A Python package for barcode generation
5
+ Author-email: atbuy <buy@atbuy.dev>
5
6
  License-Expression: MIT
6
- License-File: LICENSE
7
+ Project-URL: Homepage, https://github.com/atbuy/pybarcodes
8
+ Project-URL: Repository, https://github.com/atbuy/pybarcodes
9
+ Project-URL: Bug Tracker, https://github.com/atbuy/pybarcodes/issues
10
+ Project-URL: Source Code, https://github.com/atbuy/pybarcodes
11
+ Project-URL: CI/CD, https://github.com/atbuy/pybarcodes/actions
7
12
  Keywords: barcode,scanner,generator,ean13,code39,jan,python
8
- Author: atbuy
9
- Author-email: buy@atbuy.dev
10
- Requires-Python: >=3.10
11
13
  Classifier: Development Status :: 5 - Production/Stable
12
- Classifier: License :: OSI Approved :: MIT License
13
14
  Classifier: Intended Audience :: Developers
14
15
  Classifier: Natural Language :: English
15
16
  Classifier: Operating System :: OS Independent
@@ -23,13 +24,11 @@ Classifier: Topic :: Multimedia :: Graphics
23
24
  Classifier: Topic :: Software Development :: Libraries
24
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
26
  Classifier: Topic :: Utilities
26
- Requires-Dist: pillow (>8,<13)
27
- Project-URL: Bug Tracker, https://github.com/atbuy/pybarcodes/issues
28
- Project-URL: CI/CD, https://github.com/atbuy/pybarcodes/actions
29
- Project-URL: Homepage, https://github.com/atbuy/pybarcodes
30
- Project-URL: Repository, https://github.com/atbuy/pybarcodes
31
- Project-URL: Source Code, https://github.com/atbuy/pybarcodes
27
+ Requires-Python: >=3.10
32
28
  Description-Content-Type: text/x-rst
29
+ License-File: LICENSE
30
+ Requires-Dist: pillow<13,>8
31
+ Dynamic: license-file
33
32
 
34
33
  pybarcodes
35
34
  ==========
@@ -49,7 +48,7 @@ pybarcodes
49
48
  :alt: PyPI supported Python versions
50
49
 
51
50
 
52
- This is a python package to create and read barcodes
51
+ This is a Python package to create barcodes.
53
52
  You can create file-like objects, text files and images from just a barcode number.
54
53
  Image generation is fast so it can be used to create images in bulk.
55
54
 
@@ -70,7 +69,7 @@ PRs are welcome :)
70
69
  Installing
71
70
  -----------
72
71
 
73
- **Python 3.9 or higher is required**
72
+ **Python 3.10 or higher is required**
74
73
 
75
74
  To install the library you can run the following command:
76
75
 
@@ -134,4 +133,3 @@ Links
134
133
 
135
134
  - `Documentation <https://pybarcodes.readthedocs.io/en/latest/index.html>`_
136
135
  - `PyPi <https://pypi.org/project/pybarcodes/>`_
137
-
@@ -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
 
@@ -37,7 +37,7 @@ PRs are welcome :)
37
37
  Installing
38
38
  -----------
39
39
 
40
- **Python 3.9 or higher is required**
40
+ **Python 3.10 or higher is required**
41
41
 
42
42
  To install the library you can run the following command:
43
43
 
@@ -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, 1)
10
+ __version__ = (1, 2, 1)
11
11
 
12
12
  VersionInfo = namedtuple("VersionInfo", "major minor patch")
13
13
 
@@ -0,0 +1,311 @@
1
+ from collections import namedtuple
2
+ from io import BytesIO
3
+ from typing import Union
4
+
5
+ from PIL import Image, ImageDraw, ImageFont
6
+
7
+
8
+ class Barcode:
9
+ """A base class for all barcode types"""
10
+
11
+ def __init__(self, barcode: Union[str, int]):
12
+ self.code = self.normalize(barcode)
13
+
14
+ @classmethod
15
+ def validate(cls, barcode: Union[str, int]) -> None:
16
+ """Validate barcode input."""
17
+
18
+ @classmethod
19
+ def normalize(cls, barcode: Union[str, int]) -> str:
20
+ """Return the normalized barcode value used by the instance."""
21
+
22
+ cls.validate(barcode)
23
+ return str(barcode)
24
+
25
+ @property
26
+ def image(self) -> Image.Image:
27
+ """Retrieves and returns the PIL.Image object with the barcode
28
+
29
+ Returns
30
+ -------
31
+ PIl.Image.Image:
32
+ The barcode image
33
+ """
34
+
35
+ return self.render()
36
+
37
+ def render(
38
+ self,
39
+ size: tuple = None,
40
+ module_width: int = None,
41
+ bar_height: int = None,
42
+ quiet_zone: int = None,
43
+ font_size: int = None,
44
+ draw_text: bool = True,
45
+ ) -> Image.Image:
46
+ """Create a PIL Image object for the barcode."""
47
+
48
+ img = self._get_barcode_image(
49
+ module_width=module_width,
50
+ bar_height=bar_height,
51
+ quiet_zone=quiet_zone,
52
+ font_size=font_size,
53
+ draw_text=draw_text,
54
+ )
55
+ if size is not None:
56
+ resampling = getattr(Image, "Resampling", Image)
57
+ img = img.resize(size, resampling.NEAREST)
58
+ return img
59
+
60
+ def save(
61
+ self,
62
+ path: str,
63
+ size: tuple = None,
64
+ module_width: int = None,
65
+ bar_height: int = None,
66
+ quiet_zone: int = None,
67
+ font_size: int = None,
68
+ draw_text: bool = True,
69
+ **save_kwargs,
70
+ ) -> Image.Image:
71
+ """Create a PIL Image object and save it to the path given.
72
+
73
+ It also returns that image object to the caller.
74
+
75
+ Parameters
76
+ ----------
77
+ path: str
78
+ The path to save the image to
79
+
80
+ Returns
81
+ -------
82
+ Returns a PIL Image object to the caller
83
+ """
84
+
85
+ img = self.render(
86
+ size=size,
87
+ module_width=module_width,
88
+ bar_height=bar_height,
89
+ quiet_zone=quiet_zone,
90
+ font_size=font_size,
91
+ draw_text=draw_text,
92
+ )
93
+ img.save(path, **save_kwargs)
94
+ return img
95
+
96
+ def show(self) -> None:
97
+ """Shows the barcode image"""
98
+
99
+ self.render().show()
100
+
101
+ def to_text_bytes(self, encoding: str = "ascii") -> bytes:
102
+ """Return the normalized barcode text as bytes."""
103
+
104
+ return self.code.encode(encoding)
105
+
106
+ def to_text_bytesio(self, encoding: str = "ascii") -> BytesIO:
107
+ """
108
+ Write the barcode to a BytesIO object
109
+
110
+ Returns
111
+ -------
112
+ Returns the BytesIO object created
113
+ """
114
+
115
+ obj = BytesIO(self.to_text_bytes(encoding=encoding))
116
+ obj.seek(0)
117
+ return obj
118
+
119
+ def to_bytesio(self, encoding: str = "ascii") -> BytesIO:
120
+ """Return the normalized barcode text in a BytesIO object."""
121
+
122
+ return self.to_text_bytesio(encoding=encoding)
123
+
124
+ def to_image_bytesio(
125
+ self,
126
+ format: str = "PNG",
127
+ size: tuple = None,
128
+ module_width: int = None,
129
+ bar_height: int = None,
130
+ quiet_zone: int = None,
131
+ font_size: int = None,
132
+ draw_text: bool = True,
133
+ **save_kwargs,
134
+ ) -> BytesIO:
135
+ """Return the rendered barcode image in a BytesIO object."""
136
+
137
+ obj = BytesIO()
138
+ self.render(
139
+ size=size,
140
+ module_width=module_width,
141
+ bar_height=bar_height,
142
+ quiet_zone=quiet_zone,
143
+ font_size=font_size,
144
+ draw_text=draw_text,
145
+ ).save(obj, format=format, **save_kwargs)
146
+ obj.seek(0)
147
+ return obj
148
+
149
+ def to_image_bytes(
150
+ self,
151
+ format: str = "PNG",
152
+ size: tuple = None,
153
+ module_width: int = None,
154
+ bar_height: int = None,
155
+ quiet_zone: int = None,
156
+ font_size: int = None,
157
+ draw_text: bool = True,
158
+ **save_kwargs,
159
+ ) -> bytes:
160
+ """Return the rendered barcode image as bytes."""
161
+
162
+ return self.to_image_bytesio(
163
+ format=format,
164
+ size=size,
165
+ module_width=module_width,
166
+ bar_height=bar_height,
167
+ quiet_zone=quiet_zone,
168
+ font_size=font_size,
169
+ draw_text=draw_text,
170
+ **save_kwargs,
171
+ ).getvalue()
172
+
173
+ def write(self, path: str, encoding: str = "ascii") -> None:
174
+ """
175
+ Tries to save the barcode to a text file
176
+
177
+ Parameters
178
+ ----------
179
+ path: str
180
+ The path of the file
181
+ """
182
+ with open(path, "w", encoding=encoding) as file:
183
+ file.write(self.code)
184
+
185
+ @staticmethod
186
+ def _positive_int(value: int, name: str) -> int:
187
+ value = int(value)
188
+ if value <= 0:
189
+ raise ValueError(f"{name} must be greater than 0.")
190
+ return value
191
+
192
+ def _get_render_options(
193
+ self,
194
+ module_width: int = None,
195
+ bar_height: int = None,
196
+ quiet_zone: int = None,
197
+ font_size: int = None,
198
+ draw_text: bool = True,
199
+ ):
200
+ padding = self.BARCODE_PADDING
201
+ selected_size, default_font_size = self.BARCODE_SIZE, self.BARCODE_FONT_SIZE
202
+
203
+ module_width = (
204
+ self._get_column_size()
205
+ if module_width is None
206
+ else self._positive_int(module_width, "module_width")
207
+ )
208
+ bar_height = (
209
+ selected_size[1]
210
+ if bar_height is None
211
+ else self._positive_int(bar_height, "bar_height")
212
+ )
213
+ quiet_zone = (
214
+ padding.width // 2
215
+ if quiet_zone is None
216
+ else self._positive_int(quiet_zone, "quiet_zone")
217
+ )
218
+ font_size = (
219
+ default_font_size
220
+ if font_size is None
221
+ else self._positive_int(font_size, "font_size")
222
+ )
223
+ text_padding = padding.height if draw_text else 0
224
+
225
+ return module_width, bar_height, quiet_zone, font_size, text_padding
226
+
227
+ def _get_default_font(self, font_size: int) -> ImageFont.ImageFont:
228
+ try:
229
+ return ImageFont.load_default(size=font_size)
230
+ except TypeError:
231
+ return ImageFont.load_default()
232
+
233
+ def _get_barcode_image(
234
+ self,
235
+ module_width: int = None,
236
+ bar_height: int = None,
237
+ quiet_zone: int = None,
238
+ font_size: int = None,
239
+ draw_text: bool = True,
240
+ ) -> Image.Image:
241
+ """Creates a PIL Image from the binary string of the barcode
242
+
243
+ Returns
244
+ -------
245
+ A PIL Image with the barcode is returned to the caller.
246
+ """
247
+
248
+ module_width, bar_height, quiet_zone, font_size, text_padding = (
249
+ self._get_render_options(
250
+ module_width=module_width,
251
+ bar_height=bar_height,
252
+ quiet_zone=quiet_zone,
253
+ font_size=font_size,
254
+ draw_text=draw_text,
255
+ )
256
+ )
257
+
258
+ binary_string = self.get_binary_string
259
+
260
+ # Create the image for the barcode
261
+ img = Image.new(
262
+ "RGB",
263
+ (module_width * len(binary_string), bar_height),
264
+ (255, 255, 255),
265
+ )
266
+
267
+ index = 0
268
+ for digit in binary_string:
269
+ color = (0, 0, 0) if digit == "1" else (255, 255, 255)
270
+ column = Image.new("RGB", (module_width, img.height), color)
271
+ img.paste(column, (index, 0))
272
+ index += module_width
273
+
274
+ base = Image.new(
275
+ "RGB",
276
+ (img.width + quiet_zone * 2, bar_height + text_padding),
277
+ (255, 255, 255),
278
+ )
279
+
280
+ # Paste the barcode on the center of the padded base
281
+ Point = namedtuple("Point", "x y")
282
+ base_center = Point(base.width // 2, base.height // 2)
283
+
284
+ base.paste(img, (quiet_zone, text_padding // 2))
285
+
286
+ if not draw_text:
287
+ return base
288
+
289
+ draw = ImageDraw.Draw(base)
290
+ font = self._get_default_font(font_size)
291
+
292
+ text_width = draw.textlength(self.code, font)
293
+ x = base_center.x - text_width // 2
294
+ y = text_padding // 2 + img.height
295
+
296
+ draw.text((x, y), self.code, (0, 0, 0), font=font)
297
+ return base
298
+
299
+ def __eq__(self, other):
300
+ if isinstance(other, self.__class__):
301
+ return self.code == other.code
302
+ elif isinstance(other, str):
303
+ return self.code == other
304
+
305
+ return False
306
+
307
+ def __str__(self) -> str:
308
+ return f"<{self.__class__.__name__}(code={self.code})>"
309
+
310
+ def __repr__(self):
311
+ return self.__str__()
@@ -1,8 +1,7 @@
1
1
  from collections import namedtuple
2
- from pathlib import Path
3
2
  from typing import Union
4
3
 
5
- from PIL import Image, ImageDraw, ImageFont
4
+ from PIL import Image, ImageDraw
6
5
 
7
6
  from .barcode import Barcode
8
7
  from .codings import codex as CODEXCoding
@@ -15,7 +14,7 @@ class Code(Barcode):
15
14
  def __init__(self, barcode: Union[str, int]):
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: Union[str, int]) -> 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: Union[str, int]) -> 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.
@@ -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: int = None,
130
+ bar_height: int = None,
131
+ quiet_zone: int = None,
132
+ font_size: 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