Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
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
ADDON_QUIET_ZONE = "000000000" # 9-module separator between main code and addon (GS1 spec)
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
)

22 changes: 22 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 use CODES["A"] and CODES["B"] defined below

EDGE = "101"
MIDDLE = "01010"
CODES = {
Expand Down Expand Up @@ -52,3 +60,17 @@
"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",
]

21 changes: 21 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,17 @@
"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",
]

109 changes: 102 additions & 7 deletions barcode/ean.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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 +49,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 +71,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 +96,18 @@ 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:
if self.guardbar:
return self.ean[0] + " " + self.ean[1:7] + " " + self.ean[7:] + " >"
return self.ean
base = self.ean[0] + " " + self.ean[1:7] + " " + self.ean[7:] + " >"
else:
base = self.ean
if self.addon:
return f"{base} {self.addon}"
return base

def calculate_checksum(self, value: str | None = None) -> int:
"""Calculates and returns the checksum for EAN13-Code.
Expand Down Expand Up @@ -112,8 +136,66 @@ 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)
"""
if not self.addon:
return ""

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

if len(self.addon) == 2:
code += self._build_addon2()
else:
code += self._build_addon5()

return code

def _build_addon2(self) -> str:
"""Builds EAN-2 addon pattern.

Parity is determined by the 2-digit value mod 4.
"""
value = int(self.addon)
parity = _ean.ADDON2_PARITY[value % 4]

code = _ean.ADDON_START
for i, digit in enumerate(self.addon):
if i > 0:
code += _ean.ADDON_SEPARATOR
code += _ean.CODES[parity[i]][int(digit)]
return code

def _build_addon5(self) -> str:
"""Builds EAN-5 addon pattern.

Parity is determined by a checksum calculation.
"""
# Calculate checksum for parity pattern
checksum = 0
for i, digit in enumerate(self.addon):
weight = 3 if i % 2 == 0 else 9
checksum += int(digit) * weight
checksum %= 10
parity = _ean.ADDON5_PARITY[checksum]

code = _ean.ADDON_START
for i, digit in enumerate(self.addon):
if i > 0:
code += _ean.ADDON_SEPARATOR
code += _ean.CODES[parity[i]][int(digit)]
return code

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

Expand All @@ -136,8 +218,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 +251,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 +270,21 @@ 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):
if self.guardbar:
return "< " + self.ean[:4] + " " + self.ean[4:] + " >"
return self.ean
base = "< " + self.ean[:4] + " " + self.ean[4:] + " >"
else:
base = self.ean
if self.addon:
return f"{base} {self.addon}"
return base


class EuropeanArticleNumber8WithGuard(EuropeanArticleNumber8):
Expand All @@ -204,8 +298,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