Skip to content

Commit cc7d103

Browse files
committed
Add EAN-2 and EAN-5 addon support for UPC-A (EAN-12) barcodes as per GITN standard.
1 parent d10c26f commit cc7d103

File tree

6 files changed

+283
-36
lines changed

6 files changed

+283
-36
lines changed

barcode/charsets/addons.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Common addon patterns for EAN-2 and EAN-5 supplemental barcodes.
2+
3+
These patterns are shared by EAN-13, EAN-8, UPC-A, and related barcode types.
4+
Based on GS1/ISO standard.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
# Addon guard patterns
10+
ADDON_START = "1011" # Start guard for addon
11+
ADDON_SEPARATOR = "01" # Separator between addon digits
12+
13+
# Addon digit encoding (uses A and B parity patterns)
14+
ADDON_CODES = {
15+
"A": (
16+
"0001101",
17+
"0011001",
18+
"0010011",
19+
"0111101",
20+
"0100011",
21+
"0110001",
22+
"0101111",
23+
"0111011",
24+
"0110111",
25+
"0001011",
26+
),
27+
"B": (
28+
"0100111",
29+
"0110011",
30+
"0011011",
31+
"0100001",
32+
"0011101",
33+
"0111001",
34+
"0000101",
35+
"0010001",
36+
"0001001",
37+
"0010111",
38+
),
39+
}
40+
41+
# EAN-2 parity patterns: determined by value mod 4
42+
ADDON2_PARITY = (
43+
"AA", # 0
44+
"AB", # 1
45+
"BA", # 2
46+
"BB", # 3
47+
)
48+
49+
# EAN-5 parity patterns: determined by checksum
50+
ADDON5_PARITY = (
51+
"BBAAA", # 0
52+
"BABAA", # 1
53+
"BAABA", # 2
54+
"BAAAB", # 3
55+
"ABBAA", # 4
56+
"AABBA", # 5
57+
"AAABB", # 6
58+
"ABABA", # 7
59+
"ABAAB", # 8
60+
"AABAB", # 9
61+
)
62+

barcode/charsets/ean.py

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
from __future__ import annotations
22

3+
from barcode.charsets.addons import ADDON2_PARITY
4+
from barcode.charsets.addons import ADDON5_PARITY
5+
from barcode.charsets.addons import ADDON_SEPARATOR
6+
from barcode.charsets.addons import ADDON_START
7+
8+
# Note: Addon codes use CODES["A"] and CODES["B"] defined below
9+
310
EDGE = "101"
411
MIDDLE = "01010"
512
CODES = {
@@ -53,29 +60,15 @@
5360
"ABBABA",
5461
)
5562

56-
# EAN-2/EAN-5 Addon patterns (GS1/ISO standard)
57-
ADDON_START = "1011" # Start guard for addon
58-
ADDON_SEPARATOR = "01" # Separator between addon digits
59-
60-
# EAN-2 parity patterns: determined by value mod 4
61-
ADDON2_PARITY = (
62-
"AA", # 0
63-
"AB", # 1
64-
"BA", # 2
65-
"BB", # 3
66-
)
67-
68-
# EAN-5 parity patterns: determined by checksum
69-
ADDON5_PARITY = (
70-
"BBAAA", # 0
71-
"BABAA", # 1
72-
"BAABA", # 2
73-
"BAAAB", # 3
74-
"ABBAA", # 4
75-
"AABBA", # 5
76-
"AAABB", # 6
77-
"ABABA", # 7
78-
"ABAAB", # 8
79-
"AABAB", # 9
80-
)
63+
# Re-export addon constants for backwards compatibility
64+
__all__ = [
65+
"ADDON2_PARITY",
66+
"ADDON5_PARITY",
67+
"ADDON_SEPARATOR",
68+
"ADDON_START",
69+
"CODES",
70+
"EDGE",
71+
"LEFT_PATTERN",
72+
"MIDDLE",
73+
]
8174

barcode/charsets/upc.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from __future__ import annotations
22

3+
from barcode.charsets.addons import ADDON2_PARITY
4+
from barcode.charsets.addons import ADDON5_PARITY
5+
from barcode.charsets.addons import ADDON_CODES
6+
from barcode.charsets.addons import ADDON_SEPARATOR
7+
from barcode.charsets.addons import ADDON_START
8+
39
EDGE = "101"
410
MIDDLE = "01010"
511
CODES = {
@@ -28,3 +34,16 @@
2834
"1110100",
2935
),
3036
}
37+
38+
# Re-export addon constants for backwards compatibility
39+
__all__ = [
40+
"ADDON2_PARITY",
41+
"ADDON5_PARITY",
42+
"ADDON_CODES",
43+
"ADDON_SEPARATOR",
44+
"ADDON_START",
45+
"CODES",
46+
"EDGE",
47+
"MIDDLE",
48+
]
49+

barcode/upc.py

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ class UniversalProductCodeA(Barcode):
2525

2626
digits = 11
2727

