diff --git a/barcode/isxn.py b/barcode/isxn.py index e74ea58..c7fe31a 100755 --- a/barcode/isxn.py +++ b/barcode/isxn.py @@ -66,17 +66,15 @@ class InternationalStandardBookNumber10(InternationalStandardBookNumber13): name = "ISBN-10" - digits = 9 + isbn_digits = 9 def __init__(self, isbn, writer=None) -> None: - isbn = isbn.replace("-", "") - isbn = isbn[: self.digits] + isbn = isbn.replace("-", "")[:self.isbn_digits] + self.isbn10 = f"{isbn}{self._calculate_checksum(isbn)}" super().__init__("978" + isbn, writer) - self.isbn10 = isbn - self.isbn10 = f"{isbn}{self._calculate_checksum()}" - def _calculate_checksum(self): - tmp = sum(x * int(y) for x, y in enumerate(self.isbn10[:9], start=1)) % 11 + def _calculate_checksum(self, isbn): + tmp = sum(x * int(y) for x, y in enumerate(isbn[:self.isbn_digits], start=1)) % 11 if tmp == 10: return "X" @@ -99,19 +97,18 @@ class InternationalStandardSerialNumber(EuropeanArticleNumber13): name = "ISSN" - digits = 7 + issn_digits = 7 def __init__(self, issn, writer=None) -> None: - issn = issn.replace("-", "") - issn = issn[: self.digits] - self.issn = issn - self.issn = f"{issn}{self._calculate_checksum()}" - super().__init__(self.make_ean(), writer) + issn = issn.replace("-", "")[: self.issn_digits] + self.issn = f"{issn}{self._calculate_checksum(issn)}" + super().__init__(f"977{issn}00", writer) #checksum is overwritten by on .build + - def _calculate_checksum(self): + def _calculate_checksum(self, issn): tmp = ( 11 - - sum(x * int(y) for x, y in enumerate(reversed(self.issn[:7]), start=2)) + - sum(x * int(y) for x, y in enumerate(reversed(issn[:self.issn_digits]), start=2)) % 11 ) if tmp == 10: @@ -119,9 +116,6 @@ def _calculate_checksum(self): return tmp - def make_ean(self): - return f"977{self.issn[:7]}00{self._calculate_checksum()}" - def __str__(self) -> str: return self.issn diff --git a/barcode/itf.py b/barcode/itf.py index f40d9cb..4632160 100644 --- a/barcode/itf.py +++ b/barcode/itf.py @@ -70,7 +70,7 @@ def build(self) -> list[str]: raw += "0" * self.narrow return [raw] - def render(self, writer_options, text=None): + def render(self, writer_options: dict | None = None, text: str | None = None): options = { "module_width": MIN_SIZE / self.narrow, "quiet_zone": MIN_QUIET_ZONE, diff --git a/pyproject.toml b/pyproject.toml index 891d6c3..9594c37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dynamic = ["version"] [project.optional-dependencies] images = ["pillow"] +test_with_pyzbar = ["pillow", "cairosvg", "pyzbar"] [project.scripts] python-barcode = "barcode.pybarcode:main" diff --git a/tests/test_with_pyzbar.py b/tests/test_with_pyzbar.py new file mode 100644 index 0000000..d7931d9 --- /dev/null +++ b/tests/test_with_pyzbar.py @@ -0,0 +1,111 @@ +import pytest + +pytest.importorskip("pyzbar") +pytest.importorskip("PIL") + +import os +import barcode +from barcode.base import Barcode +from barcode.writer import ImageWriter, SVGWriter +from pyzbar.pyzbar import decode +from PIL import Image +from io import BytesIO + + +try: + import cairosvg + import cairocffi + cairocffi.Context(cairocffi.ImageSurface(cairocffi.FORMAT_ARGB32, 1, 1)) + HAS_CAIROSVG = True +except (ImportError, OSError): + HAS_CAIROSVG = False + + +def get_normalized_code(barcode_instance: Barcode, code: str) -> str: + if isinstance(barcode_instance, barcode.UPCA) and len(code) > 12: + return code[-12:] ## return last 12, because may be leftpadded with zero from pyzbar. + return code + + +def perform_pyzbar_validation(barcode_instance: Barcode, img: Image, from_svg: bool = False) -> None: + try: + classname = type(barcode_instance).name + decoded = decode(img) + assert decoded, f"{classname} failed to decode" + except AssertionError as e: + filename = f"pyzbar_decode_fail_{classname}{"_from_svg" if from_svg else ""}.png" + directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_outputs") + os.makedirs(directory, exist_ok=True) + img.save(os.path.join(directory, filename)) + raise + + normalized_code = get_normalized_code(barcode_instance, decoded[0].data.decode("ascii")) + fullcode_classes = ( + barcode.Gs1_128, + barcode.ISBN10, + barcode.ISSN, + ) + expected_code = str(barcode_instance if not isinstance(barcode_instance, fullcode_classes) else barcode_instance.get_fullcode()) + assert normalized_code == expected_code, f"{classname}: invalid" + return True + + +def get_valid_barcode_tuples() -> tuple[tuple[Barcode, str]]: + VALID_EAN8_CODE = "73513544" + VALID_EAN13CODE = "1000009029223" + VALID_BARCODES = ( + (barcode.EAN8, VALID_EAN8_CODE), + (barcode.EAN8_GUARD, VALID_EAN8_CODE), + (barcode.EAN13, VALID_EAN13CODE), + (barcode.EAN13_GUARD, VALID_EAN13CODE), + (barcode.UPCA, "036000291452"), + (barcode.Code128, "A99BCDEF1234678"), + (barcode.Code39, "QWERTY"), + (barcode.JAN, "4901234567894"), + (barcode.ISSN, "1234567"), ## uses get_fullcode to validate, since __str__ returns issn value + (barcode.ISBN10, "306406152"), ## uses get_fullcode to validate, since __str__ returns isbn10 value + (barcode.ISBN13, "9783064061521"), + (barcode.Gs1_128, "YYYyyyy"), ## use get_fullcode to validate, since code prefixes with "\xf1" character on init + (barcode.ITF, "10000090292221"), + (barcode.PZN, "1234567"), + #(barcode.CODABAR, ""), ## pyzbar does not support decoding this + #(barcode.EAN14, "10000090292221"), ## pyzbar does not support decoding this, but I wonder if ITF is not essentially this, can't scan image with phone either. is useful for testing + ) + return VALID_BARCODES + + +def test_imagewriter() -> None: + for barcode_class, valid_code in get_valid_barcode_tuples(): + ## maybe consider using barcode.get_barcode() and using strings instead of classes. + barcode_instance = barcode_class(valid_code, writer=ImageWriter()) + img = barcode_instance.render() + + assert img, f"{type(barcode_instance).name} Failed to render" + perform_pyzbar_validation(barcode_instance, img) + + +def test_ean14_png_decode_failure() -> None: + '''We expect this to fail for now, but if that stops this test can probably be removed. and added to get_valid_barcode_tuples''' + barcode_instance = barcode.get_barcode("EAN14", "10000090292221", writer=ImageWriter()) + img = barcode_instance.render() + assert img, f"{type(barcode_instance).name} Failed to render" + try: + validation_success = perform_pyzbar_validation(barcode_instance, img) + except AssertionError: + validation_success = False + assert validation_success == False, "We expected failure, but this succeeded." + + +@pytest.mark.skipif(not HAS_CAIROSVG, reason="cairosvg is not installed or can't load library") +def test_svgwriter() -> None: + for barcode_class, valid_code in get_valid_barcode_tuples(): + barcode_instance = barcode_class(valid_code, writer=SVGWriter()) + svg_data = barcode_instance.render() + buf = BytesIO() + cairosvg.svg2png(bytestring=svg_data, write_to=buf, scale=2) ## scale it so antialiasing does not happen + buf.seek(0) + img = Image.open(buf) + + assert img, f"{barcode_class} Failed to render" + perform_pyzbar_validation(barcode_instance, img, from_svg=True) +