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 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
@@ -0,0 +1,7 @@
1
+ """``python -m dmgen ...`` — то же, что команда ``dmgen`` (см. _cli.py)."""
2
+
3
+ import sys
4
+
5
+ from ._cli import main
6
+
7
+ sys.exit(main())
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
+ интернета».