from collections import namedtuple
from io import BytesIO
from os import PathLike
from typing import Any, Optional, Union
from PIL import Image, ImageDraw, ImageFont
BarcodeInput = Union[str, int]
PathInput = Union[str, PathLike[str]]
RenderSize = tuple[int, int]
RenderOptions = tuple[int, int, int, int, int]
[docs]
class Barcode:
"""A base class for all barcode types"""
BARCODE_SIZE: RenderSize
BARCODE_FONT_SIZE: int
BARCODE_COLUMN_NUMBER: int
BARCODE_PADDING: Any
def __init__(self, barcode: BarcodeInput):
self.code = self.normalize(barcode)
[docs]
@classmethod
def validate(cls, barcode: BarcodeInput) -> None:
"""Validate barcode input."""
[docs]
@classmethod
def normalize(cls, barcode: BarcodeInput) -> str:
"""Return the normalized barcode value used by the instance."""
cls.validate(barcode)
return str(barcode)
@property
def image(self) -> Image.Image:
"""Retrieves and returns the PIL.Image object with the barcode
Returns
-------
PIl.Image.Image:
The barcode image
"""
return self.render()
[docs]
def render(
self,
size: Optional[RenderSize] = None,
module_width: Optional[int] = None,
bar_height: Optional[int] = None,
quiet_zone: Optional[int] = None,
font_size: Optional[int] = None,
draw_text: bool = True,
) -> Image.Image:
"""Create a PIL Image object for the barcode."""
img = self._get_barcode_image(
module_width=module_width,
bar_height=bar_height,
quiet_zone=quiet_zone,
font_size=font_size,
draw_text=draw_text,
)
if size is not None:
resampling = getattr(Image, "Resampling", Image)
img = img.resize(size, resampling.NEAREST)
return img
[docs]
def save(
self,
path: PathInput,
size: Optional[RenderSize] = None,
module_width: Optional[int] = None,
bar_height: Optional[int] = None,
quiet_zone: Optional[int] = None,
font_size: Optional[int] = None,
draw_text: bool = True,
**save_kwargs: Any,
) -> Image.Image:
"""Create a PIL Image object and save it to the path given.
It also returns that image object to the caller.
Parameters
----------
path: str
The path to save the image to
Returns
-------
Returns a PIL Image object to the caller
"""
img = self.render(
size=size,
module_width=module_width,
bar_height=bar_height,
quiet_zone=quiet_zone,
font_size=font_size,
draw_text=draw_text,
)
img.save(path, **save_kwargs)
return img
[docs]
def show(self) -> None:
"""Shows the barcode image"""
self.render().show()
[docs]
def to_text_bytes(self, encoding: str = "ascii") -> bytes:
"""Return the normalized barcode text as bytes."""
return self.code.encode(encoding)
[docs]
def to_text_bytesio(self, encoding: str = "ascii") -> BytesIO:
"""
Write the barcode to a BytesIO object
Returns
-------
Returns the BytesIO object created
"""
obj = BytesIO(self.to_text_bytes(encoding=encoding))
obj.seek(0)
return obj
[docs]
def to_bytesio(self, encoding: str = "ascii") -> BytesIO:
"""Return the normalized barcode text in a BytesIO object."""
return self.to_text_bytesio(encoding=encoding)
[docs]
def to_image_bytesio(
self,
format: str = "PNG",
size: Optional[RenderSize] = None,
module_width: Optional[int] = None,
bar_height: Optional[int] = None,
quiet_zone: Optional[int] = None,
font_size: Optional[int] = None,
draw_text: bool = True,
**save_kwargs: Any,
) -> BytesIO:
"""Return the rendered barcode image in a BytesIO object."""
obj = BytesIO()
self.render(
size=size,
module_width=module_width,
bar_height=bar_height,
quiet_zone=quiet_zone,
font_size=font_size,
draw_text=draw_text,
).save(obj, format=format, **save_kwargs)
obj.seek(0)
return obj
[docs]
def to_image_bytes(
self,
format: str = "PNG",
size: Optional[RenderSize] = None,
module_width: Optional[int] = None,
bar_height: Optional[int] = None,
quiet_zone: Optional[int] = None,
font_size: Optional[int] = None,
draw_text: bool = True,
**save_kwargs: Any,
) -> bytes:
"""Return the rendered barcode image as bytes."""
return self.to_image_bytesio(
format=format,
size=size,
module_width=module_width,
bar_height=bar_height,
quiet_zone=quiet_zone,
font_size=font_size,
draw_text=draw_text,
**save_kwargs,
).getvalue()
[docs]
def write(self, path: PathInput, encoding: str = "ascii") -> None:
"""
Tries to save the barcode to a text file
Parameters
----------
path: str
The path of the file
"""
with open(path, "w", encoding=encoding) as file:
file.write(self.code)
@staticmethod
def _positive_int(value: int, name: str) -> int:
value = int(value)
if value <= 0:
raise ValueError(f"{name} must be greater than 0.")
return value
def _get_render_options(
self,
module_width: Optional[int] = None,
bar_height: Optional[int] = None,
quiet_zone: Optional[int] = None,
font_size: Optional[int] = None,
draw_text: bool = True,
) -> RenderOptions:
padding = self.BARCODE_PADDING
selected_size, default_font_size = self.BARCODE_SIZE, self.BARCODE_FONT_SIZE
module_width = (
self._get_column_size()
if module_width is None
else self._positive_int(module_width, "module_width")
)
bar_height = (
selected_size[1]
if bar_height is None
else self._positive_int(bar_height, "bar_height")
)
quiet_zone = (
padding.width // 2
if quiet_zone is None
else self._positive_int(quiet_zone, "quiet_zone")
)
font_size = (
default_font_size
if font_size is None
else self._positive_int(font_size, "font_size")
)
text_padding = padding.height if draw_text else 0
return module_width, bar_height, quiet_zone, font_size, text_padding
def _get_default_font(self, font_size: int) -> ImageFont.ImageFont:
try:
return ImageFont.load_default(size=font_size)
except TypeError:
return ImageFont.load_default()
def _get_barcode_image(
self,
module_width: Optional[int] = None,
bar_height: Optional[int] = None,
quiet_zone: Optional[int] = None,
font_size: Optional[int] = None,
draw_text: bool = True,
) -> Image.Image:
"""Creates a PIL Image from the binary string of the barcode
Returns
-------
A PIL Image with the barcode is returned to the caller.
"""
module_width, bar_height, quiet_zone, font_size, text_padding = (
self._get_render_options(
module_width=module_width,
bar_height=bar_height,
quiet_zone=quiet_zone,
font_size=font_size,
draw_text=draw_text,
)
)
binary_string = self.get_binary_string
# Create the image for the barcode
img = Image.new(
"RGB",
(module_width * len(binary_string), bar_height),
(255, 255, 255),
)
index = 0
for digit in binary_string:
color = (0, 0, 0) if digit == "1" else (255, 255, 255)
column = Image.new("RGB", (module_width, img.height), color)
img.paste(column, (index, 0))
index += module_width
base = Image.new(
"RGB",
(img.width + quiet_zone * 2, bar_height + text_padding),
(255, 255, 255),
)
# Paste the barcode on the center of the padded base
Point = namedtuple("Point", "x y")
base_center = Point(base.width // 2, base.height // 2)
base.paste(img, (quiet_zone, text_padding // 2))
if not draw_text:
return base
draw = ImageDraw.Draw(base)
font = self._get_default_font(font_size)
text_width = draw.textlength(self.code, font)
x = base_center.x - text_width // 2
y = text_padding // 2 + img.height
draw.text((x, y), self.code, (0, 0, 0), font=font)
return base
def __eq__(self, other: object) -> bool:
if isinstance(other, self.__class__):
return self.code == other.code
elif isinstance(other, str):
return self.code == other
return False
def __str__(self) -> str:
return f"<{self.__class__.__name__}(code={self.code})>"
def __repr__(self) -> str:
return self.__str__()