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.
- streepjescode-1.0.0/LICENSE +20 -0
- streepjescode-1.0.0/PKG-INFO +88 -0
- streepjescode-1.0.0/README.md +62 -0
- streepjescode-1.0.0/pyproject.toml +39 -0
- streepjescode-1.0.0/streepjescode/__init__.py +15 -0
- streepjescode-1.0.0/streepjescode/__main__.py +29 -0
- streepjescode-1.0.0/streepjescode/barcode.py +19 -0
- streepjescode-1.0.0/streepjescode/ean.py +90 -0
- streepjescode-1.0.0/streepjescode/exceptions.py +2 -0
- streepjescode-1.0.0/streepjescode/renderers.py +130 -0
- streepjescode-1.0.0/streepjescode/upc.py +79 -0
- streepjescode-1.0.0/streepjescode/utils.py +17 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
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,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)
|