streepjescode 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2026- Jesse Hitch
2
+ Copyright (c) 2020-2022 Philipp Hagemeister
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: streepjescode
3
+ Version: 1.0.0
4
+ Summary: Generate nicely formatted barcodes (UPC-A and EAN)
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: barcode,UPC
8
+ Author: Philipp Hagemeister
9
+ Author-email: phihag@phihag.de
10
+ Requires-Python: >=3.10,<4.0.0
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Project-URL: Documentation, http://git.open.engineering/jessebot/streepjescode
22
+ Project-URL: Homepage, http://git.open.engineering/jessebot/streepjescode
23
+ Project-URL: Repository, http://codeberg.org/jessebot/streepjescode
24
+ Description-Content-Type: text/markdown
25
+
26
+ # beautiful_barcode
27
+
28
+ Generate well-formatted, production-ready Barcodes.
29
+
30
+ By default, existing Python barcode libraries like [python-barcode](https://pypi.org/project/python-barcode/) generate good barcodes, but any and all formatting of the text is left up to the user. beautiful_barcode generates a nicely formatted barcode with interleaved text out of the box:
31
+
32
+ ![Example barcode](example_path.svg)
33
+
34
+ Depending on your renderer (and true by default), text in the barcode is *not* an SVG `<text>` element, as such an elment may render differently on different machines depending on font availability.
35
+
36
+ This library is currently limited to UPC-A/EAN and EPS/SVG – that's all we (the original authors) needed. Patches welcome!
37
+
38
+ # Installation
39
+
40
+ ```sh
41
+ $ pip install beautiful_barcode
42
+ ```
43
+
44
+ # Usage
45
+
46
+ ```
47
+ >>> from beautiful_barcode import GTIN
48
+ >>> GTIN('123456789012').write('output.svg')
49
+ ```
50
+
51
+ Command line:
52
+
53
+ ```sh
54
+ $ python -m beautiful_barcode 123456789012 -o output.svg
55
+ ```
56
+
57
+ ## Quickstart
58
+
59
+ ```bash
60
+ ~$ git clone https://github.com/boxine/beautiful_barcode.git
61
+ ~$ cd beautiful_barcode
62
+ ~/beautiful_barcode$ make
63
+ help List all commands
64
+ install-poetry install or update poetry
65
+ install install via poetry
66
+ update Update the dependencies as according to the pyproject.toml file
67
+ lint Run code formatters and linter
68
+ fix-code-style Fix code formatting
69
+ tox-listenvs List all tox test environments
70
+ tox Run pytest via tox with all environments
71
+ tox-py36 Run pytest via tox with *python v3.6*
72
+ tox-py37 Run pytest via tox with *python v3.7*
73
+ tox-py38 Run pytest via tox with *python v3.8*
74
+ tox-py39 Run pytest via tox with *python v3.9*
75
+ pytest Run pytest
76
+ pytest-ci Run pytest with CI settings
77
+ publish Release new version to PyPi
78
+ makemessages Make and compile locales message files
79
+ ```
80
+
81
+ # Attribution
82
+
83
+ This project was forked from [boxine/beautiful_barcode](https://github.com/boxine/beautiful_barcode)in order to add a no AI policy, build for the latest version of Python (and poetry), and maybe also add a proper CLI for convenience.
84
+
85
+ ## License
86
+
87
+ [MIT](./LICENSE)
88
+
@@ -0,0 +1,62 @@
1
+ # beautiful_barcode
2
+
3
+ Generate well-formatted, production-ready Barcodes.
4
+
5
+ By default, existing Python barcode libraries like [python-barcode](https://pypi.org/project/python-barcode/) generate good barcodes, but any and all formatting of the text is left up to the user. beautiful_barcode generates a nicely formatted barcode with interleaved text out of the box:
6
+
7
+ ![Example barcode](example_path.svg)
8
+
9
+ Depending on your renderer (and true by default), text in the barcode is *not* an SVG `<text>` element, as such an elment may render differently on different machines depending on font availability.
10
+
11
+ This library is currently limited to UPC-A/EAN and EPS/SVG – that's all we (the original authors) needed. Patches welcome!
12
+
13
+ # Installation
14
+
15
+ ```sh
16
+ $ pip install beautiful_barcode
17
+ ```
18
+
19
+ # Usage
20
+
21
+ ```
22
+ >>> from beautiful_barcode import GTIN
23
+ >>> GTIN('123456789012').write('output.svg')
24
+ ```
25
+
26
+ Command line:
27
+
28
+ ```sh
29
+ $ python -m beautiful_barcode 123456789012 -o output.svg
30
+ ```
31
+
32
+ ## Quickstart
33
+
34
+ ```bash
35
+ ~$ git clone https://github.com/boxine/beautiful_barcode.git
36
+ ~$ cd beautiful_barcode
37
+ ~/beautiful_barcode$ make
38
+ help List all commands
39
+ install-poetry install or update poetry
40
+ install install via poetry
41
+ update Update the dependencies as according to the pyproject.toml file
42
+ lint Run code formatters and linter
43
+ fix-code-style Fix code formatting
44
+ tox-listenvs List all tox test environments
45
+ tox Run pytest via tox with all environments
46
+ tox-py36 Run pytest via tox with *python v3.6*
47
+ tox-py37 Run pytest via tox with *python v3.7*
48
+ tox-py38 Run pytest via tox with *python v3.8*
49
+ tox-py39 Run pytest via tox with *python v3.9*
50
+ pytest Run pytest
51
+ pytest-ci Run pytest with CI settings
52
+ publish Release new version to PyPi
53
+ makemessages Make and compile locales message files
54
+ ```
55
+
56
+ # Attribution
57
+
58
+ This project was forked from [boxine/beautiful_barcode](https://github.com/boxine/beautiful_barcode)in order to add a no AI policy, build for the latest version of Python (and poetry), and maybe also add a proper CLI for convenience.
59
+
60
+ ## License
61
+
62
+ [MIT](./LICENSE)
@@ -0,0 +1,39 @@
1
+ [tool.poetry]
2
+ name = 'streepjescode'
3
+ version = "1.0.0"
4
+ description = 'Generate nicely formatted barcodes (UPC-A and EAN)'
5
+ authors = [
6
+ 'Philipp Hagemeister <phihag@phihag.de>',
7
+ 'Jesse Hitch <jessebot@linux.com>',
8
+ ]
9
+ classifiers = [
10
+ 'Development Status :: 5 - Production/Stable',
11
+ 'Topic :: Software Development :: Libraries :: Python Modules',
12
+ 'Intended Audience :: Developers',
13
+ ]
14
+ homepage = "http://git.open.engineering/jessebot/streepjescode"
15
+ repository = "http://codeberg.org/jessebot/streepjescode"
16
+ documentation = "http://git.open.engineering/jessebot/streepjescode"
17
+ license = 'MIT'
18
+ keywords = ['barcode', 'UPC']
19
+ readme = 'README.md'
20
+ packages = [{include = "streepjescode"}]
21
+
22
+ [tool.poetry.dependencies]
23
+ python = '>=3.10,<4.0.0'
24
+
25
+ [poetry.group.dev.dependencies]
26
+ tox = '*'
27
+ pytest = '>=6.2.5' # https://stackoverflow.com/a/69569206/35070
28
+ pytest-randomly = '*'
29
+ pytest-cov = '*'
30
+ pytest-django = '*'
31
+ flake8 = '*'
32
+ flynt = '*'
33
+ autopep8 = '*'
34
+ isort = '*'
35
+ poetry-publish = "*" # https://github.com/jedie/poetry-publish
36
+
37
+ [build-system]
38
+ requires = ["poetry-core"]
39
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,15 @@
1
+ __version__ = '1.1.3'
2
+
3
+ from .ean import EAN # noqa
4
+ from .exceptions import InvalidGTIN
5
+ from .upc import UPCA # noqa
6
+
7
+
8
+ def GTIN(gtin):
9
+ length = len(gtin)
10
+ if length == 12:
11
+ return UPCA(gtin)
12
+ elif length == 13:
13
+ return EAN(gtin)
14
+ else:
15
+ raise InvalidGTIN(f'Unsupported GTIN {gtin!r} of length {length}')
@@ -0,0 +1,29 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from . import GTIN, renderers
5
+
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser('Generate a barcode')
9
+ parser.add_argument('-o', '--output', metavar='FILENAME', help='Output file', default='-')
10
+ parser.add_argument(
11
+ '-r', '--renderer', default='auto', choices=renderers.NAMED_RENDERERS.keys(),
12
+ help='Renderer implementation to use.')
13
+ parser.add_argument(
14
+ 'NUMBER',
15
+ help='UPC-A or EAN number, for example 123456789012 or 4251192108913')
16
+ args = parser.parse_args()
17
+
18
+ barcode = GTIN(args.NUMBER)
19
+ render_kwargs = {
20
+ 'renderer': args.renderer,
21
+ }
22
+ if args.output == '-':
23
+ sys.stdout.buffer.write(barcode.render(**render_kwargs))
24
+ else:
25
+ barcode.write(args.output, **render_kwargs)
26
+
27
+
28
+ if __name__ == '__main__':
29
+ main()
@@ -0,0 +1,19 @@
1
+ from . import renderers
2
+
3
+
4
+ class Barcode:
5
+ def to_modules(self) -> str:
6
+ """ Convert the Barcode to modules (1 or 0) """
7
+ raise NotImplementedError
8
+
9
+ def render(self, renderer='auto', filename=None) -> bytes:
10
+ """ Renders the barcode as a bytes object """
11
+
12
+ rend = renderers.make_renderer(renderer, filename=filename)
13
+ self._paint(rend)
14
+ return rend.to_bytes()
15
+
16
+ def write(self, filename, renderer='auto'):
17
+ data = self.render(renderer=renderer, filename=filename)
18
+ with open(filename, 'wb') as out_file:
19
+ out_file.write(data)
@@ -0,0 +1,90 @@
1
+ import re
2
+
3
+ from .barcode import Barcode
4
+ from .exceptions import InvalidGTIN
5
+ from .utils import module_coords
6
+
7
+
8
+ # noqa See https://en.wikipedia.org/wiki/International_Article_Number#Binary_encoding_of_data_digits_into_EAN-13_barcode
9
+ _START_END = '101'
10
+ _MIDDLE = '01010'
11
+ _L_DIGITS = (
12
+ '0001101', '0011001', '0010011', '0111101', '0100011',
13
+ '0110001', '0101111', '0111011', '0110111', '0001011',
14
+ )
15
+ _G_DIGITS = (
16
+ '0100111', '0110011', '0011011', '0100001', '0011101',
17
+ '0111001', '0000101', '0010001', '0001001', '0010111',
18
+ )
19
+ _DIGITS_BY_GROUP = {'L': _L_DIGITS, 'G': _G_DIGITS}
20
+ _GROUPS_BY_FIRST_DIGIT = (
21
+ 'LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG',
22
+ 'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL',
23
+ )
24
+ _R_DIGITS = (
25
+ '1110010', '1100110', '1101100', '1000010', '1011100',
26
+ '1001110', '1010000', '1000100', '1001000', '1110100',
27
+ )
28
+ _EAN_MODULE_COUNT = 95
29
+
30
+
31
+ def calc_ean_checksum(ean):
32
+ return (- sum(
33
+ (1 if pos % 2 == 0 else 3) * num
34
+ for pos, num in enumerate(map(int, ean[:-1])))) % 10
35
+
36
+
37
+ class EAN(Barcode):
38
+ def __init__(self, ean: str):
39
+ super().__init__()
40
+
41
+ assert isinstance(ean, str)
42
+ if not re.match(r'^[0-9]{13}$', ean):
43
+ raise InvalidGTIN(f'EAN {ean!r} is not 13 latin digits')
44
+ checksum = calc_ean_checksum(ean)
45
+ if checksum != int(ean[-1]):
46
+ raise InvalidGTIN(f'EAN checksum of {ean} should be {checksum}')
47
+
48
+ self.ean = ean
49
+
50
+ def to_modules(self):
51
+ groups = _GROUPS_BY_FIRST_DIGIT[int(self.ean[0])]
52
+
53
+ res = (
54
+ _START_END +
55
+ ''.join(_DIGITS_BY_GROUP[group][int(c)] for c, group in zip(self.ean[1:7], groups)) +
56
+ _MIDDLE +
57
+ ''.join(_R_DIGITS[int(c)] for c in self.ean[7:]) +
58
+ _START_END
59
+ )
60
+ assert len(res) == _EAN_MODULE_COUNT
61
+ return res
62
+
63
+ def _paint(self, renderer):
64
+ MODULE_WIDTH = 2
65
+ MODULE_HEIGHT_SMALL = 75
66
+ MODULE_HEIGHT = 90
67
+ QUIET_ZONE = 9
68
+ WIDTH = (QUIET_ZONE + _EAN_MODULE_COUNT + QUIET_ZONE) * MODULE_WIDTH
69
+ HEIGHT = 100
70
+
71
+ renderer.initialize(WIDTH, HEIGHT)
72
+ for left, width in module_coords(self.to_modules()):
73
+ use_large_module = (
74
+ (left + width <= 3) or
75
+ (left >= 45 and left < 50) or
76
+ left >= 92
77
+ )
78
+ renderer.rect(
79
+ x=(left + QUIET_ZONE) * MODULE_WIDTH,
80
+ y=0,
81
+ width=width * MODULE_WIDTH,
82
+ height=(MODULE_HEIGHT if use_large_module else MODULE_HEIGHT_SMALL)
83
+ )
84
+
85
+ # Write human-readable EAN
86
+ TEXT_Y = 95
87
+ TEXT_SCALE = 1.18 # slightly larger font (since EAN blocks less space than UPC)
88
+ renderer.text(self.ean[:1], x=3, y=TEXT_Y, scale=TEXT_SCALE)
89
+ renderer.text(self.ean[1:7], x=24, y=TEXT_Y, scale=TEXT_SCALE)
90
+ renderer.text(self.ean[7:], x=117, y=TEXT_Y, scale=TEXT_SCALE)
@@ -0,0 +1,2 @@
1
+ class InvalidGTIN(ValueError):
2
+ pass
@@ -0,0 +1,130 @@
1
+ import subprocess
2
+ import xml.dom.minidom
3
+
4
+
5
+ class SVGRenderer:
6
+ def initialize(self, width, height):
7
+ self.doc = xml.dom.minidom.Document()
8
+ self.root = self.doc.createElement('svg')
9
+ self.root.setAttribute('version', '1.1')
10
+ self.root.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
11
+ self.root.setAttribute('width', f'{width}px')
12
+ self.root.setAttribute('height', f'{height}px')
13
+ self.root.setAttribute('viewBox', f'0 0 {width} {height}')
14
+ self.doc.appendChild(self.root)
15
+
16
+ def rect(self, x, y, width, height):
17
+ rect = self.doc.createElement('rect')
18
+ rect.setAttribute('x', str(x))
19
+ rect.setAttribute('y', str(y))
20
+ rect.setAttribute('width', str(width))
21
+ rect.setAttribute('height', str(height))
22
+ rect.setAttribute('fill', '#000')
23
+ self.root.appendChild(rect)
24
+
25
+ def to_bytes(self):
26
+ return self.doc.toprettyxml(indent=' ').encode('utf-8')
27
+
28
+
29
+ class SimpleTextSVGRenderer(SVGRenderer):
30
+ def text(self, characters, x, y, scale=1):
31
+ font_size = 16 * scale
32
+ text_el = self.doc.createElement('text')
33
+ text_el.appendChild(self.doc.createTextNode(characters))
34
+ text_el.setAttribute('x', str(x))
35
+ text_el.setAttribute('y', str(y))
36
+ text_el.setAttribute('style', f'font-size: {font_size}pt; font-family: Arial;')
37
+ self.root.appendChild(text_el)
38
+
39
+
40
+ class InkscapeSVGRenderer(SimpleTextSVGRenderer):
41
+ """ Convert texts to paths so that the file renders the same everywhere """
42
+
43
+ def to_bytes(self):
44
+ plain_svg = super().to_bytes()
45
+ proc = subprocess.run(
46
+ ['inkscape', '--pipe', '--export-text-to-path', '--export-plain-svg',
47
+ '--export-area-page', '--export-type=svg'],
48
+ input=plain_svg, stdout=subprocess.PIPE, check=True)
49
+ return proc.stdout
50
+
51
+
52
+ class InkscapeEPSRenderer(SimpleTextSVGRenderer):
53
+ def to_bytes(self):
54
+ plain_svg = super().to_bytes()
55
+ proc = subprocess.run(
56
+ ['inkscape', '--pipe', '--export-text-to-path', '--export-type=eps'],
57
+ input=plain_svg, stdout=subprocess.PIPE, check=True)
58
+ return proc.stdout
59
+
60
+
61
+ class InkscapePDFRenderer(SimpleTextSVGRenderer):
62
+ def to_bytes(self):
63
+ plain_svg = super().to_bytes()
64
+ proc = subprocess.run(
65
+ ['inkscape', '--pipe', '--export-text-to-path', '--export-type=pdf'],
66
+ input=plain_svg, stdout=subprocess.PIPE, check=True)
67
+ return proc.stdout
68
+
69
+
70
+ # Digits as paths (so that they render the same on every machine, no matter the local fonts).
71
+ # Picked from our template. Looks to be just Arial or a variant thereof.
72
+ _DIGIT_PATH = {
73
+ '1': 'M 7.9479169,0 H 6.0729168 v -11.947917 q -0.6770833,0.645833 -1.78125,1.291667 -1.09375,0.645833 -1.9687501,0.9687497 V -11.5 q 1.5729167,-0.739584 2.7500001,-1.791667 1.1770834,-1.052083 1.6666667,-2.041667 h 1.2083334 z', # noqa
74
+ '2': 'M 10.739584,-1.8020834 V 0 H 0.64583335 Q 0.62500002,-0.67708335 0.86458336,-1.3020834 1.25,-2.3333334 2.0937501,-3.3333334 q 0.8541667,-1.0000001 2.4583334,-2.3125001 2.4895834,-2.0416667 3.3645834,-3.2291668 0.875,-1.1979167 0.875,-2.2604167 0,-1.114583 -0.8020833,-1.875 -0.7916667,-0.770833 -2.0729168,-0.770833 -1.3541667,0 -2.1666667,0.8125 -0.8125,0.8125 -0.8229167,2.25 L 1,-10.916667 q 0.1979167,-2.15625 1.4895834,-3.28125 1.2916667,-1.135417 3.4687501,-1.135417 2.1979167,0 3.4791668,1.21875 1.2812497,1.21875 1.2812497,3.020834 0,0.916666 -0.375,1.8020831 -0.3749997,0.8854166 -1.2499997,1.8645833 -0.8645834,0.9791667 -2.8854168,2.6875001 -1.6875,1.4166667 -2.1666667,1.9270834 -0.4791667,0.5 -0.7916667,1.0104167 z', # noqa
75
+ '3': 'm 0.89583336,-4.0312501 1.87500004,-0.25 q 0.3229167,1.59375 1.09375,2.3020834 0.7812501,0.6979167 1.8958334,0.6979167 1.3229167,0 2.2291668,-0.9166667 0.9166667,-0.9166667 0.9166667,-2.2708334 0,-1.2916667 -0.8437501,-2.1250001 -0.84375,-0.84375 -2.1458334,-0.84375 -0.53125,0 -1.3229167,0.2083333 l 0.2083334,-1.6458334 q 0.1875,0.020833 0.3020833,0.020833 1.1979167,0 2.1562501,-0.625 0.9583333,-0.6250001 0.9583333,-1.9270831 0,-1.03125 -0.6979166,-1.708334 -0.6979167,-0.677083 -1.8020834,-0.677083 -1.0937501,0 -1.8229168,0.6875 -0.7291666,0.6875 -0.9375,2.0625 l -1.875,-0.333333 q 0.34375,-1.885417 1.5625,-2.916667 1.21875,-1.041667 3.0312501,-1.041667 1.25,0 2.3020834,0.541667 1.0520834,0.53125 1.6041667,1.458333 0.5625004,0.927084 0.5625004,1.96875 0,0.989584 -0.5312504,1.8020837 -0.53125,0.8125 -1.5729167,1.2916667 1.3541667,0.3125 2.1041671,1.3020834 0.75,0.9791667 0.75,2.4583334 0,2.0000001 -1.4583337,3.3958334 -1.4583334,1.38541674 -3.6875001,1.38541674 -2.0104168,0 -3.3437501,-1.1979167 Q 1.0833334,-2.1250001 0.89583336,-4.0312501 Z', # noqa
76
+ '4': 'M 6.8958335,0 V -3.6562501 H 0.27083334 V -5.3750002 L 7.2395835,-15.270834 h 1.5312501 v 9.8958338 h 2.0625004 v 1.7187501 H 8.7708336 V 0 Z m 0,-5.3750002 v -6.8854168 l -4.7812501,6.8854168 z', # noqa
77
+ '5': 'M 0.88541669,-4.0000001 2.8541668,-4.1666668 q 0.21875,1.4375001 1.0104166,2.1666667 0.8020834,0.7187501 1.9270834,0.7187501 1.3541667,0 2.2916668,-1.0208334 0.9375,-1.0208334 0.9375,-2.7083334 0,-1.6041667 -0.90625,-2.5312501 -0.8958334,-0.9270834 -2.3541668,-0.9270834 -0.90625,0 -1.6354167,0.4166667 -0.7291667,0.40625 -1.1458333,1.0625001 L 1.21875,-7.2187502 2.6979167,-15.0625 h 7.5937503 v 1.791666 H 4.1979168 l -0.8229167,4.1041671 q 1.375,-0.9583331 2.8854168,-0.9583331 2,0 3.3750001,1.3854164 1.375,1.3854167 1.375,3.5625001 0,2.0729167 -1.2083334,3.5833335 -1.46875,1.85416667 -4.0104168,1.85416667 -2.0833334,0 -3.4062501,-1.1666667 -1.3125,-1.16666667 -1.50000001,-3.09375007 z', # noqa
78
+ '6': 'm 10.614584,-11.53125 -1.8645837,0.145833 q -0.25,-1.104167 -0.7083334,-1.604167 -0.7604167,-0.802083 -1.875,-0.802083 -0.8958334,0 -1.5729168,0.5 -0.8854167,0.645833 -1.3958333,1.885417 -0.5104167,1.239583 -0.5312501,3.5312498 0.6770834,-1.0312501 1.6562501,-1.5312501 0.9791667,-0.5 2.0520834,-0.5 1.875,0 3.1875001,1.3854167 1.3229167,1.3750001 1.3229167,3.5625001 0,1.4375001 -0.625,2.6770834 -0.6145834,1.2291667 -1.6979167,1.88541675 -1.0833334,0.65625002 -2.4583335,0.65625002 -2.34375,0 -3.8229167,-1.71875007 -1.47916674,-1.7291667 -1.47916674,-5.6875001 0,-4.4270835 1.63541674,-6.4375005 1.4270833,-1.75 3.8437501,-1.75 1.8020834,0 2.9479167,1.010417 1.1562501,1.010417 1.3854171,2.791667 z m -7.6562506,6.5833332 q 0,0.96875 0.40625,1.8541667 0.4166667,0.8854167 1.1562501,1.3541667 0.7395833,0.4583334 1.5520833,0.4583334 1.1875001,0 2.0416668,-0.9583334 0.8541667,-0.9583334 0.8541667,-2.6041667 0,-1.5833334 -0.8437501,-2.4895835 -0.84375,-0.9166666 -2.125,-0.9166666 -1.2708334,0 -2.1562501,0.9166666 -0.8854167,0.9062501 -0.8854167,2.3854168 z', # noqa
79
+ '7': 'm 1.0104167,-13.270834 v -1.802083 h 9.8854173 v 1.458333 Q 9.4375003,-12.0625 8.0000002,-9.4895836 6.5729169,-6.9166669 5.7916668,-4.1979168 5.2291668,-2.2812501 5.0729168,0 H 3.1458334 q 0.03125,-1.8020834 0.7083334,-4.3541668 0.6770833,-2.5520834 1.9375,-4.9166668 1.2708334,-2.3750004 2.6979168,-4.0000004 z', # noqa
80
+ '8': 'm 3.7708334,-8.2812502 q -1.1666667,-0.4270834 -1.7291667,-1.2187501 -0.5625,-0.7916667 -0.5625,-1.8958337 0,-1.666666 1.1979167,-2.802083 1.1979167,-1.135417 3.1875001,-1.135417 2.0000001,0 3.2187501,1.166667 1.2187504,1.15625 1.2187504,2.822917 0,1.0625 -0.5625004,1.8541664 -0.5520833,0.78125 -1.6875,1.2083334 1.40625,0.4583333 2.1354164,1.4791667 0.739584,1.0208333 0.739584,2.4375 0,1.9583334 -1.385417,3.2916668 -1.3854168,1.33333337 -3.6458335,1.33333337 -2.2604167,0 -3.6458334,-1.33333337 -1.38541674,-1.34375 -1.38541674,-3.3437501 0,-1.4895834 0.75000004,-2.4895834 0.7604167,-1.0104167 2.15625,-1.375 z m -0.375,-3.1770838 q 0,1.083334 0.6979167,1.7708337 0.6979167,0.6875 1.8125001,0.6875 1.0833333,0 1.7708334,-0.6770833 0.6979166,-0.6875004 0.6979166,-1.6770834 0,-1.03125 -0.71875,-1.729167 -0.7083333,-0.708333 -1.7708334,-0.708333 -1.0729167,0 -1.78125,0.6875 -0.7083334,0.6875 -0.7083334,1.645833 z m -0.6041667,7.0520839 q 0,0.8020833 0.3750001,1.5520833 0.3854166,0.7500001 1.1354167,1.1666667 0.75,0.4062501 1.6145833,0.4062501 1.3437501,0 2.2187501,-0.8645834 0.875,-0.8645834 0.875,-2.1979167 0,-1.3541667 -0.90625,-2.2395834 -0.8958334,-0.8854167 -2.2500001,-0.8854167 -1.3229167,0 -2.1979167,0.875 -0.8645834,0.875 -0.8645834,2.1875001 z', # noqa
81
+ '9': 'm 1.1666667,-3.5312501 1.8020834,-0.1666667 q 0.2291667,1.2708334 0.875,1.8437501 0.6458334,0.5729167 1.6562501,0.5729167 0.8645833,0 1.5104167,-0.3958334 0.65625,-0.3958333 1.0729167,-1.0520833 0.4166667,-0.6666667 0.6979167,-1.7916668 0.28125,-1.125 0.28125,-2.2916667 0,-0.125 -0.010417,-0.375 -0.5625,0.8958333 -1.5416667,1.4583334 -0.96875,0.5520833 -2.1041667,0.5520833 -1.8958334,0 -3.2083335,-1.375 -1.31250001,-1.3750001 -1.31250001,-3.6250005 0,-2.322916 1.36458341,-3.739583 1.375,-1.416667 3.4375001,-1.416667 1.4895833,0 2.7187501,0.802084 1.2395833,0.802083 1.8749997,2.291666 0.645834,1.479167 0.645834,4.2916671 0,2.9270834 -0.635417,4.6666668 -0.6354167,1.7291667 -1.8958334,2.63541675 -1.2500001,0.90625002 -2.9375001,0.90625002 -1.7916667,0 -2.9270834,-0.98958336 Q 1.3958334,-1.7291667 1.1666667,-3.5312501 Z m 7.6770836,-6.7395839 q 0,-1.614583 -0.8645834,-2.5625 -0.8541667,-0.947916 -2.0625001,-0.947916 -1.25,0 -2.1770834,1.020833 -0.9270833,1.020833 -0.9270833,2.645833 0,1.4583337 0.875,2.3750004 0.8854167,0.9062501 2.1770834,0.9062501 1.3020834,0 2.1354167,-0.9062501 0.8437501,-0.9166667 0.8437501,-2.5312504 z', # noqa
82
+ '0': 'm 0.88541669,-7.5312502 q 0,-2.7083338 0.55208331,-4.3541668 0.5625001,-1.65625 1.6562501,-2.552083 1.1041667,-0.895834 2.7708334,-0.895834 1.2291667,0 2.1562501,0.5 0.9270833,0.489584 1.53125,1.427084 0.6041664,0.927083 0.9479164,2.270833 0.34375,1.3333334 0.34375,3.6041668 0,2.6875001 -0.552083,4.3437501 -0.5520834,1.6458334 -1.6562501,2.55208341 -1.09375,0.89583336 -2.7708334,0.89583336 -2.2083334,0 -3.4687501,-1.58333337 -1.51041671,-1.9062501 -1.51041671,-6.2083335 z m 1.92708341,0 q 0,3.7604168 0.875,5.0104168 0.8854167,1.2395834 2.1770834,1.2395834 1.2916667,0 2.1666667,-1.2500001 0.8854167,-1.25 0.8854167,-5.0000001 0,-3.7708338 -0.8854167,-5.0104168 -0.875,-1.239583 -2.1875,-1.239583 -1.2916667,0 -2.0625001,1.09375 -0.96875,1.395833 -0.96875,5.1562498 z', # noqa
83
+ }
84
+ _DIGIT_WIDTH = {
85
+ '1': 11,
86
+ '2': 12,
87
+ '3': 12,
88
+ '4': 12,
89
+ '5': 12,
90
+ '6': 12,
91
+ '7': 12,
92
+ '8': 12,
93
+ '9': 12,
94
+ '0': 12,
95
+ }
96
+
97
+
98
+ class PathSVGRenderer(SVGRenderer):
99
+ def text(self, characters, x, y, scale=1):
100
+ scale_transform = '' if scale == 1 else f' scale({scale} {scale})'
101
+ for char in characters:
102
+ path_el = self.doc.createElement('path')
103
+ path_el.setAttribute('d', _DIGIT_PATH[char])
104
+ path_el.setAttribute('fill', '#000')
105
+ path_el.setAttribute('transform', f'translate({x}, {y}){scale_transform}')
106
+ self.root.appendChild(path_el)
107
+
108
+ x += _DIGIT_WIDTH[char] * scale
109
+
110
+
111
+ def make_renderer(spec, filename=None):
112
+ if isinstance(spec, SVGRenderer):
113
+ return spec # Already a renderer, great
114
+
115
+ if spec == 'auto' and filename and filename.lower().endswith('.eps'):
116
+ klass = InkscapeEPSRenderer
117
+ else:
118
+ klass = NAMED_RENDERERS.get(spec, spec)
119
+
120
+ return klass()
121
+
122
+
123
+ NAMED_RENDERERS = {
124
+ 'auto': PathSVGRenderer, # Subject to change
125
+ 'simple': SimpleTextSVGRenderer,
126
+ 'inkscape': InkscapeSVGRenderer,
127
+ 'inkscape_eps': InkscapeEPSRenderer,
128
+ 'inkscape_pdf': InkscapePDFRenderer,
129
+ 'path': PathSVGRenderer,
130
+ }
@@ -0,0 +1,79 @@
1
+ import re
2
+
3
+ from .barcode import Barcode
4
+ from .exceptions import InvalidGTIN
5
+ from .utils import module_coords
6
+
7
+
8
+ # See https://en.wikipedia.org/wiki/Universal_Product_Code#Encoding
9
+ _START_END = '101'
10
+ _MIDDLE = '01010'
11
+ _LEFT = (
12
+ '0001101', '0011001', '0010011', '0111101', '0100011', '0110001', '0101111', '0111011',
13
+ '0110111', '0001011',
14
+ )
15
+ _RIGHT = (
16
+ '1110010', '1100110', '1101100', '1000010', '1011100', '1001110', '1010000', '1000100',
17
+ '1001000', '1110100',
18
+ )
19
+ _UPC_MODULE_COUNT = 95
20
+
21
+
22
+ def calc_upc_checksum(upc):
23
+ return (- sum(
24
+ (3 if pos % 2 == 0 else 1) * num
25
+ for pos, num in enumerate(map(int, upc[:-1])))) % 10
26
+
27
+
28
+ class UPCA(Barcode):
29
+ def __init__(self, upc: str):
30
+ super().__init__()
31
+
32
+ assert isinstance(upc, str)
33
+ if not re.match(r'^[0-9]{12}$', upc):
34
+ raise InvalidGTIN(f'UPC {upc!r} is not 12 latin digits')
35
+ upc_checksum = calc_upc_checksum(upc)
36
+ if upc_checksum != int(upc[-1]):
37
+ raise InvalidGTIN(f'UPC checksum of {upc} should be {upc_checksum}')
38
+
39
+ self.upc = upc
40
+
41
+ def to_modules(self):
42
+ res = (
43
+ _START_END +
44
+ ''.join(_LEFT[int(c)] for c in self.upc[:6]) +
45
+ _MIDDLE +
46
+ ''.join(_RIGHT[int(c)] for c in self.upc[6:]) +
47
+ _START_END
48
+ )
49
+ assert len(res) == _UPC_MODULE_COUNT
50
+ return res
51
+
52
+ def _paint(self, renderer):
53
+ MODULE_WIDTH = 2
54
+ MODULE_HEIGHT_SMALL = 75
55
+ MODULE_HEIGHT = 90
56
+ QUIET_ZONE = 9
57
+ WIDTH = (QUIET_ZONE + _UPC_MODULE_COUNT + QUIET_ZONE) * MODULE_WIDTH
58
+ HEIGHT = 100
59
+
60
+ renderer.initialize(WIDTH, HEIGHT)
61
+ for left, width in module_coords(self.to_modules()):
62
+ use_large_module = (
63
+ (left + width < 11) or
64
+ (left > 45 and left + width < 50) or
65
+ left > 84
66
+ )
67
+ renderer.rect(
68
+ x=(left + QUIET_ZONE) * MODULE_WIDTH,
69
+ y=0,
70
+ width=width * MODULE_WIDTH,
71
+ height=(MODULE_HEIGHT if use_large_module else MODULE_HEIGHT_SMALL)
72
+ )
73
+
74
+ # Write human-readable UPC
75
+ TEXT_Y = 95
76
+ renderer.text(self.upc[:1], x=3, y=TEXT_Y)
77
+ renderer.text(self.upc[1:6], x=44, y=TEXT_Y)
78
+ renderer.text(self.upc[6:11], x=123, y=TEXT_Y)
79
+ renderer.text(self.upc[11:], x=212, y=TEXT_Y)
@@ -0,0 +1,17 @@
1
+ import re
2
+
3
+
4
+ def module_coords(modules):
5
+ """ Yields tuples of position to left (in modules) and width (in modules) of painted areas. """
6
+
7
+ assert re.match(r'^[01]*$', modules)
8
+
9
+ width = 0
10
+ for x, c in enumerate(modules):
11
+ if c == '0' and width > 0:
12
+ yield (x - width, width)
13
+ width = 0
14
+ elif c == '1':
15
+ width += 1
16
+ if width > 0:
17
+ yield (len(modules) - width, width)