Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ jobs:
cache: pip
cache-dependency-path: |
pyproject.toml
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libzbar0 libcairo2-dev
- name: Install system dependencies (macOS)
if: runner.os == 'macOS'
run: brew install zbar cairo
- name: Install system dependencies (Windows)
if: runner.os == 'Windows'
run: choco install zbar
- name: Install test dependency
run: pip install tox
- name: Run tests
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*.kpf
*.svg
*.db
.coverage
.idea/*
barcode/__pycache*
build/*
Expand Down
81 changes: 81 additions & 0 deletions barcode/addon_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Utility functions for building EAN-2 and EAN-5 addon barcodes.

This module provides shared functionality for addon barcode generation
used by EAN and UPC barcode classes.
"""

from __future__ import annotations

from barcode.charsets.addons import ADDON2_PARITY
from barcode.charsets.addons import ADDON5_PARITY
from barcode.charsets.addons import ADDON_CODES
from barcode.charsets.addons import ADDON_QUIET_ZONE
from barcode.charsets.addons import ADDON_SEPARATOR
from barcode.charsets.addons import ADDON_START


def build_addon(addon: str) -> str:
"""Build the complete addon barcode pattern (EAN-2 or EAN-5).

:param addon: The addon digits (2 or 5 digits)
:returns: The addon pattern as string (including quiet zone separator)
"""
if not addon:
return ""

# Add quiet zone (9 modules) before addon per GS1 specification
code = ADDON_QUIET_ZONE

if len(addon) == 2:
code += build_addon2(addon)
else:
code += build_addon5(addon)

return code


def build_addon2(addon: str) -> str:
"""Build EAN-2 addon pattern.

Parity is determined by the 2-digit value mod 4.

:param addon: The 2-digit addon string
:returns: The EAN-2 addon pattern (using 'A' for addon bars)
"""
value = int(addon)
parity = ADDON2_PARITY[value % 4]

code = ADDON_START
for i, digit in enumerate(addon):
if i > 0:
code += ADDON_SEPARATOR
code += ADDON_CODES[parity[i]][int(digit)]

# Replace '1' with 'A' to mark addon bars for special rendering
return code.replace("1", "A")


def build_addon5(addon: str) -> str:
"""Build EAN-5 addon pattern.

Parity is determined by a checksum calculation.

:param addon: The 5-digit addon string
:returns: The EAN-5 addon pattern (using 'A' for addon bars)
"""
# Calculate checksum for parity pattern
checksum = 0
for i, digit in enumerate(addon):
weight = 3 if i % 2 == 0 else 9
checksum += int(digit) * weight
checksum %= 10
parity = ADDON5_PARITY[checksum]

code = ADDON_START
for i, digit in enumerate(addon):
if i > 0:
code += ADDON_SEPARATOR
code += ADDON_CODES[parity[i]][int(digit)]

# Replace '1' with 'A' to mark addon bars for special rendering
return code.replace("1", "A")
63 changes: 63 additions & 0 deletions barcode/charsets/addons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Common addon patterns for EAN-2 and EAN-5 supplemental barcodes.

These patterns are shared by EAN-13, EAN-8, UPC-A, and related barcode types.
Based on GS1/ISO standard.
"""

from __future__ import annotations

# Addon guard patterns
# 9-module separator between main code and addon (GS1 spec)
ADDON_QUIET_ZONE = "000000000"
ADDON_START = "1011" # Start guard for addon
ADDON_SEPARATOR = "01" # Separator between addon digits

# Addon digit encoding (uses A and B parity patterns)
ADDON_CODES = {
"A": (
"0001101",
"0011001",
"0010011",
"0111101",
"0100011",
"0110001",
"0101111",
"0111011",
"0110111",
"0001011",
),
"B": (
"0100111",
"0110011",
"0011011",
"0100001",
"0011101",
"0111001",
"0000101",
"0010001",
"0001001",
"0010111",
),
}

# EAN-2 parity patterns: determined by value mod 4
ADDON2_PARITY = (
"AA", # 0
"AB", # 1
"BA", # 2
"BB", # 3
)

# EAN-5 parity patterns: determined by checksum
ADDON5_PARITY = (
"BBAAA", # 0
"BABAA", # 1
"BAABA", # 2
"BAAAB", # 3
"ABBAA", # 4
"AABBA", # 5
"AAABB", # 6
"ABABA", # 7
"ABAAB", # 8
"AABAB", # 9
)
21 changes: 21 additions & 0 deletions barcode/charsets/ean.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
from __future__ import annotations

from barcode.charsets.addons import ADDON2_PARITY
from barcode.charsets.addons import ADDON5_PARITY
from barcode.charsets.addons import ADDON_QUIET_ZONE
from barcode.charsets.addons import ADDON_SEPARATOR
from barcode.charsets.addons import ADDON_START

# Note: Addon codes are defined in barcode.charsets.addons, but they use the
# same A/B digit encodings as CODES["A"] and CODES["B"] defined below.
EDGE = "101"
MIDDLE = "01010"
CODES = {
Expand Down Expand Up @@ -52,3 +60,16 @@
"ABABBA",
"ABBABA",
)

# Re-export addon constants for backwards compatibility
__all__ = [
"ADDON2_PARITY",
"ADDON5_PARITY",
"ADDON_QUIET_ZONE",
"ADDON_SEPARATOR",
"ADDON_START",
"CODES",
"EDGE",
"LEFT_PATTERN",
"MIDDLE",
]
20 changes: 20 additions & 0 deletions barcode/charsets/upc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from __future__ import annotations

from barcode.charsets.addons import ADDON2_PARITY
from barcode.charsets.addons import ADDON5_PARITY
from barcode.charsets.addons import ADDON_CODES
from barcode.charsets.addons import ADDON_QUIET_ZONE
from barcode.charsets.addons import ADDON_SEPARATOR
from barcode.charsets.addons import ADDON_START

EDGE = "101"
MIDDLE = "01010"
CODES = {
Expand Down Expand Up @@ -28,3 +35,16 @@
"1110100",
),
}

# Re-export addon constants for backwards compatibility
__all__ = [
"ADDON2_PARITY",
"ADDON5_PARITY",
"ADDON_CODES",
"ADDON_QUIET_ZONE",
"ADDON_SEPARATOR",
"ADDON_START",
"CODES",
"EDGE",
"MIDDLE",
]
58 changes: 51 additions & 7 deletions barcode/ean.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
__docformat__ = "restructuredtext en"


from barcode import addon_utils
from barcode.base import Barcode
from barcode.charsets import ean as _ean
from barcode.errors import IllegalCharacterError
Expand Down Expand Up @@ -35,6 +36,8 @@ class EuropeanArticleNumber13(Barcode):
:param ean: The ean number as string. If the value is too long, it is trimmed.
:param writer: The writer to render the barcode (default: SVGWriter).
:param no_checksum: Don't calculate the checksum. Use the provided input instead.
:param guardbar: If True, use guard bar markers in the output.
:param addon: Optional 2 or 5 digit addon (EAN-2 or EAN-5).
"""

name = "EAN-13"
Expand All @@ -47,6 +50,7 @@ def __init__(
writer=None,
no_checksum: bool = False,
guardbar: bool = False,
addon: str | None = None,
) -> None:
if not ean[: self.digits].isdigit():
raise IllegalCharacterError(f"EAN code can only contain numbers {ean}.")
Expand All @@ -68,6 +72,21 @@ def __init__(

self.ean = f"{base}{last}"

# Validate and store addon
self.addon = None
if addon is not None:
addon = addon.strip()
if addon:
if not addon.isdigit():
raise IllegalCharacterError(
f"Addon can only contain numbers, got {addon}."
)
if len(addon) not in (2, 5):
raise NumberOfDigitsError(
f"Addon must be 2 or 5 digits, received {len(addon)}."
)
self.addon = addon

self.guardbar = guardbar
if guardbar:
self.EDGE = _ean.EDGE.replace("1", "G")
Expand All @@ -78,12 +97,15 @@ def __init__(
self.writer = writer or self.default_writer()

def __str__(self) -> str:
if self.addon:
return f"{self.ean} {self.addon}"
return self.ean

def get_fullcode(self) -> str:
addon = "" if not self.addon else f" {self.addon}"
if self.guardbar:
return self.ean[0] + " " + self.ean[1:7] + " " + self.ean[7:] + " >"
return self.ean
return self.ean[0] + " " + self.ean[1:7] + " " + self.ean[7:] + addon + " >"
return f"{self.ean}{addon}"

def calculate_checksum(self, value: str | None = None) -> int:
"""Calculates and returns the checksum for EAN13-Code.
Expand Down Expand Up @@ -112,8 +134,20 @@ def build(self) -> list[str]:
for number in self.ean[7:]:
code += _ean.CODES["C"][int(number)]
code += self.EDGE

# Add addon if present
if self.addon:
code += self._build_addon()

return [code]

def _build_addon(self) -> str:
"""Builds the addon barcode pattern (EAN-2 or EAN-5).

:returns: The addon pattern as string (including quiet zone separator)
"""
return addon_utils.build_addon(self.addon or "")

def to_ascii(self) -> str:
"""Returns an ascii representation of the barcode.

Expand All @@ -136,8 +170,10 @@ class EuropeanArticleNumber13WithGuard(EuropeanArticleNumber13):

name = "EAN-13 with guards"

def __init__(self, ean, writer=None, no_checksum=False, guardbar=True) -> None:
super().__init__(ean, writer, no_checksum, guardbar)
def __init__(
self, ean, writer=None, no_checksum=False, guardbar=True, addon=None
) -> None:
super().__init__(ean, writer, no_checksum, guardbar, addon)


class JapanArticleNumber(EuropeanArticleNumber13):
Expand Down Expand Up @@ -167,6 +203,7 @@ class EuropeanArticleNumber8(EuropeanArticleNumber13):

:param ean: The ean number as string.
:param writer: The writer to render the barcode (default: SVGWriter).
:param addon: Optional 2 or 5 digit addon (EAN-2 or EAN-5).
"""

name = "EAN-8"
Expand All @@ -185,12 +222,18 @@ def build(self) -> list[str]:
for number in self.ean[4:]:
code += _ean.CODES["C"][int(number)]
code += self.EDGE

# Add addon if present
if self.addon:
code += self._build_addon()

return [code]

def get_fullcode(self):
addon = "" if not self.addon else f" {self.addon}"
if self.guardbar:
return "< " + self.ean[:4] + " " + self.ean[4:] + " >"
return self.ean
return "< " + self.ean[:4] + " " + self.ean[4:] + addon + " >"
return f"{self.ean}{addon}"


class EuropeanArticleNumber8WithGuard(EuropeanArticleNumber8):
Expand All @@ -204,8 +247,9 @@ def __init__(
writer=None,
no_checksum: bool = False,
guardbar: bool = True,
addon: str | None = None,
) -> None:
super().__init__(ean, writer, no_checksum, guardbar)
super().__init__(ean, writer, no_checksum, guardbar, addon)


class EuropeanArticleNumber14(EuropeanArticleNumber13):
Expand Down
Loading