28-
def __init__(self, upc, writer=None, make_ean=False) -> None:
28+
def __init__(
29+
self,
30+
upc: str,
31+
writer=None,
32+
make_ean: bool = False,
33+
addon: str | None = None,
34+
) -> None:
2935
"""Initializes new UPC-A barcode.
3036
3137
:param str upc: The upc number as string.
@@ -34,6 +40,7 @@ def __init__(self, upc, writer=None, make_ean=False) -> None:
3440
:param bool make_ean: Indicates if a leading zero should be added to
3541
the barcode. This converts the UPC into a valid European Article
3642
Number (EAN).
43+
:param addon: Optional 2 or 5 digit addon (EAN-2 or EAN-5).
3744
"""
3845
self.ean = make_ean
3946
upc = upc[: self.digits]
@@ -45,19 +52,35 @@ def __init__(self, upc, writer=None, make_ean=False) -> None:
4552
)
4653
self.upc = upc
4754
self.upc = f"{upc}{self.calculate_checksum()}"
55+
56+
# Validate and store addon
57+
self.addon: str | None = None
58+
if addon is not None:
59+
addon = addon.strip()
60+
if addon:
61+
if not addon.isdigit():
62+
raise IllegalCharacterError(
63+
f"Addon can only contain numbers, got {addon}."
64+
)
65+
if len(addon) not in (2, 5):
66+
raise NumberOfDigitsError(
67+
f"Addon must be 2 or 5 digits, received {len(addon)}."
68+
)
69+
self.addon = addon
70+
4871
self.writer = writer or self.default_writer()
4972

5073
def __str__(self) -> str:
51-
if self.ean:
52-
return "0" + self.upc
53-
54-
return self.upc
74+
base = "0" + self.upc if self.ean else self.upc
75+
if self.addon:
76+
return f"{base} {self.addon}"
77+
return base
5578

5679
def get_fullcode(self):
57-
if self.ean:
58-
return "0" + self.upc
59-
60-
return self.upc
80+
base = "0" + self.upc if self.ean else self.upc
81+
if self.addon:
82+
return f"{base} {self.addon}"
83+
return base
6184

6285
def calculate_checksum(self):
6386
"""Calculates the checksum for UPCA/UPC codes
@@ -86,7 +109,7 @@ def build(self) -> list[str]:
86109
"""
87110
code = _upc.EDGE[:]
88111

89-
for _i, number in enumerate(self.upc[0:6]):
112+
for number in self.upc[0:6]:
90113
code += _upc.CODES["L"][int(number)]
91114

92115
code += _upc.MIDDLE
@@ -96,8 +119,59 @@ def build(self) -> list[str]:
96119

97120
code += _upc.EDGE
98121

122+
# Add addon if present
123+
if self.addon:
124+
code += self._build_addon()
125+
99126
return [code]
100127

128+
def _build_addon(self) -> str:
129+
"""Builds the addon barcode pattern (EAN-2 or EAN-5).
130+
131+
:returns: The addon pattern as string
132+
"""
133+
if not self.addon:
134+
return ""
135+
136+
if len(self.addon) == 2:
137+
return self._build_addon2()
138+
return self._build_addon5()
139+
140+
def _build_addon2(self) -> str:
141+
"""Builds EAN-2 addon pattern.
142+
143+
Parity is determined by the 2-digit value mod 4.
144+
"""
145+
value = int(self.addon)
146+
parity = _upc.ADDON2_PARITY[value % 4]
147+
148+
code = _upc.ADDON_START
149+
for i, digit in enumerate(self.addon):
150+
if i > 0:
151+
code += _upc.ADDON_SEPARATOR
152+
code += _upc.ADDON_CODES[parity[i]][int(digit)]
153+
return code
154+
155+
def _build_addon5(self) -> str:
156+
"""Builds EAN-5 addon pattern.
157+
158+
Parity is determined by a checksum calculation.
159+
"""
160+
# Calculate checksum for parity pattern
161+
checksum = 0
162+
for i, digit in enumerate(self.addon):
163+
weight = 3 if i % 2 == 0 else 9
164+
checksum += int(digit) * weight
165+
checksum %= 10
166+
parity = _upc.ADDON5_PARITY[checksum]
167+
168+
code = _upc.ADDON_START
169+
for i, digit in enumerate(self.addon):
170+
if i > 0:
171+
code += _upc.ADDON_SEPARATOR
172+
code += _upc.ADDON_CODES[parity[i]][int(digit)]
173+
return code
174+
101175
def to_ascii(self) -> str:
102176
"""Returns an ascii representation of the barcode.
103177

docs/changelog.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
Changelog
22
---------
33

4-
v0.16.3
4+
current
55
~~~~~~~
66
* Added support for EAN-2 and EAN-5 addons. These supplemental barcodes can be
77
added to EAN-13, EAN-8, ISBN-13, ISBN-10, and ISSN barcodes via the ``addon``
88
parameter. EAN-2 is commonly used for periodical issue numbers, EAN-5 for
99
book prices.
10+
* Added support for EAN-2 and EAN-5 addons to UPC-A barcodes via the ``addon``
11+
parameter, following the same interface as EAN-13.
1012
* Fixed ISSN to accept full EAN-13 format (13 digits starting with 977) and
1113
preserve digits 11-12 (sequence variant) instead of always replacing them
1214
with "00".

0 commit comments

Comments
 (0)