diff --git a/barcode/__init__.py b/barcode/__init__.py index 8c54f8c..1a16b4e 100755 --- a/barcode/__init__.py +++ b/barcode/__init__.py @@ -5,8 +5,10 @@ """ from __future__ import annotations +import os from typing import TYPE_CHECKING from typing import BinaryIO +from typing import overload from barcode.codabar import CODABAR from barcode.codex import PZN @@ -28,11 +30,10 @@ from barcode.version import version # noqa: F401 if TYPE_CHECKING: - import os - + from barcode.base import Barcode from barcode.writer import BaseWriter -__BARCODE_MAP = { +__BARCODE_MAP: dict[str, type[Barcode]] = { "codabar": CODABAR, "code128": Code128, "code39": Code39, @@ -61,12 +62,29 @@ PROVIDED_BARCODES.sort() +@overload +def get( + name: str, code: str, writer: BaseWriter | None = None, options: dict | None = None +) -> Barcode: + ... + + +@overload +def get( + name: str, + code: None = None, + writer: BaseWriter | None = None, + options: dict | None = None, +) -> type[Barcode]: + ... + + def get( name: str, code: str | None = None, writer: BaseWriter | None = None, options: dict | None = None, -): +) -> Barcode | type[Barcode]: """Helper method for getting a generator or even a generated code. :param name: The name of the type of barcode desired. @@ -79,6 +97,7 @@ def get( generating. """ options = options or {} + barcode: type[Barcode] try: barcode = __BARCODE_MAP[name.lower()] except KeyError as e: @@ -89,7 +108,7 @@ def get( return barcode -def get_class(name: str): +def get_class(name: str) -> type[Barcode]: return get_barcode(name) @@ -97,7 +116,7 @@ def generate( name: str, code: str, writer: BaseWriter | None = None, - output: str | (os.PathLike | (BinaryIO | None)) = None, + output: str | os.PathLike | BinaryIO | None = None, writer_options: dict | None = None, text: str | None = None, ) -> str | None: @@ -113,6 +132,9 @@ def generate( """ from barcode.base import Barcode + if output is None: + raise TypeError("'output' cannot be None") + writer = writer or Barcode.default_writer() writer.set_options(writer_options or {}) @@ -120,11 +142,12 @@ def generate( if isinstance(output, str): return barcode.save(output, writer_options, text) - if output: - barcode.write(output, writer_options, text) + if isinstance(output, os.PathLike): + with open(output, "wb") as fp: + barcode.write(fp, writer_options, text) return None - - raise TypeError("'output' cannot be None") + barcode.write(output, writer_options, text) + return None get_barcode = get diff --git a/barcode/base.py b/barcode/base.py index d5296ae..95223fb 100755 --- a/barcode/base.py +++ b/barcode/base.py @@ -34,16 +34,24 @@ class Barcode: writer: BaseWriter + def __init__(self, code: str, writer: BaseWriter | None = None, **options) -> None: + raise NotImplementedError + def to_ascii(self) -> str: - code = self.build() - for i, line in enumerate(code): - code[i] = line.replace("1", "X").replace("0", " ") - return "\n".join(code) + code_list = self.build() + if not len(code_list) == 1: + raise RuntimeError("Code list must contain a single element.") + code = code_list[0] + return code.replace("1", "X").replace("0", " ") def __repr__(self) -> str: return f"<{self.__class__.__name__}({self.get_fullcode()!r})>" def build(self) -> list[str]: + """Return a single-element list with a string encoding the barcode. + + Typically the string consists of 1s and 0s, although it can contain + other characters such as G for guard lines (e.g. in EAN13).""" raise NotImplementedError def get_fullcode(self): @@ -101,5 +109,8 @@ def render(self, writer_options: dict | None = None, text: str | None = None): else: options["text"] = self.get_fullcode() self.writer.set_options(options) - code = self.build() - return self.writer.render(code) + code_list = self.build() + if not len(code_list) == 1: + raise RuntimeError("Code list must contain a single element.") + code = code_list[0] + return self.writer.render([code]) diff --git a/barcode/codabar.py b/barcode/codabar.py index 6395459..aecd8ab 100644 --- a/barcode/codabar.py +++ b/barcode/codabar.py @@ -41,7 +41,7 @@ def __str__(self) -> str: def get_fullcode(self): return self.code - def build(self): + def build(self) -> list[str]: try: data = ( codabar.STARTSTOP[self.code[0]] + "n" diff --git a/barcode/codex.py b/barcode/codex.py index ef72187..b82c020 100755 --- a/barcode/codex.py +++ b/barcode/codex.py @@ -4,7 +4,9 @@ """ from __future__ import annotations +from typing import TYPE_CHECKING from typing import Collection +from typing import Literal from barcode.base import Barcode from barcode.charsets import code39 @@ -13,6 +15,9 @@ from barcode.errors import IllegalCharacterError from barcode.errors import NumberOfDigitsError +if TYPE_CHECKING: + from barcode.writer import BaseWriter + __docformat__ = "restructuredtext en" # Sizes @@ -66,12 +71,13 @@ def calculate_checksum(self): return k return None - def build(self): + def build(self) -> list[str]: chars = [code39.EDGE] for char in self.code: chars.append(code39.MAP[char][1]) chars.append(code39.EDGE) - return [code39.MIDDLE.join(chars)] + result = code39.MIDDLE.join(chars) + return [result] def render(self, writer_options=None, text=None): options = {"module_width": MIN_SIZE, "quiet_zone": MIN_QUIET_ZONE} @@ -135,8 +141,12 @@ class Code128(Barcode): """ name = "Code 128" + _charset: Literal["A", "B", "C"] + code: str + writer: BaseWriter + buffer: str - def __init__(self, code, writer=None) -> None: + def __init__(self, code: str, writer=None) -> None: self.code = code self.writer = writer or self.default_writer() self._charset = "B" @@ -147,13 +157,15 @@ def __str__(self) -> str: return self.code @property - def encoded(self): + def encoded(self) -> list[int]: return self._build() - def get_fullcode(self): + def get_fullcode(self) -> str: return self.code - def _new_charset(self, which): + def _new_charset(self, which: Literal["A", "B", "C"]) -> list[int]: + if which == self._charset: + raise ValueError(f"Already in charset {which}") if which == "A": code = self._convert("TO_A") elif which == "B": @@ -163,11 +175,11 @@ def _new_charset(self, which): self._charset = which return [code] - def _maybe_switch_charset(self, pos): + def _maybe_switch_charset(self, pos: int) -> list[int]: char = self.code[pos] next_ = self.code[pos : pos + 10] - def look_next(): + def look_next() -> bool: digits = 0 for c in next_: if c.isdigit(): @@ -176,7 +188,7 @@ def look_next(): break return digits > 3 - codes = [] + codes: list[int] = [] if self._charset == "C" and not char.isdigit(): if char in code128.B: codes = self._new_charset("B") @@ -197,7 +209,7 @@ def look_next(): codes = self._new_charset("B") return codes - def _convert(self, char): + def _convert(self, char: str): if self._charset == "A": return code128.A[char] if self._charset == "B": @@ -212,22 +224,23 @@ def _convert(self, char): self._buffer = "" return value return None - return None - return None + raise RuntimeError( + f"Character {char} could not be converted in charset {self._charset}." + ) - def _try_to_optimize(self, encoded): + def _try_to_optimize(self, encoded: list[int]) -> list[int]: if encoded[1] in code128.TO: encoded[:2] = [code128.TO[encoded[1]]] return encoded - def _calculate_checksum(self, encoded): + def _calculate_checksum(self, encoded: list[int]) -> int: cs = [encoded[0]] for i, code_num in enumerate(encoded[1:], start=1): cs.append(i * code_num) return sum(cs) % 103 - def _build(self): - encoded = [code128.START_CODES[self._charset]] + def _build(self) -> list[int]: + encoded: list[int] = [code128.START_CODES[self._charset]] for i, char in enumerate(self.code): encoded.extend(self._maybe_switch_charset(i)) code_num = self._convert(char) @@ -240,7 +253,7 @@ def _build(self): self._buffer = "" return self._try_to_optimize(encoded) - def build(self): + def build(self) -> list[str]: encoded = self._build() encoded.append(self._calculate_checksum(encoded)) code = "" diff --git a/barcode/ean.py b/barcode/ean.py index 028d20a..c65d17c 100755 --- a/barcode/ean.py +++ b/barcode/ean.py @@ -94,11 +94,11 @@ def calculate_checksum(self, value: str | None = None) -> int: oddsum = sum(int(x) for x in ean_without_checksum[-1::-2]) return (10 - ((evensum + oddsum * 3) % 10)) % 10 - def build(self): + def build(self) -> list[str]: """Builds the barcode pattern from `self.ean`. :returns: The pattern as string - :rtype: String + :rtype: List containing the string as a single element """ code = self.EDGE[:] pattern = _ean.LEFT_PATTERN[int(self.ean[0])] @@ -110,15 +110,16 @@ def build(self): code += self.EDGE return [code] - def to_ascii(self): + def to_ascii(self) -> str: """Returns an ascii representation of the barcode. :rtype: String """ - code = self.build() - for i, line in enumerate(code): - code[i] = line.replace("G", "|").replace("1", "|").replace("0", " ") - return "\n".join(code) + code_list = self.build() + if not len(code_list) == 1: + raise RuntimeError("Code list must contain a single element.") + code = code_list[0] + return code.replace("G", "|").replace("1", "|").replace("0", " ") def render(self, writer_options=None, text=None): options = {"module_width": SIZES["SC2"]} @@ -171,11 +172,10 @@ class EuropeanArticleNumber8(EuropeanArticleNumber13): digits = 7 - def build(self): + def build(self) -> list[str]: """Builds the barcode pattern from `self.ean`. - :returns: The pattern as string - :rtype: String + :returns: A list containing the string as a single element """ code = self.EDGE[:] for number in self.ean[:4]: diff --git a/barcode/itf.py b/barcode/itf.py index 428dd4d..7d9c2fe 100644 --- a/barcode/itf.py +++ b/barcode/itf.py @@ -48,7 +48,7 @@ def __str__(self) -> str: def get_fullcode(self): return self.code - def build(self): + def build(self) -> list[str]: data = itf.START for i in range(0, len(self.code), 2): bars_digit = int(self.code[i]) diff --git a/barcode/upc.py b/barcode/upc.py index 37b9f76..9093b8f 100755 --- a/barcode/upc.py +++ b/barcode/upc.py @@ -77,11 +77,11 @@ def sum_(x, y): return 10 - check - def build(self): + def build(self) -> list[str]: """Builds the barcode pattern from 'self.upc' :return: The pattern as string - :rtype: str + :rtype: List containing the string as a single element """ code = _upc.EDGE[:] @@ -97,16 +97,17 @@ def build(self): return [code] - def to_ascii(self): + def to_ascii(self) -> str: """Returns an ascii representation of the barcode. :rtype: str """ - code = self.build() - for i, line in enumerate(code): - code[i] = line.replace("1", "|").replace("0", "_") - return "\n".join(code) + code_list = self.build() + if len(code_list) != 1: + raise RuntimeError("Code list must contain a single element.") + code = code_list[0] + return code.replace("1", "|").replace("0", "_") def render(self, writer_options=None, text=None): options = {"module_width": 0.33} diff --git a/barcode/writer.py b/barcode/writer.py index 27ca933..b3d32fd 100755 --- a/barcode/writer.py +++ b/barcode/writer.py @@ -2,7 +2,7 @@ import gzip import os -import xml.dom +import xml.dom.minidom from typing import TYPE_CHECKING from typing import BinaryIO from typing import Callable @@ -14,6 +14,9 @@ from typing import Generator from typing import Literal + from PIL.Image import Image as T_Image + from PIL.ImageDraw import ImageDraw as T_ImageDraw + class InternalText(TypedDict): start: list end: list @@ -28,27 +31,22 @@ class Callbacks(TypedDict): try: - import Image - import ImageDraw - import ImageFont + from PIL import Image + from PIL import ImageDraw + from PIL import ImageFont except ImportError: - try: - from PIL import Image - from PIL import ImageDraw - from PIL import ImageFont - except ImportError: - import logging + import logging - log = logging.getLogger("pyBarcode") - log.info("Pillow not found. Image output disabled") - Image = ImageDraw = ImageFont = None + log = logging.getLogger("pyBarcode") + log.info("Pillow not found. Image output disabled") + Image = ImageDraw = ImageFont = None -def mm2px(mm, dpi: int): +def mm2px(mm: float, dpi: int) -> float: return (mm * dpi) / 25.4 -def pt2mm(pt): +def pt2mm(pt: float) -> float: return pt * 0.352777778 @@ -57,8 +55,9 @@ def _set_attributes(element, **attributes): element.setAttribute(key, value) -def create_svg_object(with_doctype=False): - imp = xml.dom.getDOMImplementation() +def create_svg_object(with_doctype=False) -> xml.dom.minidom.Document: + imp = xml.dom.minidom.getDOMImplementation() + assert imp is not None doctype = imp.createDocumentType( "svg", "-//W3C//DTD SVG 1.1//EN", @@ -96,6 +95,21 @@ class BaseWriter: """ _callbacks: Callbacks + module_width: float + module_height: float + font_path: str + font_size: float + quiet_zone: float + background: str | int + foreground: str | int + text: str + human: str + text_distance: float + text_line_distance: float + center_text: bool + guard_height_factor: float + margin_top: float + margin_bottom: float def __init__( self, @@ -204,65 +218,60 @@ def packed(self, line: str) -> Generator[tuple[int, float], str, None]: yield (-c, self.guard_height_factor) c = 1 - def render(self, code): + def render(self, code: list[str]): """Renders the barcode to whatever the inheriting writer provides, using the registered callbacks. :parameters: code : List - List of strings matching the writer spec + List consisting of a single string matching the writer spec (only contain 0 or 1 or G). """ if self._callbacks["initialize"] is not None: self._callbacks["initialize"](code) ypos = self.margin_top base_height = self.module_height - for cc, line in enumerate(code): - # Left quiet zone is x startposition - xpos = self.quiet_zone - bxs = xpos # x start of barcode - text: InternalText = { - "start": [], # The x start of a guard - "end": [], # The x end of a guard - "xpos": [], # The x position where to write a text block - # Flag that indicates if the previous mod was part of an guard block: - "was_guard": False, - } - for mod, height_factor in self.packed(line): - if mod < 1: - color = self.background - else: - color = self.foreground - - if text["was_guard"] and height_factor == 1: - # The current guard ended, store its x position - text["end"].append(xpos) - text["was_guard"] = False - elif not text["was_guard"] and height_factor != 1: - # A guard started, store its x position - text["start"].append(xpos) - text["was_guard"] = True - - self.module_height = base_height * height_factor - # remove painting for background colored tiles? - self._callbacks["paint_module"]( - xpos, ypos, self.module_width * abs(mod), color - ) - xpos += self.module_width * abs(mod) + if len(code) != 1: + raise NotImplementedError("Only one line of code is supported") + line = code[0] + # Left quiet zone is x startposition + xpos = self.quiet_zone + bxs = xpos # x start of barcode + text: InternalText = { + "start": [], # The x start of a guard + "end": [], # The x end of a guard + "xpos": [], # The x position where to write a text block + # Flag that indicates if the previous mod was part of an guard block: + "was_guard": False, + } + for mod, height_factor in self.packed(line): + if mod < 1: + color = self.background else: - if height_factor != 1: + color = self.foreground + + if text["was_guard"] and height_factor == 1: + # The current guard ended, store its x position text["end"].append(xpos) - self.module_height = base_height - - bxe = xpos - # Add right quiet zone to every line, except last line, - # quiet zone already provided with background, - # should it be removed completely? - if (cc + 1) != len(code): - self._callbacks["paint_module"]( - xpos, ypos, self.quiet_zone, self.background - ) - ypos += self.module_height + text["was_guard"] = False + elif not text["was_guard"] and height_factor != 1: + # A guard started, store its x position + text["start"].append(xpos) + text["was_guard"] = True + + self.module_height = base_height * height_factor + # remove painting for background colored tiles? + self._callbacks["paint_module"]( + xpos, ypos, self.module_width * abs(mod), color + ) + xpos += self.module_width * abs(mod) + else: + if height_factor != 1: + text["end"].append(xpos) + self.module_height = base_height + + bxe = xpos + ypos += self.module_height if self.text and self._callbacks["paint_text"] is not None: if not text["start"]: @@ -306,14 +315,17 @@ def __init__(self) -> None: self._create_text, self._finish, ) - self.compress = False - self.with_doctype = True - self._document = None - self._root = None - self._group = None - - def _init(self, code): - width, height = self.calculate_size(len(code[0]), len(code)) + self.compress: bool = False + self.with_doctype: bool = True + self._document: xml.dom.minidom.Document + self._root: xml.dom.minidom.Element + self._group: xml.dom.minidom.Element + + def _init(self, code: list[str]): + if len(code) != 1: + raise NotImplementedError("Only one line of code is supported") + line = code[0] + width, height = self.calculate_size(len(line), 1) self._document = create_svg_object(self.with_doctype) self._root = self._document.documentElement attributes = { @@ -362,9 +374,9 @@ def _create_text(self, xpos, ypos): attributes = { "x": SIZE.format(xpos), "y": SIZE.format(ypos), - "style": "fill:{};font-size:{}pt;text-anchor:middle;".format( - self.foreground, - self.font_size, + "style": ( + f"fill:{self.foreground};" + f"font-size:{self.font_size}pt;text-anchor:middle;" ), } _set_attributes(element, **attributes) @@ -427,16 +439,21 @@ def __init__(self, format="PNG", mode="RGB", dpi=300) -> None: self.format = format self.mode = mode self.dpi = dpi - self._image = None - self._draw = None - - def _init(self, code): - width, height = self.calculate_size(len(code[0]), len(code)) + self._image: T_Image + self._draw: T_ImageDraw + + def _init(self, code: list[str]) -> None: + if ImageDraw is None: + raise RuntimeError("Pillow not found. Cannot create image.") + if len(code) != 1: + raise NotImplementedError("Only one line of code is supported") + line = code[0] + width, height = self.calculate_size(len(line), 1) size = (int(mm2px(width, self.dpi)), int(mm2px(height, self.dpi))) self._image = Image.new(self.mode, size, self.background) self._draw = ImageDraw.Draw(self._image) - def _paint_module(self, xpos, ypos, width, color): + def _paint_module(self, xpos: float, ypos: float, width: float, color): size = [ (mm2px(xpos, self.dpi), mm2px(ypos, self.dpi)), ( @@ -447,6 +464,7 @@ def _paint_module(self, xpos, ypos, width, color): self._draw.rectangle(size, outline=color, fill=color) def _paint_text(self, xpos, ypos): + assert ImageFont is not None font_size = int(mm2px(pt2mm(self.font_size), self.dpi)) if font_size <= 0: return @@ -461,7 +479,7 @@ def _paint_text(self, xpos, ypos): ) ypos += pt2mm(self.font_size) / 2 + self.text_line_distance - def _finish(self) -> Image: + def _finish(self) -> T_Image: return self._image def save(self, filename: str, output) -> str: