libscanner-dmr 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.
- dmr/__init__.py +68 -0
- dmr/__main__.py +7 -0
- dmr/_cli.py +50 -0
- dmr/_ffi.py +261 -0
- dmr/_lib/dmr.exe +0 -0
- dmr/_lib/dmr_c.dll +0 -0
- dmr/diagnostics.py +49 -0
- dmr/image.py +227 -0
- dmr/license.py +97 -0
- dmr/paths.py +32 -0
- dmr/scanner.py +167 -0
- dmr/settings.py +137 -0
- libscanner_dmr-1.0.0.dist-info/BUILD-INFO.json +15 -0
- libscanner_dmr-1.0.0.dist-info/METADATA +105 -0
- libscanner_dmr-1.0.0.dist-info/RECORD +31 -0
- libscanner_dmr-1.0.0.dist-info/WHEEL +4 -0
- libscanner_dmr-1.0.0.dist-info/entry_points.txt +2 -0
- libscanner_dmr-1.0.0.dist-info/licenses/GCC-COPYING.RUNTIME.txt +73 -0
- libscanner_dmr-1.0.0.dist-info/licenses/GCC-COPYING3.txt +674 -0
- libscanner_dmr-1.0.0.dist-info/licenses/LICENSE.en.txt +112 -0
- libscanner_dmr-1.0.0.dist-info/licenses/LICENSE.txt +103 -0
- libscanner_dmr-1.0.0.dist-info/licenses/MinGW-w64-runtime-COPYING.txt +265 -0
- libscanner_dmr-1.0.0.dist-info/licenses/OpenCV-COPYRIGHT.txt +62 -0
- libscanner_dmr-1.0.0.dist-info/licenses/OpenCV-LICENSE.txt +202 -0
- libscanner_dmr-1.0.0.dist-info/licenses/THIRD-PARTY-NOTICES.txt +43 -0
- libscanner_dmr-1.0.0.dist-info/licenses/libjpeg-turbo-LICENSE.md +135 -0
- libscanner_dmr-1.0.0.dist-info/licenses/libpng-LICENSE.txt +134 -0
- libscanner_dmr-1.0.0.dist-info/licenses/monocypher-LICENCE.md +167 -0
- libscanner_dmr-1.0.0.dist-info/licenses/stb-LICENSE.txt +43 -0
- libscanner_dmr-1.0.0.dist-info/licenses/winpthreads-COPYING.txt +57 -0
- libscanner_dmr-1.0.0.dist-info/licenses/zlib-LICENSE.txt +22 -0
dmr/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""dmr — чтение кодов DataMatrix (ECC200, ГОСТ Р ИСО/МЭК 16022) из Python.
|
|
2
|
+
|
|
3
|
+
Обёртка на ctypes поверх плоского C ABI библиотеки (dmr_c, dmr/dmr_c.h,
|
|
4
|
+
FLAT_C_ABI_PLAN.md). Требует собранной и доступной библиотеки dmr_c
|
|
5
|
+
(dmr_c.dll / libdmr_c.so) — см. README.md рядом с этим пакетом.
|
|
6
|
+
|
|
7
|
+
Быстрый старт::
|
|
8
|
+
|
|
9
|
+
import dmr
|
|
10
|
+
|
|
11
|
+
img = dmr.Image("кадр.jpg", dmr.LoadAs.GRAY)
|
|
12
|
+
scanner = dmr.Scanner() # поток кадров, умолчание
|
|
13
|
+
result = scanner.scan(img.view())
|
|
14
|
+
if result.ok:
|
|
15
|
+
print(result.text.decode("utf-8"))
|
|
16
|
+
|
|
17
|
+
Кадр из чужого буфера — без копирования, видом на него (буфер обязан быть
|
|
18
|
+
жив на момент scan())::
|
|
19
|
+
|
|
20
|
+
view = dmr.ImageView(buf, width, height, stride, dmr.PixelFormat.BGR8)
|
|
21
|
+
result = scanner.scan(view)
|
|
22
|
+
|
|
23
|
+
Лицензия и диагностика — отдельными подмодулями, ``dmr.license`` и
|
|
24
|
+
``dmr.diagnostics``, тем же приёмом, что namespace ``dmr::license`` в C++ API.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from . import diagnostics, license # noqa: F401 (публичные подмодули)
|
|
28
|
+
from ._ffi import DmrError
|
|
29
|
+
from .image import Image, ImageView, LoadAs, LoadStatus, PixelFormat, channels_of
|
|
30
|
+
from .scanner import Box, Code, Point, ScanResult, Scanner, Status, scan
|
|
31
|
+
from .settings import Settings
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"DmrError",
|
|
35
|
+
"Image",
|
|
36
|
+
"ImageView",
|
|
37
|
+
"LoadAs",
|
|
38
|
+
"LoadStatus",
|
|
39
|
+
"PixelFormat",
|
|
40
|
+
"channels_of",
|
|
41
|
+
"Box",
|
|
42
|
+
"Code",
|
|
43
|
+
"Point",
|
|
44
|
+
"ScanResult",
|
|
45
|
+
"Scanner",
|
|
46
|
+
"Status",
|
|
47
|
+
"scan",
|
|
48
|
+
"Settings",
|
|
49
|
+
"license",
|
|
50
|
+
"diagnostics",
|
|
51
|
+
"version",
|
|
52
|
+
"abi_version",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def version() -> str:
|
|
57
|
+
"""Версия СОБРАННОЙ библиотеки dmr_c — как dmr::version() в C++ API."""
|
|
58
|
+
from . import _ffi
|
|
59
|
+
|
|
60
|
+
return _ffi.copy_version(_ffi.lib.dmr_version)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def abi_version() -> str:
|
|
64
|
+
"""Версия самой границы C ABI (DMR_C_ABI_VERSION_MAJOR.MINOR) — отдельно
|
|
65
|
+
от версии алгоритма (см. version()); FLAT_C_ABI_PLAN.md, §5."""
|
|
66
|
+
from . import _ffi
|
|
67
|
+
|
|
68
|
+
return _ffi.copy_version(_ffi.lib.dmr_c_abi_version)
|
dmr/__main__.py
ADDED
dmr/_cli.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Команда ``dmr`` и ``python -m dmr`` — собранная утилита-оболочка из колеса.
|
|
2
|
+
|
|
3
|
+
Колесо поставки (tools/build_wheel.py) кладёт утилиту ``dmr`` (``dmr.exe`` на
|
|
4
|
+
Windows, без DLL MinGW рядом) в ``dmr/_lib/`` — рядом с библиотекой dmr_c, — а
|
|
5
|
+
``pip install`` делает из этой функции команду ``dmr`` в PATH окружения. Всё,
|
|
6
|
+
что умеет утилита, — как у неё: ``dmr снимок.png``, ``dmr --license status``,
|
|
7
|
+
справка — ``dmr --help``; аргументы и код возврата передаются как есть.
|
|
8
|
+
|
|
9
|
+
Файл ключа ``license.key`` для неё и для библиотеки — один, в ``dmr/_lib/``:
|
|
10
|
+
утилита ищет его рядом с собой, библиотека — рядом с dmr_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 = "dmr.exe" if sys.platform == "win32" else "dmr"
|
|
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"утилиты dmr нет в пакете ({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())
|
dmr/_ffi.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Сырая граница ctypes поверх dmr_c (include/dmr/dmr_c.h).
|
|
2
|
+
|
|
3
|
+
Не публичный модуль пакета — сюда не ходят снаружи. Здесь только два дела:
|
|
4
|
+
найти и загрузить dmr_c.dll/libdmr_c.so и объявить сигнатуры (argtypes/
|
|
5
|
+
restype) для каждой функции границы, ничего сверх. Пайтонический слой —
|
|
6
|
+
в остальных модулях пакета (settings.py, image.py, scanner.py, license.py).
|
|
7
|
+
|
|
8
|
+
ВАЖНОЕ О ТИПАХ, а не только формальность:
|
|
9
|
+
|
|
10
|
+
- Строки, которые библиотека ВОЗВРАЩАЕТ через dmr_free_string (лицензия),
|
|
11
|
+
объявлены restype=c_void_p, а не c_char_p. Если объявить их c_char_p,
|
|
12
|
+
ctypes сам превратит указатель в bytes ПРИ ВЫЗОВЕ и вернёт копию —
|
|
13
|
+
а исходный указатель, без которого dmr_free_string нечего освобождать,
|
|
14
|
+
потеряется навсегда. Освобождать нечем — течёт при каждом вызове.
|
|
15
|
+
- dmr_scan_result_code_text — та же ловушка и вдобавок опасная: c_char_p
|
|
16
|
+
читает строку до первого нулевого байта, а GS1-код может НЕ иметь такого
|
|
17
|
+
байта там, где заканчивается настоящий текст (0x1D — GS, не 0x00), то
|
|
18
|
+
есть строка легко читалась бы длиннее или короче настоящей. Отсюда —
|
|
19
|
+
restype=c_void_p и обязательное чтение через dmr_scan_result_code_text_len,
|
|
20
|
+
а не через сам указатель.
|
|
21
|
+
- Пути (dmr_load_image и подобные) — bytes, не str: ctypes НЕ кодирует str
|
|
22
|
+
в bytes для c_char_p сам, и это то самое свойство, которое нужно здесь —
|
|
23
|
+
кодировка пути системная, не UTF-8 (см. paths.py), и неявное авто-
|
|
24
|
+
кодирование ctypes её бы просто подменило.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import ctypes
|
|
30
|
+
import os
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DmrError(RuntimeError):
|
|
36
|
+
"""Внутренняя ошибка самой границы: перехваченное на стороне C++
|
|
37
|
+
исключение (NULL/DMR_STATUS_INTERNAL_ERROR — FLAT_C_ABI_PLAN.md, §4).
|
|
38
|
+
|
|
39
|
+
Не путать с обычным отказом операции (например, неверный лицензионный
|
|
40
|
+
ключ) — тот приходит как {"ok": false, "error": ...} в JSON и исключения
|
|
41
|
+
не поднимает: это ожидаемый исход, а не поломка границы.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
DMR_MAX_WARP_SCALES = 16
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DmrImageView(ctypes.Structure):
|
|
49
|
+
_fields_ = [
|
|
50
|
+
("data", ctypes.c_void_p),
|
|
51
|
+
("width", ctypes.c_int),
|
|
52
|
+
("height", ctypes.c_int),
|
|
53
|
+
("stride", ctypes.c_int),
|
|
54
|
+
("format", ctypes.c_int),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DmrSettings(ctypes.Structure):
|
|
59
|
+
# ABI 2.0: первым полем — struct_size, sizeof этой структуры у нас
|
|
60
|
+
# (dmr_c.h). Выставлять его до любого вызова, принимающего настройки, —
|
|
61
|
+
# settings.py делает это сам (new_settings_struct).
|
|
62
|
+
_fields_ = [
|
|
63
|
+
("struct_size", ctypes.c_uint32),
|
|
64
|
+
("max_codes", ctypes.c_int),
|
|
65
|
+
("require_gs1", ctypes.c_int),
|
|
66
|
+
("forced_rotation", ctypes.c_int),
|
|
67
|
+
("budget_ms", ctypes.c_double),
|
|
68
|
+
("row_parallel", ctypes.c_int),
|
|
69
|
+
("max_size_tries", ctypes.c_int),
|
|
70
|
+
("warp_scales", ctypes.c_int * DMR_MAX_WARP_SCALES),
|
|
71
|
+
("warp_scales_count", ctypes.c_int),
|
|
72
|
+
("max_candidates", ctypes.c_int),
|
|
73
|
+
("reader_cache_mb", ctypes.c_double),
|
|
74
|
+
("mc_hint_run", ctypes.c_int),
|
|
75
|
+
("max_pattern_error", ctypes.c_double),
|
|
76
|
+
("lpat_retry_scale", ctypes.c_double),
|
|
77
|
+
("direct_sampling", ctypes.c_int),
|
|
78
|
+
("use_lpattern_finder", ctypes.c_int),
|
|
79
|
+
("use_texture_finder", ctypes.c_int),
|
|
80
|
+
("collect_details", ctypes.c_int),
|
|
81
|
+
# ABI 1.1 — предел размера кадра (dmr::Settings::imageLimits).
|
|
82
|
+
("max_image_side", ctypes.c_int),
|
|
83
|
+
("max_image_pixels", ctypes.c_int64),
|
|
84
|
+
# ABI 2.0 — предохранитель (dmr::Settings::timeoutMs).
|
|
85
|
+
("timeout_ms", ctypes.c_double),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def new_settings_struct(**fields) -> DmrSettings:
|
|
90
|
+
"""DmrSettings с выставленным struct_size — иначе библиотека отвергает
|
|
91
|
+
структуру (пресет вернёт 0, dmr_scanner_create — NULL)."""
|
|
92
|
+
return DmrSettings(struct_size=ctypes.sizeof(DmrSettings), **fields)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# void (*)(const char* line, size_t len, void* user) — line НЕ гарантированно
|
|
96
|
+
# завершена нулём как C-строка "до len", поэтому тип параметра — c_void_p, а
|
|
97
|
+
# не c_char_p: тем, кто ставит приёмник (log_sink.py), передаётся сырой
|
|
98
|
+
# указатель, а не преждевременно и неверно декодированная строка.
|
|
99
|
+
DmrLogSinkFunc = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _candidate_names() -> list[str]:
|
|
103
|
+
if sys.platform == "win32":
|
|
104
|
+
return ["dmr_c.dll"]
|
|
105
|
+
if sys.platform == "darwin":
|
|
106
|
+
return ["libdmr_c.dylib"]
|
|
107
|
+
return ["libdmr_c.so"]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _load_library() -> ctypes.CDLL:
|
|
111
|
+
"""Библиотека грузится по ПОЛНОМУ пути, а не по имени.
|
|
112
|
+
|
|
113
|
+
Поиск по имени (PATH, текущий каталог, пути линковщика) отдаёт выбор
|
|
114
|
+
файла окружению процесса: подложенная в такой каталог dmr_c.dll
|
|
115
|
+
исполнялась бы с правами приложения (аудит 2026-10-01, P-17). Поэтому:
|
|
116
|
+
|
|
117
|
+
1. dmr/_lib/ внутри пакета — так её кладёт колесо поставки
|
|
118
|
+
(tools/build_wheel.py). Если она там есть, берётся ТОЛЬКО она:
|
|
119
|
+
переменная окружения установленный пакет не перенаправляет;
|
|
120
|
+
2. файл рядом с модулями пакета (dmr/dmr_c.dll) — положили вручную;
|
|
121
|
+
3. DMR_C_LIBRARY — полный путь к файлу; для разработки (пакет из
|
|
122
|
+
исходников, где _lib нет) и своей сборки. Относительный путь не
|
|
123
|
+
принимается: он зависел бы от текущего каталога.
|
|
124
|
+
|
|
125
|
+
Системного поиска нет.
|
|
126
|
+
"""
|
|
127
|
+
names = _candidate_names()
|
|
128
|
+
here = Path(__file__).resolve().parent
|
|
129
|
+
for d in (here / "_lib", here):
|
|
130
|
+
for name in names:
|
|
131
|
+
p = d / name
|
|
132
|
+
if p.is_file():
|
|
133
|
+
return ctypes.CDLL(str(p))
|
|
134
|
+
|
|
135
|
+
env = os.environ.get("DMR_C_LIBRARY")
|
|
136
|
+
if env:
|
|
137
|
+
p = Path(env)
|
|
138
|
+
if not p.is_absolute():
|
|
139
|
+
raise OSError(f"DMR_C_LIBRARY должен быть полным путём к файлу, получено: {env!r}")
|
|
140
|
+
return ctypes.CDLL(str(p))
|
|
141
|
+
|
|
142
|
+
raise OSError(
|
|
143
|
+
"не найдена библиотека dmr_c (искали: " + ", ".join(names) + " в "
|
|
144
|
+
+ str(here / "_lib") + " и " + str(here) + "). "
|
|
145
|
+
"Поставьте колесо из поставки (библиотека внутри него) или соберите: "
|
|
146
|
+
"meson setup build -Dcabi=true && meson compile -C build, и укажите "
|
|
147
|
+
"полный путь переменной DMR_C_LIBRARY."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
lib = _load_library()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _proto(name: str, argtypes: list, restype) -> None:
|
|
155
|
+
fn = getattr(lib, name)
|
|
156
|
+
fn.argtypes = argtypes
|
|
157
|
+
fn.restype = restype
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
_c_void_p = ctypes.c_void_p
|
|
161
|
+
_c_char_p = ctypes.c_char_p
|
|
162
|
+
_c_int = ctypes.c_int
|
|
163
|
+
_c_size_t = ctypes.c_size_t
|
|
164
|
+
_c_double = ctypes.c_double
|
|
165
|
+
_c_float = ctypes.c_float
|
|
166
|
+
_p_settings = ctypes.POINTER(DmrSettings)
|
|
167
|
+
_p_view = ctypes.POINTER(DmrImageView)
|
|
168
|
+
_p_int = ctypes.POINTER(_c_int)
|
|
169
|
+
_p_float = ctypes.POINTER(_c_float)
|
|
170
|
+
_p_voidp = ctypes.POINTER(_c_void_p)
|
|
171
|
+
|
|
172
|
+
_proto("dmr_c_abi_version", [_c_char_p, _c_size_t], _c_size_t)
|
|
173
|
+
|
|
174
|
+
# Структуры выше описаны по заголовку ABI 2.x. Библиотека другой старшей
|
|
175
|
+
# версии раскладывает их иначе (1.x — без struct_size): работать с ней значило
|
|
176
|
+
# бы читать и писать поля вкривь, поэтому — отказ при загрузке, а не порча
|
|
177
|
+
# памяти при первом вызове.
|
|
178
|
+
ABI_MAJOR = 2
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _check_abi() -> None:
|
|
182
|
+
buf = ctypes.create_string_buffer(32)
|
|
183
|
+
lib.dmr_c_abi_version(buf, len(buf))
|
|
184
|
+
got = buf.value.decode("ascii", errors="replace")
|
|
185
|
+
major = got.split(".", 1)[0]
|
|
186
|
+
if major != str(ABI_MAJOR):
|
|
187
|
+
raise OSError(
|
|
188
|
+
f"библиотека dmr_c с ABI {got!r}, а пакет dmr написан для ABI {ABI_MAJOR}.x: "
|
|
189
|
+
"поставьте библиотеку и пакет одной поставки")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
_check_abi()
|
|
193
|
+
|
|
194
|
+
_proto("dmr_version", [_c_char_p, _c_size_t], _c_size_t)
|
|
195
|
+
|
|
196
|
+
_proto("dmr_load_image", [_c_char_p, _c_int, _p_voidp], _c_int)
|
|
197
|
+
_proto("dmr_load_image_ex", [_c_char_p, _c_int, _p_settings, _p_voidp], _c_int)
|
|
198
|
+
_proto("dmr_image_view", [_c_void_p], DmrImageView)
|
|
199
|
+
_proto("dmr_image_free", [_c_void_p], None)
|
|
200
|
+
|
|
201
|
+
_proto("dmr_settings_stream", [_p_settings], _c_int)
|
|
202
|
+
_proto("dmr_settings_single", [_p_settings], _c_int)
|
|
203
|
+
_proto("dmr_settings_multi", [_p_settings, _c_int], _c_int)
|
|
204
|
+
|
|
205
|
+
_proto("dmr_scanner_create", [_p_settings], _c_void_p)
|
|
206
|
+
_proto("dmr_scanner_destroy", [_c_void_p], None)
|
|
207
|
+
_proto("dmr_scanner_module_count_hint", [_c_void_p], _c_int)
|
|
208
|
+
_proto("dmr_scanner_reset_stream", [_c_void_p], None)
|
|
209
|
+
|
|
210
|
+
_proto("dmr_scanner_scan", [_c_void_p, _p_view, _c_int], _c_void_p)
|
|
211
|
+
_proto("dmr_scan_result_free", [_c_void_p], None)
|
|
212
|
+
_proto("dmr_scan_result_status", [_c_void_p], _c_int)
|
|
213
|
+
_proto("dmr_scan_result_timed_out", [_c_void_p], _c_int)
|
|
214
|
+
_proto("dmr_scan_result_elapsed_ms", [_c_void_p], _c_double)
|
|
215
|
+
_proto("dmr_scan_result_code_count", [_c_void_p], _c_int)
|
|
216
|
+
_proto("dmr_scan_result_code_text", [_c_void_p, _c_int], _c_void_p)
|
|
217
|
+
_proto("dmr_scan_result_code_text_len", [_c_void_p, _c_int], _c_size_t)
|
|
218
|
+
_proto("dmr_scan_result_code_box", [_c_void_p, _c_int, _p_int, _p_int, _p_int, _p_int], None)
|
|
219
|
+
_proto("dmr_scan_result_code_corners", [_c_void_p, _c_int, _p_float], None)
|
|
220
|
+
_proto("dmr_scan_result_code_module_count", [_c_void_p, _c_int], _c_int)
|
|
221
|
+
|
|
222
|
+
_proto("dmr_set_log_sink", [DmrLogSinkFunc, _c_void_p], None)
|
|
223
|
+
_proto("dmr_set_verbose", [_c_int], None)
|
|
224
|
+
_proto("dmr_is_verbose", [], _c_int)
|
|
225
|
+
|
|
226
|
+
_proto("dmr_license_configure", [_c_char_p, _c_char_p, _c_int, _c_int], None)
|
|
227
|
+
_proto("dmr_license_status_json", [], _c_void_p)
|
|
228
|
+
_proto("dmr_license_activate_json", [_c_char_p], _c_void_p)
|
|
229
|
+
_proto("dmr_license_refresh_json", [], _c_void_p)
|
|
230
|
+
_proto("dmr_license_deactivate_json", [_c_char_p], _c_void_p)
|
|
231
|
+
_proto("dmr_license_deactivate_to_file_json", [_c_char_p, _c_char_p], _c_void_p)
|
|
232
|
+
_proto("dmr_license_write_activation_request_json", [_c_char_p], _c_void_p)
|
|
233
|
+
_proto("dmr_license_import_license_file_json", [_c_char_p], _c_void_p)
|
|
234
|
+
_proto("dmr_license_fingerprint_json", [], _c_void_p)
|
|
235
|
+
_proto("dmr_free_string", [_c_void_p], None)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def take_string(ptr: int | None) -> str:
|
|
239
|
+
"""Забрать строку, отданную библиотекой через dmr_free_string
|
|
240
|
+
(лицензия — там любой непустой результат такой), и освободить её.
|
|
241
|
+
NULL — DmrError: это внутренняя ошибка границы, не обычный отказ
|
|
242
|
+
операции (тот приходит валидным JSON с "ok": false)."""
|
|
243
|
+
if not ptr:
|
|
244
|
+
raise DmrError("граница вернула NULL вместо строки (внутренняя ошибка)")
|
|
245
|
+
try:
|
|
246
|
+
return ctypes.string_at(ptr).decode("utf-8")
|
|
247
|
+
finally:
|
|
248
|
+
lib.dmr_free_string(ptr)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def copy_version(cfunc) -> str:
|
|
252
|
+
"""snprintf-контракт dmr_version/dmr_c_abi_version: буфер угадан заведомо
|
|
253
|
+
большим, но если когда-нибудь не хватит — перевызов размером ровно в
|
|
254
|
+
то, что попросила сама библиотека (её же возврат), а не догадка."""
|
|
255
|
+
size = 64
|
|
256
|
+
while True:
|
|
257
|
+
buf = ctypes.create_string_buffer(size)
|
|
258
|
+
need = cfunc(buf, size)
|
|
259
|
+
if need < size:
|
|
260
|
+
return buf.value.decode("utf-8", errors="replace")
|
|
261
|
+
size = need + 1
|
dmr/_lib/dmr.exe
ADDED
|
Binary file
|
dmr/_lib/dmr_c.dll
ADDED
|
Binary file
|
dmr/diagnostics.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Поток отладочных сообщений — dmr_set_log_sink/dmr_set_verbose/dmr_is_verbose
|
|
2
|
+
(dmr/dmr_c.h), соответствует dmr/Diagnostics.h в C++ API.
|
|
3
|
+
|
|
4
|
+
По умолчанию приёмника нет, и это не "есть, но молчит": без него строки не
|
|
5
|
+
собираются даже внутри библиотеки (isVerbose() ложно) — писать в чужой
|
|
6
|
+
поток библиотека не вправе, она не знает, кто его читает.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ctypes
|
|
12
|
+
from typing import Callable, Optional
|
|
13
|
+
|
|
14
|
+
from . import _ffi
|
|
15
|
+
|
|
16
|
+
# CFUNCTYPE-обёртка должна жить не короче, чем она зарегистрирована в
|
|
17
|
+
# библиотеке — ctypes не держит на неё ссылку сам, и без этой переменной
|
|
18
|
+
# сборщик мусора Python освободил бы её первым, а библиотека звала бы уже
|
|
19
|
+
# висячий указатель.
|
|
20
|
+
_current_callback: Optional[ctypes.CFUNCTYPE] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def set_log_sink(sink: Optional[Callable[[bytes], None]]) -> None:
|
|
24
|
+
"""Поставить приёмник строк лога; None — выключить.
|
|
25
|
+
|
|
26
|
+
sink получает СЫРЫЕ байты одной строки (с переводом строки на конце) —
|
|
27
|
+
не str: сообщения из библиотеки в UTF-8, но решать, как их декодировать
|
|
28
|
+
(или не декодировать вовсе, если просто пишете в файл), — дело
|
|
29
|
+
вызывающего.
|
|
30
|
+
"""
|
|
31
|
+
global _current_callback
|
|
32
|
+
if sink is None:
|
|
33
|
+
_ffi.lib.dmr_set_log_sink(None, None)
|
|
34
|
+
_current_callback = None
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
def _trampoline(line_ptr, length, _user):
|
|
38
|
+
sink(ctypes.string_at(line_ptr, length))
|
|
39
|
+
|
|
40
|
+
_current_callback = _ffi.DmrLogSinkFunc(_trampoline)
|
|
41
|
+
_ffi.lib.dmr_set_log_sink(_current_callback, None)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def set_verbose(on: bool) -> None:
|
|
45
|
+
_ffi.lib.dmr_set_verbose(int(bool(on)))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_verbose() -> bool:
|
|
49
|
+
return bool(_ffi.lib.dmr_is_verbose())
|
dmr/image.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Кадр: вид на чужой буфер (ImageView) и владеющий кадр, прочитанный с диска
|
|
2
|
+
(Image). Соответствует dmr/Image.h в C++ API и §2.1/§2.6 FLAT_C_ABI_PLAN.md.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import ctypes
|
|
8
|
+
import enum
|
|
9
|
+
import weakref
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, Union
|
|
12
|
+
|
|
13
|
+
from . import _ffi
|
|
14
|
+
from .paths import encode_path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PixelFormat(enum.IntEnum):
|
|
18
|
+
"""Укладка байт пикселя. Сканер работает с яркостью и переводит в неё
|
|
19
|
+
цветной кадр сам — если камера монохромная, отдавайте GRAY8, а не
|
|
20
|
+
размноженный в три канала полутон: ответ тот же, но дороже."""
|
|
21
|
+
|
|
22
|
+
GRAY8 = 0
|
|
23
|
+
BGR8 = 1
|
|
24
|
+
RGB8 = 2
|
|
25
|
+
BGRA8 = 3
|
|
26
|
+
RGBA8 = 4
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_CHANNELS = {
|
|
30
|
+
PixelFormat.GRAY8: 1,
|
|
31
|
+
PixelFormat.BGR8: 3,
|
|
32
|
+
PixelFormat.RGB8: 3,
|
|
33
|
+
PixelFormat.BGRA8: 4,
|
|
34
|
+
PixelFormat.RGBA8: 4,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LoadAs(enum.IntEnum):
|
|
39
|
+
COLOR = 0
|
|
40
|
+
GRAY = 1
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LoadStatus(enum.IntEnum):
|
|
44
|
+
OK = 0
|
|
45
|
+
NOT_FOUND = 1
|
|
46
|
+
UNREADABLE = 2
|
|
47
|
+
TOO_LARGE = 3
|
|
48
|
+
TIMED_OUT = 4 # декодирование не уложилось в срок (budget_ms/timeout_ms)
|
|
49
|
+
INVALID_ARGUMENT = 5 # настройки отвергнуты границей (struct_size)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def channels_of(fmt: PixelFormat) -> int:
|
|
53
|
+
return _CHANNELS[PixelFormat(fmt)]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _required_bytes(width: int, height: int, stride: int, fmt: PixelFormat) -> int:
|
|
57
|
+
"""Сколько байт прочитает сканер: (height-1) строк по stride и последняя
|
|
58
|
+
строка по width*каналов. Ровно столько, не больше, — последняя строка
|
|
59
|
+
вырезки из большего кадра не обязана иметь хвост до stride."""
|
|
60
|
+
if width <= 0 or height <= 0:
|
|
61
|
+
return 0
|
|
62
|
+
row = width * channels_of(fmt)
|
|
63
|
+
step = stride if stride > 0 else row
|
|
64
|
+
return (height - 1) * step + row
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_INT_MAX = 2**31 - 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _address_of(buf) -> tuple[int, object]:
|
|
71
|
+
"""Адрес начала буфера без копирования — для писабельных буферов
|
|
72
|
+
(bytearray, numpy-массив, mmap и подобное через buffer-протокол).
|
|
73
|
+
Для НЕПИСАБЕЛЬНЫХ буферов (bytes, read-only memoryview) ctypes не
|
|
74
|
+
может получить писабельный вид без копии — тогда копия делается ОДИН
|
|
75
|
+
раз здесь, явно, а не молча где-то внутри библиотеки. Возвращает и
|
|
76
|
+
объект, который нужно живым держать не короче самого ImageView — тот
|
|
77
|
+
же буфер, что описан в dmr::ImageView (C++ API): библиотека кадр не
|
|
78
|
+
копирует и не переживает, поэтому переживать его обязан вызывающий.
|
|
79
|
+
|
|
80
|
+
Держится сам ctypes-массив, а не буфер (аудит 2026-10-02, R-13):
|
|
81
|
+
from_buffer держит ЭКСПОРТ буфера, и пока он жив, bytearray нельзя
|
|
82
|
+
растянуть или укоротить (BufferError), а mmap — закрыть. Ссылка на сам
|
|
83
|
+
bytearray от этого не спасала: после ba.extend(...) данные переезжали,
|
|
84
|
+
адрес в виде указывал в освобождённую память, и scan() читал её."""
|
|
85
|
+
# Длина — в БАЙТАХ: len() у numpy-массива 480x640 — 480, а не 307200.
|
|
86
|
+
nbytes = memoryview(buf).nbytes
|
|
87
|
+
if nbytes == 0:
|
|
88
|
+
return 0, buf
|
|
89
|
+
try:
|
|
90
|
+
arr = (ctypes.c_uint8 * nbytes).from_buffer(buf)
|
|
91
|
+
return ctypes.addressof(arr), arr
|
|
92
|
+
except TypeError:
|
|
93
|
+
copy = bytearray(memoryview(buf).tobytes())
|
|
94
|
+
arr = (ctypes.c_uint8 * len(copy)).from_buffer(copy)
|
|
95
|
+
return ctypes.addressof(arr), arr
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ImageView:
|
|
99
|
+
"""Вид на ЧУЖОЙ буфер — как dmr::ImageView в C++ API. Буфер, на который
|
|
100
|
+
смотрит view, обязан быть жив всё время вызова Scanner.scan(); сам
|
|
101
|
+
ImageView держит на него ссылку ради этого же (keepalive), но не копирует
|
|
102
|
+
его данные."""
|
|
103
|
+
|
|
104
|
+
__slots__ = ("_struct", "_keepalive", "__weakref__")
|
|
105
|
+
|
|
106
|
+
def __init__(self, buf, width: int, height: int, stride: int, format: PixelFormat):
|
|
107
|
+
# Проверка ДО того, как адрес уйдёт в библиотеку: сканер читает
|
|
108
|
+
# (height-1)*stride + width*каналов байт, и буфер короче — чтение за
|
|
109
|
+
# его концом (аудит 2026-10-01, P-12). Размеры — в пределах int поля
|
|
110
|
+
# C-структуры: ctypes молча обрезал бы старшие разряды.
|
|
111
|
+
fmt = PixelFormat(format)
|
|
112
|
+
width, height, stride = int(width), int(height), int(stride)
|
|
113
|
+
for name, v in (("width", width), ("height", height), ("stride", stride)):
|
|
114
|
+
if not 0 <= v <= _INT_MAX:
|
|
115
|
+
raise ValueError(f"{name}: {v} вне диапазона [0, {_INT_MAX}]")
|
|
116
|
+
if stride and stride < width * channels_of(fmt):
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"stride {stride} меньше width*каналов = {width * channels_of(fmt)}")
|
|
119
|
+
need = _required_bytes(width, height, stride, fmt)
|
|
120
|
+
have = memoryview(buf).nbytes
|
|
121
|
+
if have < need:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"буфер {have} байт короче кадра {width}x{height}, stride {stride}, "
|
|
124
|
+
f"{fmt.name}: нужно не меньше {need}")
|
|
125
|
+
address, keepalive = _address_of(buf)
|
|
126
|
+
self._keepalive = keepalive
|
|
127
|
+
self._struct = _ffi.DmrImageView(
|
|
128
|
+
ctypes.c_void_p(address), width, height, stride, int(fmt)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def _from_struct(cls, struct: "_ffi.DmrImageView", keepalive: object) -> "ImageView":
|
|
133
|
+
# Для Image.view(): структура уже готова (dmr_image_view() отдал её
|
|
134
|
+
# по значению), пересобирать её из адреса заново незачем.
|
|
135
|
+
self = cls.__new__(cls)
|
|
136
|
+
self._struct = struct
|
|
137
|
+
self._keepalive = keepalive
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def width(self) -> int:
|
|
142
|
+
return self._struct.width
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def height(self) -> int:
|
|
146
|
+
return self._struct.height
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def stride(self) -> int:
|
|
150
|
+
return self._struct.stride
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def format(self) -> PixelFormat:
|
|
154
|
+
return PixelFormat(self._struct.format)
|
|
155
|
+
|
|
156
|
+
def __repr__(self) -> str:
|
|
157
|
+
return (
|
|
158
|
+
f"ImageView(width={self.width}, height={self.height}, "
|
|
159
|
+
f"stride={self.stride}, format={self.format.name})"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Image:
|
|
164
|
+
"""Кадр, прочитанный с диска, — библиотека владеет буфером сама
|
|
165
|
+
(dmr_image_t). Освобождается при close()/выходе из `with`/сборке мусора;
|
|
166
|
+
явный close() надёжнее — __del__ не гарантирован по времени."""
|
|
167
|
+
|
|
168
|
+
def __init__(self, path: Union[str, "Path", bytes], as_: LoadAs = LoadAs.GRAY,
|
|
169
|
+
settings=None):
|
|
170
|
+
"""settings — dmr.Settings: берётся предел размера кадра
|
|
171
|
+
(max_image_side, max_image_pixels) и срок (budget_ms, timeout_ms).
|
|
172
|
+
None — умолчания библиотеки (в том числе timeout_ms 10 с). Кадр
|
|
173
|
+
больше предела не декодируется: status == LoadStatus.TOO_LARGE;
|
|
174
|
+
декодирование дольше срока бросается: LoadStatus.TIMED_OUT."""
|
|
175
|
+
out = ctypes.c_void_p()
|
|
176
|
+
c_settings = settings.to_struct() if settings is not None else None
|
|
177
|
+
status = _ffi.lib.dmr_load_image_ex(
|
|
178
|
+
encode_path(path), int(as_),
|
|
179
|
+
ctypes.byref(c_settings) if c_settings is not None else None,
|
|
180
|
+
ctypes.byref(out))
|
|
181
|
+
self._ptr: Optional[ctypes.c_void_p] = out if out else None
|
|
182
|
+
self._views = weakref.WeakSet()
|
|
183
|
+
self.status = LoadStatus(status)
|
|
184
|
+
if self.status != LoadStatus.OK:
|
|
185
|
+
# *out гарантированно NULL при отказе (dmr_load_image, §2.6) —
|
|
186
|
+
# но проверка на всякий случай ничего не стоит и не лжёт молча.
|
|
187
|
+
self._ptr = None
|
|
188
|
+
|
|
189
|
+
def __bool__(self) -> bool:
|
|
190
|
+
return self._ptr is not None
|
|
191
|
+
|
|
192
|
+
def view(self) -> ImageView:
|
|
193
|
+
if self._ptr is None:
|
|
194
|
+
raise ValueError("Image не загружен (проверьте .status перед .view())")
|
|
195
|
+
struct = _ffi.lib.dmr_image_view(self._ptr)
|
|
196
|
+
# keepalive — сам Image: буфер живёт в dmr_image_t, а не в Python,
|
|
197
|
+
# и должен пережить любой scan() через этот view.
|
|
198
|
+
view = ImageView._from_struct(struct, self)
|
|
199
|
+
self._views.add(view)
|
|
200
|
+
return view
|
|
201
|
+
|
|
202
|
+
def close(self) -> None:
|
|
203
|
+
"""Освободить кадр. Пока жив хоть один ImageView от view(), —
|
|
204
|
+
RuntimeError: вид указывает в эту память, и scan() по нему после
|
|
205
|
+
освобождения читал бы освобождённую память (аудит 2026-10-01, P-12).
|
|
206
|
+
Удалите виды (del view) и закройте снова."""
|
|
207
|
+
if self._ptr is not None:
|
|
208
|
+
if len(self._views):
|
|
209
|
+
raise RuntimeError(
|
|
210
|
+
"Image.close(): жив ImageView этого кадра — освободить нельзя, "
|
|
211
|
+
"сначала удалите вид (del view)")
|
|
212
|
+
_ffi.lib.dmr_image_free(self._ptr)
|
|
213
|
+
self._ptr = None
|
|
214
|
+
|
|
215
|
+
def __enter__(self) -> "Image":
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
def __exit__(self, *exc) -> None:
|
|
219
|
+
self.close()
|
|
220
|
+
|
|
221
|
+
def __del__(self) -> None:
|
|
222
|
+
# Сюда доходит только объект без ссылок, а вид держит ссылку на свой
|
|
223
|
+
# Image — значит живых видов уже нет. Исключение из __del__ всё
|
|
224
|
+
# равно гасится, поэтому освобождаем без проверки.
|
|
225
|
+
if getattr(self, "_ptr", None) is not None:
|
|
226
|
+
_ffi.lib.dmr_image_free(self._ptr)
|
|
227
|
+
self._ptr = None
|