libscanner-dmgen 1.0.0__py3-none-win_amd64.whl
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.
- dmgen/__init__.py +248 -0
- dmgen/__main__.py +7 -0
- dmgen/_cli.py +50 -0
- dmgen/_ffi.py +157 -0
- dmgen/_lib/dmgen.exe +0 -0
- dmgen/_lib/dmgen_c.dll +0 -0
- dmgen/license.py +107 -0
- libscanner_dmgen-1.0.0.dist-info/BUILD-INFO.json +15 -0
- libscanner_dmgen-1.0.0.dist-info/METADATA +82 -0
- libscanner_dmgen-1.0.0.dist-info/RECORD +23 -0
- libscanner_dmgen-1.0.0.dist-info/WHEEL +4 -0
- libscanner_dmgen-1.0.0.dist-info/entry_points.txt +2 -0
- libscanner_dmgen-1.0.0.dist-info/licenses/Apache-2.0-zxing.txt +205 -0
- libscanner_dmgen-1.0.0.dist-info/licenses/GCC-COPYING.RUNTIME.txt +73 -0
- libscanner_dmgen-1.0.0.dist-info/licenses/GCC-COPYING3.txt +674 -0
- libscanner_dmgen-1.0.0.dist-info/licenses/LICENSE.en.txt +112 -0
- libscanner_dmgen-1.0.0.dist-info/licenses/LICENSE.txt +103 -0
- libscanner_dmgen-1.0.0.dist-info/licenses/MinGW-w64-runtime-COPYING.txt +265 -0
- libscanner_dmgen-1.0.0.dist-info/licenses/THIRD-PARTY-NOTICES.txt +62 -0
- libscanner_dmgen-1.0.0.dist-info/licenses/libdmtx-LICENSE.txt +35 -0
- libscanner_dmgen-1.0.0.dist-info/licenses/monocypher-LICENCE.md +167 -0
- libscanner_dmgen-1.0.0.dist-info/licenses/stb-LICENSE.txt +41 -0
- libscanner_dmgen-1.0.0.dist-info/licenses/winpthreads-COPYING.txt +57 -0
dmgen/__init__.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""dmgen — генератор DataMatrix ECC 200 (ГОСТ Р ИСО/МЭК 16022) с проверкой
|
|
2
|
+
GS1 и профиля «Честного знака». Обёртка на ctypes над dmgen_c.
|
|
3
|
+
|
|
4
|
+
import dmgen
|
|
5
|
+
png = dmgen.make_image("0104601234567893215Abc12Xyz!-\\x1d93dGVz", gs1="cz")
|
|
6
|
+
|
|
7
|
+
Лицензия — подмодуль dmgen.license (как namespace dmgen::license в C++ API).
|
|
8
|
+
Без пробного периода и лицензии кодирование поднимает UnlicensedError.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import ctypes
|
|
14
|
+
import os
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import List, Optional, Tuple, Union
|
|
17
|
+
|
|
18
|
+
from . import _ffi
|
|
19
|
+
from . import license # noqa: F401 (публичный подмодуль)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"make_image", "save", "encode_matrix", "validate_gs1", "version", "SIZES",
|
|
23
|
+
"DmgenError", "EncodeError", "Gs1Error", "UnlicensedError", "Report", "Element", "Issue",
|
|
24
|
+
"license",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
Data = Union[str, bytes, bytearray, memoryview]
|
|
28
|
+
|
|
29
|
+
# Коды возврата dmgen_c.
|
|
30
|
+
_OK = 0
|
|
31
|
+
_E_GS1_INVALID = 5
|
|
32
|
+
_E_UNLICENSED = 8
|
|
33
|
+
|
|
34
|
+
_GS1 = {None: 0, False: 0, "off": 0, True: 1, "gs1": 1, "cz": 2, "chestny_znak": 2}
|
|
35
|
+
_SEPARATOR = {"gs": 0, "fnc1": 1}
|
|
36
|
+
_INPUT = {"auto": 0, "raw": 1, "bracketed": 2}
|
|
37
|
+
_SCHEME = {"auto": 0, "ascii": 1, "c40": 2, "text": 3, "x12": 4, "edifact": 5,
|
|
38
|
+
"base256": 6, "minimal": 7}
|
|
39
|
+
_FORMAT = {"png": 0, "jpeg": 1, "jpg": 1}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DmgenError(RuntimeError):
|
|
43
|
+
"""Ошибка dmgen: code — код dmgen_status, message — описание по-русски."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, code: int, message: str):
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.code = code
|
|
48
|
+
self.message = message
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class EncodeError(DmgenError):
|
|
52
|
+
"""Данные не удалось закодировать (размер, недопустимый знак, запись файла…)."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class UnlicensedError(DmgenError):
|
|
56
|
+
"""Пробный период кончился, лицензии нет — см. dmgen.license.status()."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Gs1Error(DmgenError):
|
|
60
|
+
"""Строка не прошла проверку GS1; подробности — в report."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, code: int, message: str, report: "Report"):
|
|
63
|
+
super().__init__(code, message)
|
|
64
|
+
self.report = report
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class Element:
|
|
69
|
+
ai: str
|
|
70
|
+
value: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class Issue:
|
|
75
|
+
code: int
|
|
76
|
+
warning: bool
|
|
77
|
+
position: int
|
|
78
|
+
ai: str
|
|
79
|
+
message: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class Report:
|
|
84
|
+
ok: bool
|
|
85
|
+
elements: List[Element] = field(default_factory=list)
|
|
86
|
+
issues: List[Issue] = field(default_factory=list)
|
|
87
|
+
normalized: str = "" # с символом GS (\x1d) — как вернёт ридер
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _bytes(data: Data) -> bytes:
|
|
91
|
+
if isinstance(data, str):
|
|
92
|
+
return data.encode("utf-8")
|
|
93
|
+
return bytes(data)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _lookup(table: dict, value, what: str) -> int:
|
|
97
|
+
key = value.lower() if isinstance(value, str) else value
|
|
98
|
+
if key not in table:
|
|
99
|
+
raise ValueError(f"{what}: недопустимое значение {value!r}; допустимо: "
|
|
100
|
+
+ ", ".join(str(k) for k in table if isinstance(k, str)))
|
|
101
|
+
return table[key]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
_INT32_MIN, _INT32_MAX = -2**31, 2**31 - 1
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _int32(value, what: str) -> int:
|
|
108
|
+
# ctypes.c_int32 молча обрезает большое число по модулю 2**32: 2**32 + 8
|
|
109
|
+
# дошло бы до библиотеки как 8. Поэтому диапазон — до приведения;
|
|
110
|
+
# допустимые значения по смыслу (module_px до 1000 и т.п.) проверяет dmgen_c.
|
|
111
|
+
v = int(value)
|
|
112
|
+
if not _INT32_MIN <= v <= _INT32_MAX:
|
|
113
|
+
raise ValueError(f"{what}: {value!r} вне диапазона int32")
|
|
114
|
+
return v
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _options(gs1=None, separator="gs", gs1_input="auto", scheme="auto", side=0, verify=True,
|
|
118
|
+
module_px=8, quiet_zone=2, invert=False, dark=0, light=255, dpi=0.0,
|
|
119
|
+
quality=100) -> _ffi.Options:
|
|
120
|
+
o = _ffi.Options()
|
|
121
|
+
_ffi.lib.dmgen_options_init(ctypes.byref(o))
|
|
122
|
+
o.gs1 = _lookup(_GS1, gs1, "gs1")
|
|
123
|
+
o.separator = _lookup(_SEPARATOR, separator, "separator")
|
|
124
|
+
o.gs1_input = _lookup(_INPUT, gs1_input, "gs1_input")
|
|
125
|
+
o.scheme = _lookup(_SCHEME, scheme, "scheme")
|
|
126
|
+
o.side = _int32(side, "side")
|
|
127
|
+
o.verify = 1 if verify else 0
|
|
128
|
+
o.module_px = _int32(module_px, "module_px")
|
|
129
|
+
o.quiet_zone = _int32(quiet_zone, "quiet_zone")
|
|
130
|
+
o.invert = 1 if invert else 0
|
|
131
|
+
o.dark = _int32(dark, "dark")
|
|
132
|
+
o.light = _int32(light, "light")
|
|
133
|
+
o.dpi = float(dpi)
|
|
134
|
+
o.jpeg_quality = _int32(quality, "quality")
|
|
135
|
+
return o
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _raise(status: int, err: _ffi.Error, data: bytes, opts: _ffi.Options) -> None:
|
|
139
|
+
message = err.message.decode("utf-8", "replace")
|
|
140
|
+
if status == _E_GS1_INVALID:
|
|
141
|
+
mode = "cz" if opts.gs1 == 2 else "gs1"
|
|
142
|
+
raise Gs1Error(status, message, _validate(data, mode, opts.gs1_input))
|
|
143
|
+
if status == _E_UNLICENSED:
|
|
144
|
+
raise UnlicensedError(status, message)
|
|
145
|
+
raise EncodeError(status, message)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def make_image(data: Data, *, fmt: str = "png", **kw) -> bytes:
|
|
149
|
+
"""Данные → файл PNG или JPEG в памяти.
|
|
150
|
+
|
|
151
|
+
Ключи: gs1 (None | "gs1" | "cz"), separator ("gs" | "fnc1"), gs1_input,
|
|
152
|
+
scheme, side (0 или 10…144), verify, module_px, quiet_zone, invert, dark,
|
|
153
|
+
light, dpi, quality (JPEG).
|
|
154
|
+
"""
|
|
155
|
+
raw = _bytes(data)
|
|
156
|
+
opts = _options(**kw)
|
|
157
|
+
out = ctypes.c_void_p()
|
|
158
|
+
out_len = ctypes.c_size_t()
|
|
159
|
+
err = _ffi.Error()
|
|
160
|
+
st = _ffi.lib.dmgen_make_image(raw, len(raw), ctypes.byref(opts), _lookup(_FORMAT, fmt, "fmt"),
|
|
161
|
+
ctypes.byref(out), ctypes.byref(out_len), ctypes.byref(err))
|
|
162
|
+
if st != _OK:
|
|
163
|
+
_raise(st, err, raw, opts)
|
|
164
|
+
try:
|
|
165
|
+
return ctypes.string_at(out.value, out_len.value)
|
|
166
|
+
finally:
|
|
167
|
+
_ffi.lib.dmgen_free(out)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def save(data: Data, path: Union[str, os.PathLike], *, fmt: Optional[str] = None, **kw) -> None:
|
|
171
|
+
"""Данные → файл. Формат — по расширению (.jpg/.jpeg — JPEG, иначе PNG)."""
|
|
172
|
+
raw = _bytes(data)
|
|
173
|
+
p = os.fspath(path)
|
|
174
|
+
if fmt is None:
|
|
175
|
+
fmt = "jpeg" if p.lower().endswith((".jpg", ".jpeg")) else "png"
|
|
176
|
+
opts = _options(**kw)
|
|
177
|
+
err = _ffi.Error()
|
|
178
|
+
st = _ffi.lib.dmgen_save_image(raw, len(raw), ctypes.byref(opts), _lookup(_FORMAT, fmt, "fmt"),
|
|
179
|
+
p.encode("utf-8"), ctypes.byref(err))
|
|
180
|
+
if st != _OK:
|
|
181
|
+
_raise(st, err, raw, opts)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def encode_matrix(data: Data, **kw) -> List[List[int]]:
|
|
185
|
+
"""Данные → матрица модулей (строки сверху вниз, 1 — тёмный)."""
|
|
186
|
+
raw = _bytes(data)
|
|
187
|
+
opts = _options(**kw)
|
|
188
|
+
mods = ctypes.c_void_p()
|
|
189
|
+
side = ctypes.c_int32()
|
|
190
|
+
err = _ffi.Error()
|
|
191
|
+
st = _ffi.lib.dmgen_encode_matrix(raw, len(raw), ctypes.byref(opts), ctypes.byref(mods),
|
|
192
|
+
ctypes.byref(side), ctypes.byref(err))
|
|
193
|
+
if st != _OK:
|
|
194
|
+
_raise(st, err, raw, opts)
|
|
195
|
+
try:
|
|
196
|
+
n = side.value
|
|
197
|
+
flat = ctypes.string_at(mods.value, n * n)
|
|
198
|
+
return [list(flat[r * n:(r + 1) * n]) for r in range(n)]
|
|
199
|
+
finally:
|
|
200
|
+
_ffi.lib.dmgen_free(mods)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _validate(raw: bytes, mode, gs1_input) -> Report:
|
|
204
|
+
rep = ctypes.POINTER(_ffi.Gs1Report)()
|
|
205
|
+
err = _ffi.Error()
|
|
206
|
+
m = _lookup(_GS1, mode, "mode") or 1
|
|
207
|
+
inp = gs1_input if isinstance(gs1_input, int) else _lookup(_INPUT, gs1_input, "gs1_input")
|
|
208
|
+
st = _ffi.lib.dmgen_gs1_validate(raw, len(raw), m, inp, ctypes.byref(rep), ctypes.byref(err))
|
|
209
|
+
if st != _OK:
|
|
210
|
+
raise DmgenError(st, err.message.decode("utf-8", "replace"))
|
|
211
|
+
try:
|
|
212
|
+
r = rep.contents
|
|
213
|
+
elements = [
|
|
214
|
+
# AI берётся из ввода как есть и бывает не ASCII («(é1)…»): отчёт
|
|
215
|
+
# об ошибке, а не UnicodeDecodeError. Обрезка AI до 7 байт может
|
|
216
|
+
# разрезать знак UTF-8 — тоже "replace".
|
|
217
|
+
Element(r.elements[i].ai.decode("utf-8", "replace"),
|
|
218
|
+
ctypes.string_at(r.elements[i].value, r.elements[i].value_len).decode("utf-8", "replace"))
|
|
219
|
+
for i in range(r.element_count)
|
|
220
|
+
]
|
|
221
|
+
issues = [
|
|
222
|
+
Issue(r.issues[i].code, bool(r.issues[i].warning), r.issues[i].position,
|
|
223
|
+
r.issues[i].ai.decode("utf-8", "replace"), r.issues[i].message.decode("utf-8", "replace"))
|
|
224
|
+
for i in range(r.issue_count)
|
|
225
|
+
]
|
|
226
|
+
normalized = ctypes.string_at(r.normalized, r.normalized_len).decode("utf-8", "replace")
|
|
227
|
+
return Report(bool(r.ok), elements, issues, normalized)
|
|
228
|
+
finally:
|
|
229
|
+
_ffi.lib.dmgen_gs1_report_free(rep)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def validate_gs1(data: Data, mode: str = "gs1", gs1_input: str = "auto") -> Report:
|
|
233
|
+
"""Проверка строки по GS1 (mode="gs1") или профилю «Честного знака» (mode="cz")."""
|
|
234
|
+
return _validate(_bytes(data), mode, gs1_input)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def version() -> str:
|
|
238
|
+
"""Версия библиотеки dmgen_c."""
|
|
239
|
+
return _ffi.lib.dmgen_version_string().decode("ascii")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _sizes() -> Tuple[int, ...]:
|
|
243
|
+
buf = (ctypes.c_int32 * 32)()
|
|
244
|
+
n = _ffi.lib.dmgen_symbol_sizes(buf, 32)
|
|
245
|
+
return tuple(buf[i] for i in range(n))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
SIZES: Tuple[int, ...] = _sizes()
|
dmgen/__main__.py
ADDED
dmgen/_cli.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Команда ``dmgen`` и ``python -m dmgen`` — собранная утилита-оболочка из колеса.
|
|
2
|
+
|
|
3
|
+
Колесо поставки (tools/build_wheel.py) кладёт утилиту ``dmgen`` (``dmgen.exe`` и
|
|
4
|
+
DLL её рантайма на Windows) в ``dmgen/_lib/`` — рядом с библиотекой dmgen_c, — а
|
|
5
|
+
``pip install`` делает из этой функции команду ``dmgen`` в PATH окружения. Всё,
|
|
6
|
+
что умеет утилита, — как у неё: ``dmgen "текст" -o код.png``, ``dmgen --license status``,
|
|
7
|
+
``dmgen --help``; аргументы и код возврата передаются как есть.
|
|
8
|
+
|
|
9
|
+
Файл ключа ``dmgen.key`` для неё и для библиотеки — один, в ``dmgen/_lib/``:
|
|
10
|
+
утилита ищет его рядом с собой, библиотека — рядом с dmgen_c.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def executable() -> Path:
|
|
20
|
+
name = "dmgen.exe" if sys.platform == "win32" else "dmgen"
|
|
21
|
+
return Path(__file__).resolve().parent / "_lib" / name
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> int:
|
|
25
|
+
exe = executable()
|
|
26
|
+
if not exe.is_file():
|
|
27
|
+
sys.stderr.write(
|
|
28
|
+
f"утилиты dmgen нет в пакете ({exe}): она приходит с колесом поставки "
|
|
29
|
+
"(раздел «Загрузки» личного кабинета); пакет из исходников её не содержит.\n"
|
|
30
|
+
)
|
|
31
|
+
return 2
|
|
32
|
+
if sys.platform != "win32" and not os.access(exe, os.X_OK):
|
|
33
|
+
# Колесо, где бит исполнения не дошёл до установки (pip берёт его из
|
|
34
|
+
# режима файла в zip), — чиним на месте, если каталог наш.
|
|
35
|
+
try:
|
|
36
|
+
exe.chmod(exe.stat().st_mode | 0o111)
|
|
37
|
+
except OSError:
|
|
38
|
+
sys.stderr.write(f"утилита {exe} не исполняемая: chmod +x {exe}\n")
|
|
39
|
+
return 2
|
|
40
|
+
args = [str(exe), *sys.argv[1:]]
|
|
41
|
+
if sys.platform == "win32":
|
|
42
|
+
# На Windows exec заменяет процесс не так, как в POSIX (консоль и код
|
|
43
|
+
# возврата теряются) — запускаем дочерним и ждём.
|
|
44
|
+
return subprocess.call(args)
|
|
45
|
+
os.execv(args[0], args)
|
|
46
|
+
return 127 # недостижимо: execv не возвращается
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
sys.exit(main())
|
dmgen/_ffi.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Граница ctypes над dmgen_c (include/dmgen/dmgen_c.h).
|
|
2
|
+
|
|
3
|
+
Не публичный модуль: здесь только загрузка библиотеки и сигнатуры функций.
|
|
4
|
+
Возвращаемые библиотекой буферы объявлены c_void_p/POINTER, а не c_char_p:
|
|
5
|
+
иначе ctypes сделает копию, а указатель для dmgen_free потеряется; к тому
|
|
6
|
+
же данные GS1 содержат байт 0x1D и не обязаны кончаться нулём.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ctypes
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
ABI_VERSION = 1
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Options(ctypes.Structure):
|
|
20
|
+
_fields_ = [
|
|
21
|
+
("struct_size", ctypes.c_uint32),
|
|
22
|
+
("gs1", ctypes.c_int32),
|
|
23
|
+
("separator", ctypes.c_int32),
|
|
24
|
+
("gs1_input", ctypes.c_int32),
|
|
25
|
+
("scheme", ctypes.c_int32),
|
|
26
|
+
("side", ctypes.c_int32),
|
|
27
|
+
("verify", ctypes.c_int32),
|
|
28
|
+
("module_px", ctypes.c_int32),
|
|
29
|
+
("quiet_zone", ctypes.c_int32),
|
|
30
|
+
("invert", ctypes.c_int32),
|
|
31
|
+
("dark", ctypes.c_int32),
|
|
32
|
+
("light", ctypes.c_int32),
|
|
33
|
+
("jpeg_quality", ctypes.c_int32),
|
|
34
|
+
("reserved", ctypes.c_int32),
|
|
35
|
+
("dpi", ctypes.c_double),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Error(ctypes.Structure):
|
|
40
|
+
_fields_ = [("code", ctypes.c_int32), ("message", ctypes.c_char * 512)]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Gs1Element(ctypes.Structure):
|
|
44
|
+
_fields_ = [
|
|
45
|
+
("ai", ctypes.c_char * 8),
|
|
46
|
+
("value", ctypes.c_void_p),
|
|
47
|
+
("value_len", ctypes.c_size_t),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Gs1Issue(ctypes.Structure):
|
|
52
|
+
_fields_ = [
|
|
53
|
+
("code", ctypes.c_int32),
|
|
54
|
+
("warning", ctypes.c_int32),
|
|
55
|
+
("position", ctypes.c_int32),
|
|
56
|
+
("ai", ctypes.c_char * 8),
|
|
57
|
+
("message", ctypes.c_char * 256),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Gs1Report(ctypes.Structure):
|
|
62
|
+
_fields_ = [
|
|
63
|
+
("ok", ctypes.c_int32),
|
|
64
|
+
("element_count", ctypes.c_int32),
|
|
65
|
+
("elements", ctypes.POINTER(Gs1Element)),
|
|
66
|
+
("issue_count", ctypes.c_int32),
|
|
67
|
+
("issues", ctypes.POINTER(Gs1Issue)),
|
|
68
|
+
("normalized", ctypes.c_void_p),
|
|
69
|
+
("normalized_len", ctypes.c_size_t),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _packaged():
|
|
74
|
+
here = Path(__file__).resolve().parent / "_lib"
|
|
75
|
+
if sys.platform == "win32":
|
|
76
|
+
return [here / "dmgen_c.dll"]
|
|
77
|
+
if sys.platform == "darwin":
|
|
78
|
+
return [here / "libdmgen_c.dylib"]
|
|
79
|
+
return [here / "libdmgen_c.so", *sorted(here.glob("libdmgen_c.so.*"))]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _candidates():
|
|
83
|
+
# Библиотека колеса — только из _lib пакета, по абсолютному пути: ни
|
|
84
|
+
# переменная окружения, ни PATH/системные каталоги (ctypes.util.find_library)
|
|
85
|
+
# не подменяют её чужой или старой копией (аудит 2026-10-01, P-16).
|
|
86
|
+
packaged = [p for p in _packaged() if p.is_file()]
|
|
87
|
+
if packaged:
|
|
88
|
+
return packaged
|
|
89
|
+
# Пакет из исходников (разработка, meson test): _lib пуст — путь даёт DMGEN_LIB.
|
|
90
|
+
env = os.environ.get("DMGEN_LIB")
|
|
91
|
+
if env and Path(env).is_absolute():
|
|
92
|
+
return [Path(env)]
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _load():
|
|
97
|
+
tried = []
|
|
98
|
+
for p in _candidates():
|
|
99
|
+
tried.append(str(p))
|
|
100
|
+
try:
|
|
101
|
+
return ctypes.CDLL(str(p))
|
|
102
|
+
except OSError:
|
|
103
|
+
continue
|
|
104
|
+
raise OSError("не найдена библиотека dmgen_c" + ("; проверено: " + ", ".join(tried) if tried else "") +
|
|
105
|
+
". Она приходит с колесом поставки (dmgen/_lib); для пакета из исходников "
|
|
106
|
+
"укажите абсолютный путь к ней в переменной DMGEN_LIB.")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
lib = _load()
|
|
110
|
+
|
|
111
|
+
u8p = ctypes.POINTER(ctypes.c_uint8)
|
|
112
|
+
|
|
113
|
+
lib.dmgen_abi_version.restype = ctypes.c_uint32
|
|
114
|
+
lib.dmgen_abi_version.argtypes = []
|
|
115
|
+
lib.dmgen_version_string.restype = ctypes.c_char_p
|
|
116
|
+
lib.dmgen_version_string.argtypes = []
|
|
117
|
+
lib.dmgen_options_init.restype = None
|
|
118
|
+
lib.dmgen_options_init.argtypes = [ctypes.POINTER(Options)]
|
|
119
|
+
lib.dmgen_make_image.restype = ctypes.c_int32
|
|
120
|
+
lib.dmgen_make_image.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(Options), ctypes.c_int32,
|
|
121
|
+
ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t),
|
|
122
|
+
ctypes.POINTER(Error)]
|
|
123
|
+
lib.dmgen_save_image.restype = ctypes.c_int32
|
|
124
|
+
lib.dmgen_save_image.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(Options), ctypes.c_int32,
|
|
125
|
+
ctypes.c_char_p, ctypes.POINTER(Error)]
|
|
126
|
+
lib.dmgen_encode_matrix.restype = ctypes.c_int32
|
|
127
|
+
lib.dmgen_encode_matrix.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(Options),
|
|
128
|
+
ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_int32),
|
|
129
|
+
ctypes.POINTER(Error)]
|
|
130
|
+
lib.dmgen_gs1_validate.restype = ctypes.c_int32
|
|
131
|
+
lib.dmgen_gs1_validate.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int32, ctypes.c_int32,
|
|
132
|
+
ctypes.POINTER(ctypes.POINTER(Gs1Report)), ctypes.POINTER(Error)]
|
|
133
|
+
lib.dmgen_gs1_report_free.restype = None
|
|
134
|
+
lib.dmgen_gs1_report_free.argtypes = [ctypes.POINTER(Gs1Report)]
|
|
135
|
+
lib.dmgen_symbol_sizes.restype = ctypes.c_int32
|
|
136
|
+
lib.dmgen_symbol_sizes.argtypes = [ctypes.POINTER(ctypes.c_int32), ctypes.c_int32]
|
|
137
|
+
lib.dmgen_free.restype = None
|
|
138
|
+
lib.dmgen_free.argtypes = [ctypes.c_void_p]
|
|
139
|
+
|
|
140
|
+
# Лицензия: строки JSON — c_void_p, чтобы указатель дошёл до dmgen_free.
|
|
141
|
+
lib.dmgen_license_configure.restype = None
|
|
142
|
+
lib.dmgen_license_configure.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int32, ctypes.c_int32]
|
|
143
|
+
for _name, _args in [
|
|
144
|
+
("dmgen_license_status_json", []),
|
|
145
|
+
("dmgen_license_activate_json", [ctypes.c_char_p]),
|
|
146
|
+
("dmgen_license_refresh_json", []),
|
|
147
|
+
("dmgen_license_deactivate_json", [ctypes.c_char_p]),
|
|
148
|
+
("dmgen_license_deactivate_to_file_json", [ctypes.c_char_p, ctypes.c_char_p]),
|
|
149
|
+
("dmgen_license_write_activation_request_json", [ctypes.c_char_p]),
|
|
150
|
+
("dmgen_license_import_license_file_json", [ctypes.c_char_p]),
|
|
151
|
+
("dmgen_license_fingerprint_json", []),
|
|
152
|
+
]:
|
|
153
|
+
getattr(lib, _name).restype = ctypes.c_void_p
|
|
154
|
+
getattr(lib, _name).argtypes = _args
|
|
155
|
+
|
|
156
|
+
if lib.dmgen_abi_version() != ABI_VERSION:
|
|
157
|
+
raise OSError(f"dmgen_c: версия ABI {lib.dmgen_abi_version()}, обёртка ждёт {ABI_VERSION}")
|
dmgen/_lib/dmgen.exe
ADDED
|
Binary file
|
dmgen/_lib/dmgen_c.dll
ADDED
|
Binary file
|
dmgen/license.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Лицензия — обёртка над dmgen_license_* (dmgen_c.h). Функции отдают тот же
|
|
2
|
+
JSON, что и "dmgen --license ... --json", разобранный в обычный dict.
|
|
3
|
+
|
|
4
|
+
Пока нет ни пробного периода, ни лицензии, make_image/save/encode_matrix
|
|
5
|
+
поднимают dmgen.UnlicensedError. Основной способ активации — файл dmgen.key
|
|
6
|
+
с ключом рядом с dmgen_c.dll / libdmgen_c.so: вызывать ничего не нужно.
|
|
7
|
+
|
|
8
|
+
Отказ операции (неверный ключ, сеть недоступна и т.п.) — НЕ исключение: это
|
|
9
|
+
ожидаемый исход с понятной причиной, и он приходит как обычный dict
|
|
10
|
+
{"ok": False, "error": "...", "message": "...", "status": {...}}. Исключение
|
|
11
|
+
(DmgenError) — только если граница вернула NULL, то есть внутри поймано
|
|
12
|
+
исключение C++.
|
|
13
|
+
|
|
14
|
+
Пример::
|
|
15
|
+
|
|
16
|
+
import dmgen
|
|
17
|
+
|
|
18
|
+
s = dmgen.license.status()
|
|
19
|
+
if not s["can_encode"]:
|
|
20
|
+
print(s["message"])
|
|
21
|
+
|
|
22
|
+
r = dmgen.license.activate("XXXXX-XXXXX-XXXXX-XXXXX-XXXXX")
|
|
23
|
+
if not r["ok"]:
|
|
24
|
+
print(r["error"], r["message"])
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import ctypes
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
from typing import Any, Dict, Optional, Union
|
|
33
|
+
|
|
34
|
+
from . import _ffi
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"configure", "status", "activate", "refresh", "deactivate", "deactivate_to_file",
|
|
38
|
+
"write_activation_request", "import_license_file", "fingerprint",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
PathLike = Union[str, os.PathLike]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _take(ptr: Optional[int]) -> Dict[str, Any]:
|
|
45
|
+
"""JSON из строки библиотеки; строку освобождает dmgen_free."""
|
|
46
|
+
if not ptr:
|
|
47
|
+
from . import DmgenError # здесь: __init__ импортирует этот модуль
|
|
48
|
+
raise DmgenError(-1, "dmgen_c вернула NULL вместо ответа (внутренняя ошибка)")
|
|
49
|
+
try:
|
|
50
|
+
return json.loads(ctypes.string_at(ptr).decode("utf-8"))
|
|
51
|
+
finally:
|
|
52
|
+
_ffi.lib.dmgen_free(ptr)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _utf8(s: Optional[str]) -> Optional[bytes]:
|
|
56
|
+
return s.encode("utf-8") if s else None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _path(p: PathLike) -> bytes:
|
|
60
|
+
return os.fspath(p).encode("utf-8")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def configure(server_url: Optional[str] = None, app_version: Optional[str] = None,
|
|
64
|
+
timeout_ms: Optional[int] = None, auto_check: bool = True) -> None:
|
|
65
|
+
"""Настроить до первого сетевого вызова и первого кодирования. Можно не звать вовсе."""
|
|
66
|
+
_ffi.lib.dmgen_license_configure(_utf8(server_url), _utf8(app_version),
|
|
67
|
+
int(timeout_ms) if timeout_ms else 0, 1 if auto_check else 0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def status() -> Dict[str, Any]:
|
|
71
|
+
"""Состояние: state, can_encode, valid_until, days_left, message, …"""
|
|
72
|
+
return _take(_ffi.lib.dmgen_license_status_json())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def activate(key: str) -> Dict[str, Any]:
|
|
76
|
+
"""Онлайн-активация ключом."""
|
|
77
|
+
return _take(_ffi.lib.dmgen_license_activate_json(key.encode("utf-8")))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def refresh() -> Dict[str, Any]:
|
|
81
|
+
"""Сверить с сервером СЕЙЧАС, не дожидаясь суточной автосверки."""
|
|
82
|
+
return _take(_ffi.lib.dmgen_license_refresh_json())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def deactivate(reason: Optional[str] = None) -> Dict[str, Any]:
|
|
86
|
+
"""Отвязать машину по сети: расписка уходит на сервер."""
|
|
87
|
+
return _take(_ffi.lib.dmgen_license_deactivate_json(_utf8(reason)))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def deactivate_to_file(path: PathLike, reason: Optional[str] = None) -> Dict[str, Any]:
|
|
91
|
+
"""Отвязать без сети: лицензия удаляется сразу, расписка — в файл."""
|
|
92
|
+
return _take(_ffi.lib.dmgen_license_deactivate_to_file_json(_path(path), _utf8(reason)))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def write_activation_request(path: PathLike) -> Dict[str, Any]:
|
|
96
|
+
"""Файл запроса офлайн-активации — отпечаток этой машины."""
|
|
97
|
+
return _take(_ffi.lib.dmgen_license_write_activation_request_json(_path(path)))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def import_license_file(path: PathLike) -> Dict[str, Any]:
|
|
101
|
+
"""Принять license.lic. Годится и для продления."""
|
|
102
|
+
return _take(_ffi.lib.dmgen_license_import_license_file_json(_path(path)))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def fingerprint() -> str:
|
|
106
|
+
"""Сводный отпечаток этой машины, hex — для поддержки."""
|
|
107
|
+
return _take(_ffi.lib.dmgen_license_fingerprint_json())["fingerprint"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"product": "dmgen",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "enforced",
|
|
5
|
+
"insecure_test": false,
|
|
6
|
+
"license_server": "https://libscanner.com",
|
|
7
|
+
"license_product": "047e3e03-2e12-4d7f-95cf-16af8cf23302",
|
|
8
|
+
"license_key_prefix": "DMG",
|
|
9
|
+
"license_keys": "53942d61d057e01edf4042c8838cd692a979c05ed452be163c879979f41210d4",
|
|
10
|
+
"dev_env": false,
|
|
11
|
+
"buildtype": "release",
|
|
12
|
+
"system": "windows",
|
|
13
|
+
"cpu_family": "x86_64",
|
|
14
|
+
"date": "2026-10-04T07:59:07Z"
|
|
15
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: libscanner-dmgen
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Генератор DataMatrix ECC 200 с проверкой GS1 и «Честного знака» — DataMatrix generator with GS1 validation
|
|
5
|
+
Keywords: datamatrix,data-matrix,barcode,generator,gs1,ecc200,chestny-znak,marking,png
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Classifier: License :: Other/Proprietary License
|
|
8
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
12
|
+
Classifier: Topic :: Printing
|
|
13
|
+
Classifier: Natural Language :: Russian
|
|
14
|
+
Project-URL: Homepage, https://libscanner.com
|
|
15
|
+
Project-URL: Documentation, https://libscanner.com/docs/datamatrix-generator/?lang=python
|
|
16
|
+
Project-URL: Source, https://github.com/libscanner/datamatrix-generator
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# dmgen для Python
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pip install libscanner-dmgen
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Пакет импортируется как `dmgen`. Платформы: Windows x64, Linux x86-64, ARM64
|
|
27
|
+
и ARMv7 (glibc 2.36 и новее: Debian 12, Ubuntu 23.04+, Raspberry Pi OS
|
|
28
|
+
Bookworm).
|
|
29
|
+
|
|
30
|
+
> **English.** A DataMatrix ECC 200 generator for Python with GS1 and
|
|
31
|
+
> Chestny Znak (Russian product marking) validation: PNG/JPEG output, module
|
|
32
|
+
> matrix, readable error reports. Install with `pip install libscanner-dmgen`,
|
|
33
|
+
> import as `dmgen`. 30-day free trial, then a license from
|
|
34
|
+
> https://libscanner.com/en/.
|
|
35
|
+
|
|
36
|
+
Обёртка на `ctypes` над плоским C ABI библиотеки dmgen (`dmgen_c.dll` /
|
|
37
|
+
`libdmgen_c.so`). Компилируемого расширения нет: одно колесо на платформу
|
|
38
|
+
работает с любым Python 3.8+.
|
|
39
|
+
|
|
40
|
+
Колесо поставки (раздел «Загрузки» личного кабинета) несёт и собранную
|
|
41
|
+
утилиту-оболочку: после `pip install libscanner-dmgen` в окружении есть команда
|
|
42
|
+
`dmgen` (и `python -m dmgen`) — `dmgen "текст" -o код.png`,
|
|
43
|
+
`dmgen --license status`. Файл ключа `dmgen.key` — в `dmgen/_lib/`, общий для
|
|
44
|
+
утилиты и библиотеки.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import dmgen
|
|
48
|
+
|
|
49
|
+
png = dmgen.make_image("0104601234567893215Abc12Xyz!-\x1d93dGVz", gs1="cz")
|
|
50
|
+
dmgen.save("(01)04601234567893(21)5Abc12Xyz!-(93)dGVz", "code.png", gs1="cz", module_px=10)
|
|
51
|
+
jpg = dmgen.make_image("Hello", fmt="jpeg", quality=100, side=26)
|
|
52
|
+
|
|
53
|
+
rep = dmgen.validate_gs1("(01)04601234567894(21)ABC")
|
|
54
|
+
rep.ok # False
|
|
55
|
+
rep.issues[0] # Issue(code=…, warning=False, message='AI (01) GTIN: контрольная цифра 4, должна быть 3', …)
|
|
56
|
+
|
|
57
|
+
dmgen.encode_matrix("ABC") # [[1, 0, 1, …], …] — 1 тёмный модуль
|
|
58
|
+
dmgen.SIZES # (10, 12, 14, …, 144)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Строка `str` кодируется в байты UTF-8; `bytes` передаются как есть. Ошибки —
|
|
62
|
+
исключения `dmgen.Gs1Error` (с отчётом `.report`), `dmgen.EncodeError`,
|
|
63
|
+
`dmgen.UnlicensedError` (нет ни пробного периода, ни лицензии),
|
|
64
|
+
`dmgen.DmgenError` (общий предок).
|
|
65
|
+
|
|
66
|
+
Лицензия — `dmgen.license`: `status()`, `activate(key)`, `refresh()`,
|
|
67
|
+
`deactivate()`, `write_activation_request(path)`, `import_license_file(path)`,
|
|
68
|
+
`fingerprint()`; ответы — dict из того же JSON, что у `dmgen --license ... --json`.
|
|
69
|
+
Проще всего положить файл `dmgen.key` с ключом рядом с `dmgen_c.dll` /
|
|
70
|
+
`libdmgen_c.so` в пакете (`dmgen/_lib/`): библиотека активирует его сама.
|
|
71
|
+
|
|
72
|
+
Библиотека грузится только из `dmgen/_lib/` пакета. Переменная `DMGEN_LIB`
|
|
73
|
+
(абсолютный путь) — для пакета из исходников, где `_lib` пуст, и колесо не
|
|
74
|
+
переопределяет; системный поиск (PATH, каталоги библиотек) не используется.
|
|
75
|
+
|
|
76
|
+
## Документация и лицензия
|
|
77
|
+
|
|
78
|
+
Документация: https://libscanner.com/docs/datamatrix-generator/?lang=python
|
|
79
|
+
|
|
80
|
+
Пробный период — 30 дней с первого запуска на машине, без регистрации и без
|
|
81
|
+
сети. Без интернета: личный кабинет https://libscanner.com → Лицензии → «Активация без
|
|
82
|
+
интернета».
|