# libscanner — полная документация API > Собрано со страниц https://libscanner.com/docs. Указатель: https://libscanner.com/llms.txt # Чтение DataMatrix 1.0 — C++ и C ABI > Распознавание кодов DataMatrix структуры GS1 с кадров промышленной камеры — для конвейеров с маркировкой «Честного знака». Страница на сайте: https://libscanner.com/docs/datamatrix-gs1/1.0 ## Обзор Библиотека читает коды **DataMatrix (ECC200)** с кадров производственной камеры — например, коробки с маркировкой «Честного знака», где символ занимает на кадре около сотни пикселей. Ридер рассчитан на низкое разрешение съёмки — порядка двух пикселей на модуль символа — и проверяет каждую гипотезу геометрии кодовой коррекцией Рида-Соломона. Точность и скорость чтения — вверху страницы; ниже — только устройство интерфейса. Библиотека статическая, публичный интерфейс — на C++20, восемь заголовков в `include/dmr/`, плюс плоский интерфейс на C для других языков (`dmr_c.h`, раздел C ABI). Кадр передаётся видом на чужой буфер (`dmr::ImageView`) — без копирования, из уже имеющегося буфера камеры или снимка. > **Область применения.** По умолчанию сканер читает DataMatrix любой структуры. Режим `requireGs1` (раздел «Настройки») дополнительно проверяет формат содержимого — AI 01 (GTIN, 14 цифр со сходящейся контрольной суммой), затем AI 21 — и отбраковывает символы, не подходящие под структуру GS1. ## Подключение Библиотека приходит уже собранной — собирать её не нужно, собирается только ваша программа. Подключить её можно двумя способами: **статически** — C++ API из `libdmr.a` входит в вашу программу целиком, или **динамически** — через библиотеку `dmr_c` с плоским интерфейсом на C (C ABI). Состав поставки одинаковый на обеих ОС: **Windows:** ``` include\dmr\{Dmr,Diagnostics,Geometry,Image,License,Scanner,Settings,Version}.h C++ API include\dmr\dmr_c.h C ABI lib\libdmr.a статическая библиотека C++ lib\pkgconfig\dmr.pc bin\dmr_c.dll библиотека C ABI lib\dmr_c.dll.a её библиотека импорта (MinGW) lib\pkgconfig\dmr_c.pc bin\dmr.exe утилита командной строки share\doc\dmr\licenses\ лицензии сторонних библиотек ``` **Linux:** ``` include/dmr/{Dmr,Diagnostics,Geometry,Image,License,Scanner,Settings,Version}.h C++ API include/dmr/dmr_c.h C ABI lib/libdmr.a статическая библиотека C++ lib/pkgconfig/dmr.pc lib/libdmr_c.so библиотека C ABI lib/pkgconfig/dmr_c.pc bin/dmr утилита командной строки share/doc/dmr/licenses/ лицензии сторонних библиотек ``` | Ваша программа | Что брать | | --- | --- | | C++, собирается MinGW-w64 GCC (Windows) или GCC (Linux) | Статическая линковка — C++ API, `libdmr.a` | | MSVC или другой компилятор; другой язык (C, C#, Delphi, Go…); обновление библиотеки без пересборки программы | Динамическая линковка — C ABI, `dmr_c` | | Python | Колесо из «Загрузок» — [документация Python](https://libscanner.com/docs/datamatrix-gs1/1.0/python.md) | ### Статическая линковка — C++ API `libdmr.a` входит в вашу программу целиком: рядом с ней ничего класть не нужно, кроме файла ключа `license.key` — он рядом с программой (раздел «Лицензия»). Нужен компилятор **C++20**. API на C++, в результате возвращаются `std::string` и `std::vector`, поэтому компилятор и его стандартная библиотека должны быть теми же, что и у поставки (той же или более новой версии). **Windows:** > **Нужен MinGW-w64 GCC, не MSVC.** Поставка для Windows собрана MinGW-w64 GCC — `libdmr.a` в его формате и с его ABI. MSVC такой статический архив не слинкует; вашей программе нужен тот же тулчейн (например, MSYS2/MinGW или CLion с профилем MinGW). Из MSVC — динамически, через dmr_c. > > Тулчейн бесплатный: [MSYS2](https://www.msys2.org/) — установщик с сайта, затем в его консоли **MINGW64** (поставка собрана именно в этом окружении, не в UCRT64): > > ```bash > pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkgconf > ``` Пути включения и строка линковки берутся из `dmr.pc`. Путь к `lib\pkgconfig` распакованной поставки — через переменную окружения (PowerShell); пути в `.pc` считаются от места самого файла, так что каталог распаковки может быть любым: ```bash $env:PKG_CONFIG_PATH = "C:\куда\распакован\lib\pkgconfig" pkg-config --static --cflags --libs dmr ``` Системные `winhttp`, `crypt32`, `shell32` и `advapi32` (их использует проверка лицензии) уже прописаны в `Libs.private` пакета — отдельно подключать не нужно. **Linux:** Поставка собрана GCC 12 (Debian 12): нужен GCC 12 или новее. Пути включения и строка линковки — из `dmr.pc`; путь к `lib/pkgconfig` распакованной поставки — через переменную окружения, каталог распаковки может быть любым: ```bash PKG_CONFIG_PATH=/куда/распакован/lib/pkgconfig pkg-config --static --cflags --libs dmr ``` Нужны `-pthread -ldl` — они уже в пакете. `-std=c++20` файл `.pc` не добавляет — укажите сами (у GCC 12 по умолчанию C++17). Без pkg-config та же строка вручную: ```bash g++ -std=c++20 main.cpp -o мояпрограмма -I/куда/распакован/include /куда/распакован/lib/libdmr.a -pthread -ldl ``` OpenSSL в линковке не нужен: для `https://` сервера лицензий библиотека сама загружает `libssl.so.3` (или `.so.1.1`) при работе. > **Запрос к pkg-config должен быть статическим.** Поставка — одна статическая библиотека, а значит вызывающему нужны и те библиотеки, на которых стоит она сама. Без `--static` их в командной строке не окажется, и линковка упадёт на неразрешённых символах. В meson — зависимость по имени пакета: ```cpp dmr_dep = dependency('dmr', required: true, static: true) executable('мояпрограмма', 'main.cpp', dependencies: [dmr_dep], link_args: ['-static']) # Windows: рантайм MinGW внутрь программы ``` Под Windows `-static` вкомпоновывает в программу и рантайм компилятора: без него она потребует рядом `libstdc++-6.dll`, `libgcc_s_seh-1.dll` и `libwinpthread-1.dll` из MSYS2. Под Linux он не нужен. Полный `meson.build` — в «Быстром старте». ### Динамическая линковка — C ABI `dmr_c` — та же библиотека за плоским интерфейсом на C (заголовок `dmr/dmr_c.h`): функции `dmr_*` и непрозрачные указатели, без типов C++. Рантайм компилятора вкомпонован в неё статически, поэтому она не зависит ни от компилятора вызывающего, ни от его стандартной библиотеки C++ — годится для MSVC, Clang и любого языка с вызовом функций C. Всё, что библиотека отдаёт (результаты, строки), освобождается её же функциями: `dmr_scan_result_free`, `dmr_image_free`, `dmr_free_string`. Правила интерфейса, полный пример и все функции — в разделе C ABI. **Windows:** **MinGW-w64 GCC** — через `dmr_c.pc` и библиотеку импорта `lib\dmr_c.dll.a`, без `--static`: ```bash $env:PKG_CONFIG_PATH = "C:\куда\распакован\lib\pkgconfig" pkg-config --cflags --libs dmr_c ``` Это даёт `-I…\include -L…\lib -ldmr_c`. Содержимое `dmr_c.pc`: ```cpp prefix=${pcfiledir}/../.. includedir=${prefix}/include libdir=${prefix}/lib Name: dmr_c Version: 1.0.13 Libs: -L${libdir} -ldmr_c Cflags: -I${includedir} ``` **MSVC** — файла `.lib` в поставке нет, его делают из самой DLL. В консоли MSYS2 — описание экспорта: ```bash gendef dmr_c.dll # пакет mingw-w64-x86_64-tools; пишет dmr_c.def ``` затем в Developer Command Prompt for VS (x64) — библиотека импорта и сборка: ```bash lib /def:dmr_c.def /machine:x64 /out:dmr_c.lib cl /I C:\куда\распакован\include main.c dmr_c.lib ``` При запуске `dmr_c.dll` должна лежать рядом с вашим `.exe` (или в каталоге из `PATH`), а файл ключа `license.key` — рядом с `dmr_c.dll` или рядом с `.exe` (раздел «Лицензия»). **Linux:** ```bash PKG_CONFIG_PATH=/куда/распакован/lib/pkgconfig pkg-config --cflags --libs dmr_c ``` Это даёт `-I…/include -L…/lib -ldmr_c`. При запуске загрузчик должен найти `libdmr_c.so`: либо через `LD_LIBRARY_PATH`, либо положите её рядом с программой и пропишите путь поиска при линковке: ```bash LD_LIBRARY_PATH=/куда/распакован/lib ./мояпрограмма # или: libdmr_c.so рядом с программой cc main.c -o мояпрограмма $(pkg-config --cflags --libs dmr_c) -Wl,-rpath,'$ORIGIN' cp /куда/распакован/lib/libdmr_c.so . ``` Файл ключа `license.key` — рядом с `libdmr_c.so` или рядом с программой (раздел «Лицензия»). Версии в SONAME у библиотеки нет — файл называется просто `libdmr_c.so`. В meson — так же по имени пакета, без `static`: ```cpp dmr_c_dep = dependency('dmr_c', required: true) executable('мояпрограмма', 'main.c', dependencies: [dmr_c_dep]) ``` Колесо Python — обёртка как раз над `dmr_c`, библиотека лежит внутри пакета: [документация Python](https://libscanner.com/docs/datamatrix-gs1/1.0/python.md). ## Быстрый старт Минимальная программа: снимок с диска, чтение, вывод текста. Всё, что нужно, — один заголовок: ```cpp #include #include int main() { dmr::Image frame; if (dmr::loadImage("кадр.jpg", frame) != dmr::LoadStatus::Ok) { std::cerr << "не удалось прочитать файл\n"; return 1; } dmr::Scanner scanner; // поток кадров одной этикетки const dmr::ScanResult r = scanner.scan(frame); // Image неявно приводится к ImageView if (r.ok()) { std::cout << r.text() << "\n"; } else if (r.status == dmr::Status::NotFound) { std::cout << "символа на кадре нет\n"; } else { std::cout << "символ есть, прочитать не вышло\n"; } } ``` Собрать пример можно Meson. Рядом с `main.cpp` — распакованная поставка `dmr-1.0.13-windows-x64` и файл `meson.build`: ```cpp project('test_dmr', 'cpp', version : '0.1.0', default_options : ['cpp_std=c++20', 'warning_level=3', 'buildtype=release']) cxx = meson.get_compiler('cpp') # Готовая сборка dmr лежит рядом с проектом. Зависимость описываем вручную — # так pkg-config не нужен вовсе (в архивах 1.0.0, скачанных до 04.10.2026, # пути в .pc-файлах указывали на машину сборки; способ работает и с ними). dmr_root = meson.current_source_dir() / 'dmr-1.0.13-windows-x64' dmr_lib = cxx.find_library('dmr', dirs : dmr_root / 'lib', static : true) # Libs.private из dmr.pc — нужны при статической компоновке dmr_private = [] foreach l : ['winhttp', 'crypt32', 'shell32', 'advapi32'] dmr_private += cxx.find_library(l) endforeach dmr_dep = declare_dependency( include_directories : include_directories('dmr-1.0.13-windows-x64/include'), dependencies : [dmr_lib] + dmr_private) executable('test_dmr', 'main.cpp', dependencies : dmr_dep, cpp_args : ['-finput-charset=UTF-8', '-fexec-charset=UTF-8'], link_args : ['-static']) ``` Сборка — в консоли MSYS2 MINGW64: ```bash meson setup build meson compile -C build ``` То же через pkg-config: `.pc`-файлы поставки перемещаемые, поэтому вместо ручного описания хватит одной строки в `meson.build` — `dmr_dep = dependency('dmr', static : true)` — и пути к распакованному `lib/pkgconfig` при настройке: ```bash meson setup build --pkg-config-path="$PWD/dmr-1.0.13-windows-x64/lib/pkgconfig" meson compile -C build ``` Для кадра из своего конвейера захвата (SDK камеры, буфер OpenCV, Windows-битмап) диск не участвует — собирается вид на уже имеющийся буфер, без копирования: ```cpp const dmr::ImageView view{ m.data, m.cols, m.rows, (int)m.step, dmr::PixelFormat::Bgr8 }; const dmr::ScanResult r = scanner.scan(view); ``` ## Заголовки Публичных заголовков C++ семь, все под `include/dmr/`; восьмой, `dmr/Dmr.h`, включает их все разом. Девятый файл, `dmr/dmr_c.h`, — отдельный интерфейс на C (C ABI). | Заголовок | Что в нём | | --- | --- | | `dmr/Dmr.h` | Агрегат — включает всё нижеперечисленное | | `dmr/Image.h` | `ImageView`, `Image`, `PixelFormat`, `loadImage`, пути | | `dmr/Scanner.h` | `Scanner`, `ScanResult`, `Code`, `Status` | | `dmr/Settings.h` | Настройки и пресеты сканера | | `dmr/Geometry.h` | `Point`, `Box`, `Quad` — где на кадре расположен код | | `dmr/Diagnostics.h` | `explain()`, приёмник журнала (`LogSink`) | | `dmr/License.h` | Проверка и активация лицензии, пространство `dmr::license` | | `dmr/Version.h` | Версия заголовка (макросы) и собранной библиотеки (`dmr::version()`) | Заголовки не зависят друг от друга сверх необходимого: `Settings.h` и `Scanner.h` тянут за собой `` и ``, а тому, кто только собирает вид на кадр, достаточно одного `Image.h`. ## Scanner `Scanner` — объект, а не функция, и создавать его на каждый кадр не нужно: подсказка размера символа (`Settings::mcHintRun`) копится по кадрам потока и обязана их пережить, а число потоков пула — настройка процесса. При этом `scan()` объявлен `const` и не складывает в объект ничего, кроме этой подсказки: результат зависит от кадра, а не от истории чтений. Кандидаты внутри перебираются параллельно — свой пул потоков поверх `scan()` заводить не нужно. ```cpp class Scanner { public: explicit Scanner(Settings s = Settings::stream()); ~Scanner(); Scanner(Scanner&&) noexcept; Scanner& operator=(Scanner&&) noexcept; Scanner(const Scanner&) = delete; Scanner& operator=(const Scanner&) = delete; // Прочитать коды на кадре. // frame кадр целиком, цветной или полутоновый // moduleCount сторона символа в модулях; 0 — определять самому ScanResult scan(ImageView frame, int moduleCount = 0) const; // Снимок с диска и коды на нём под ОДНИМ сроком (budgetMs, timeoutMs): // загрузка и разбор вместе. Не загрузился — ImageTooLarge или // InvalidImage (при обрыве декодирования по сроку — и timedOut); // подробно — в load. С 1.0.10. ScanResult scanFile(const std::string& path, int moduleCount = 0, LoadStatus* load = nullptr) const; // Размер символа, подсказанный предыдущими кадрами; 0 — подсказки нет. int moduleCountHint() const; // Забыть подсказку. void resetStream(); const Settings& settings() const; }; // Разовое чтение без объекта: проба, скрипт, один файл. Подсказка размера // по потоку кадров здесь не работает — копить её нечему. ScanResult scan(ImageView frame, const Settings& s = Settings::single()); ``` Когда этикетка на конвейере сменилась, подсказку стоит сбросить явно, не дожидаясь, пока она погаснет сама: ```cpp scanner.resetStream(); ``` > **Ошибки возвращаются, а не бросаются.** `scan()` ловит всё внутри: внутренний сбой (например, нехватка памяти) — это `Status::Failed`, процесс живёт дальше. `loadImage()` сообщает о проблемах через `LoadStatus`. Из конструктора `Scanner` и функций `license::` может выйти только `std::bad_alloc` — как из любого кода C++, который выделяет память. C ABI не пропускает и его. > **Move-only.** `Scanner` можно перемещать (`Scanner&&`), но не копировать — конструктор и оператор копирования удалены явно. ## Результат чтения ### Status ```cpp enum class Status { Ok = 0, // прочитан хотя бы один код NotFound = 1, // символ на кадре не найден NotDecoded = 2, // символ найден, но не прочитан Unlicensed = 3, // кадр не разбирался: нет пробного периода и лицензии ImageTooLarge = 4, // кадр больше Settings::imageLimits, не разбирался InvalidImage = 5, // описание кадра противоречиво: шаг строки меньше // ширины, неизвестный формат пикселя Failed = 6, // разбор оборван внутренней ошибкой (например, нехватка // памяти); процесс живёт дальше }; ``` При `Status::Unlicensed` кадр не разбирался вовсе — причину даёт `dmr::license::status()` (раздел «Лицензия»). `Ok` всегда приходит хотя бы с одним кодом в `codes`; при любом другом статусе `codes` пуст. ### ScanResult | Поле | Тип | Смысл | | --- | --- | --- | | `status` | `Status` | Чем кончился разбор кадра | | `codes` | `std::vector` | Прочитанные коды, по одному на символ, в порядке чтения: сверху вниз, при равном верхнем крае — слева направо. Пуст, если `status` не `Ok`. При `Settings::maxCodes = 1` длина не больше единицы | | `timedOut` | `bool` | Перебор оборвался по `Settings::budgetMs` или `Settings::timeoutMs`. Взводится, только когда бюджет и правда что-то отсёк — успех при взведённом флаге возможен: код прочитан, но часть гипотез не проверена | | `elapsedMs` | `double` | Сколько занял разбор кадра, мс, без учёта загрузки снимка с диска | | `details` | `std::shared_ptr` | Разбор для диагностики; пусто, если не запрошен через `Settings::collectDetails` | Методы: `bool ok() const` — сокращение для `status == Status::Ok`; `const std::string& text() const` — текст первого кода или пустая строка, чтобы не писать `codes.empty() ? "" : codes[0].text` на каждом вызове. ### Code | Поле | Тип | Смысл | | --- | --- | --- | | `text` | `std::string` | Декодированная строка. Байты как есть: разделители GS остаются байтом `0x1D`, а не подставляются печатным видом | | `box` | `Box` | Где символ найден на исходном кадре — прямоугольник кандидата | | `corners` | `Quad` | Четыре угла символа там же, точнее бокса (раздел «Геометрия») | | `moduleCount` | `int` | Сторона символа в модулях | Повторы в `codes` уже слиты: два кандидата с одинаковой строкой и пересекающимися `box` — один символ, увиденный дважды, он возвращается одним кодом; две одинаковые этикетки, чьи боксы не пересекаются, остаются двумя кодами. Порядок — сверху вниз, затем слева направо. ### Несколько кодов на кадре ```cpp const dmr::Scanner scanner(dmr::Settings::multi(0)); // 0 — сколько найдётся for (const dmr::Code& c : scanner.scan(frame).codes) use(c.text, c.box, c.moduleCount); ``` `multi(n)` — это `stream()` с `maxCodes = n`: настроен на поток кадров камеры. Для несвязанных снимков (папка, отдельные файлы) с несколькими кодами добавьте то, что меняет `single()` — поля независимы: ```cpp auto s = dmr::Settings::multi(0); s.mcHintRun = 0; // без подсказки от прошлых кадров s.maxSizeTries = 24; // перебор всех размеров на кадре, где ничего не прочитано ``` ## Кадр ### PixelFormat ```cpp enum class PixelFormat { Gray8, // один канал: яркость; для сканера самый дешёвый Bgr8, // три канала, порядок OpenCV Rgb8, // три канала, порядок большинства остальных Bgra8, // четыре канала, порядок OpenCV и Windows Rgba8, // четыре канала }; ``` Сканер работает с яркостью и цветной кадр первым делом переводит в неё. Для монохромной камеры кадр стоит передавать как `Gray8`, а не размноженным в три канала — ответ тот же, а кадр обрабатывается быстрее. ### ImageView Вид на чужой буфер: библиотека его не копирует, не освобождает и не переживает — буфер обязан быть жив всё время вызова. ```cpp struct ImageView { const uint8_t* data = nullptr; int width = 0; int height = 0; int stride = 0; // байт в строке; 0 — плотно PixelFormat format = PixelFormat::Gray8; bool empty() const; int channels() const; int rowBytes() const; // stride с уже разобранным нулём const uint8_t* row(int y) const; }; ``` `stride = 0` значит «плотно» (`width * каналов`), а не «ноль байт в строке»: кадр камеры почти никогда не плотный — строки выравнивают, а вырезка наследует длину строки оригинала. Собирается на месте вызова без копирования: ```cpp const dmr::ImageView view{ m.data, m.cols, m.rows, (int)m.step, dmr::PixelFormat::Bgr8 }; ``` ### Image Кадр, которым библиотека владеет сама — нужен там, где она его создаёт, то есть при чтении с диска. Неявно приводится к `ImageView`, поэтому передаётся в `scan()` напрямую, без `.view()`: ```cpp class Image { public: Image() = default; Image(int width, int height, PixelFormat format); void reset(int width, int height, PixelFormat format); // плотная укладка void clear(); int width() const; int height() const; int stride() const; PixelFormat format() const; int channels() const; bool empty() const; uint8_t* data(); uint8_t* row(int y); ImageView view() const; operator ImageView() const; // неявно — для вызова scan(image) }; ``` ### Чтение с диска ```cpp enum class LoadStatus { Ok, // снимок прочитан NotFound, // файла нет Unreadable, // файл есть, но это не читаемый снимок TooLarge, // размер из заголовка больше ImageLimits; пиксели не читались }; enum class LoadAs { Color, // три канала, PixelFormat::Bgr8 Gray, // один канал, PixelFormat::Gray8 }; LoadStatus loadImage(const std::string& path, Image& out, LoadAs as = LoadAs::Color, const ImageLimits& limits = ImageLimits{}); // 16384 по стороне, 100 Мп ``` Форматы: **JPEG и PNG** — определяются по первым байтам файла, а не по расширению. JPEG обычный и прогрессивный, полутоновый и цветной; CMYK не поддерживается. PNG любой глубины и типа цвета: 16 бит сводятся к 8, палитра и малая глубина разворачиваются, альфа-канал отбрасывается (без наложения на фон). BMP, GIF, TIFF и WebP дают `Unreadable` — переведите их заранее или передайте уже раскодированные пиксели через `ImageView`. Обрезанный JPEG читается, насколько хватает данных. Для сканера `Gray` — лучший выбор: снимок монохромной камеры (полутон в трёхканальном JPEG) в этом виде читается дешевле, а ответ побайтово тот же, потому что каждый канал `Color` и есть эта яркость. При отказе `out` очищается — половина кадра от прошлой попытки хуже пустого. ```cpp dmr::Image frame; switch (dmr::loadImage("кадр.jpg", frame, dmr::LoadAs::Gray)) { case dmr::LoadStatus::Ok: break; case dmr::LoadStatus::NotFound: /* файла нет */ break; case dmr::LoadStatus::Unreadable: /* файл есть, но это не снимок */ break; } ``` > **Кодировка пути.** `loadImage` сама разбирается с системной кодировкой пути — каталог с русским именем не роняет чтение. Для собственной работы с путями в заголовке есть `toPath(const std::string&)`, `fromPath(const std::filesystem::path&)` (обратное преобразование) и `printablePath(...)` — путь в UTF-8, для журнала. ## Геометрия ```cpp struct Point { float x = 0.0f; float y = 0.0f; }; struct Box { int x = 0, y = 0, width = 0, height = 0; bool empty() const; }; struct Quad { Point corner[4]; }; ``` `Point` дробный: углы символа находятся с субмодульной точностью, и округление до целого теряло бы как раз её. `Box` — прямоугольник кандидата на исходном кадре; `Quad` — четыре его угла точнее, чем прямоугольником: на конвейере символ снят под углом. ```cpp for (const dmr::Point& p : c.corners.corner) drawTo(p.x, p.y); ``` > **Порядок углов.** Углы `Quad` идут по кругу, и под индексом 3 стоит тот, который **детектор** принял за угол сплошной L-рамки — решение, принятое по плотности сторон до всякого чтения. Окончательный поворот символа выбирается позже и с ним может не совпасть. Для разметки поверх кадра `corner[3]` годится, для выводов о содержимом символа — нет. ## Настройки Значения по умолчанию — не выбор наугад, а результат тестирования на большом наборе кадров. Начинать стоит с пресета, а не с отдельных полей — имя пресета уже говорит, под какой сценарий он настроен. | Пресет | Для чего | Чем отличается | | --- | --- | --- | | `Settings::stream()` | Поток кадров одной этикетки — конвейер, камера. Умолчание | Подсказка размера по предыдущим кадрам включена, перебор размеров выключен | | `Settings::single()` | Разовые снимки — папка, ручная проверка | Подсказки нет, перебираются 24 размера символа | | `Settings::multi(n)` | Несколько кодов на одном кадре | `stream()` с `maxCodes = n` (0 — сколько найдётся): подсказка включена, перебора размеров нет. Каждый кадр проходит полный перебор — примерно вчетверо дороже одиночного чтения | ```cpp auto s = dmr::Settings::stream(); s.budgetMs = 120.0; // 0 — без ограничения s.requireGs1 = true; // принимать только коды структуры GS1 const dmr::Scanner scanner(s); ``` > **stream() против single().** Разница не стилистическая, а в скорости. `single()` перебирает до 24 размеров символа, если размер заранее не известен, — на кадре, который так и не удалось прочитать (отказном), это заметно медленнее, чем без перебора, хотя и поднимает долю успешных чтений на несколько процентов. На потоке кадров одной этикетки размер от кадра к кадру не меняется, поэтому перебор не даёт выигрыша в точности вообще — только платит временем на каждом отказе. Именно поэтому в `stream()` он выключен, а в `single()` включён: для разрозненных снимков, где размер заранее не знаешь, эта плата оправдана. ### Все поля | Поле | Умолчание | Смысл | | --- | --- | --- | | `maxCodes` | 1 | Сколько кодов искать: 1 — один, 0 — сколько найдётся, N — не больше N | | `requireGs1` | false | Принимать только коды структуры GS1. Сошедшаяся коррекция Рида-Соломона сама по себе не доказывает верного чтения — проверка дополнительно смотрит на формат содержимого (AI 01, 14 цифр GTIN со сходящейся контрольной суммой, затем AI 21) | | `forcedRotation` | -1 | Поворот символа, 0..3 — четверти по часовой; -1 — определять самому | | `imageLimits` | 16384 по стороне, 100 000 000 пикселей | Наибольший кадр, который сканер берётся разбирать (`dmr::ImageLimits` из `dmr/Image.h`: `maxSide`, `maxPixels`; 0 — жёсткий потолок 65535 и 2^29). Кадр больше сразу получает `Status::ImageTooLarge`. Те же пределы передайте в `loadImage()` — там они сверяются с заголовком файла до выделения памяти. Защита, а не настройка качества | | `budgetMs` | 0.0 | Бюджет времени на кадр, мс; 0 — без ограничения. Срок мягкий: проверка идёт между стадиями перебора, а не внутри них, поэтому время кадра может превысить бюджет на длительность идущей стадии | | `timeoutMs` | 10000.0 | Предохранитель на кадр, мс; 0 и больше 60000 — потолок 60 с: дольше кадр не разбирается никогда. Для снимков из недоверенного источника задавайте и `budgetMs`. Защита от враждебных кадров (штриховка, текстура во весь кадр), а не настройка задержки: работает как `budgetMs` — из двух действует меньший, по истечении взводится `timedOut`, — но не меняет, чем перебор платит за скорость. `loadImage()` в C++ принимает только пределы размера, а загрузку и разбор под одним сроком даёт `Scanner::scanFile()`; в C ABI (`dmr_load_image_ex` → `DMR_LOAD_TIMED_OUT`) и в Python (`dmr.Image(path, settings=…)`) срок действует и на декодирование | | `rowParallel` | true | Делить строки ядер изображения между потоками пула, пока кадр идёт одним потоком. Снижает задержку кадра ценой процессорного времени — выключать, если на машине несколько сканеров или важнее пропускная способность, а не задержка | | `maxSizeTries` | 1 | Сколько размеров символа пробовать, когда размеру верить нельзя; 1 — не перебирать | | `warpScales` | пусто | Масштабы холста выпрямления для последнего прохода, пикселей на модуль; пусто — не перебирать; значения вне 1…32 пропускаются. Разбирается из строки вида `"7,8,10"` методом `setWarpScales()` | | `maxCandidates` | 6 | Сколько кандидатов брать у детектора по L-паттерну за один проход | | `readerCacheMb` | 64.0 | Предел памяти, МБ на кадр, под повторное использование работы; 0 — ничего не хранить. На ответ не влияет | | `mcHintRun` | 5 | Сколько кадров подряд должны сойтись на одном размере символа, чтобы дальше он проверялся первым; 0 — не подсказывать | | `maxPatternError` | 0.25 | Предел доли ошибок в служебных модулях (L-рамка, timing), при котором результат вообще рассматривается | | `lpatRetryScale` | 0.8 | Повторный поиск по L-паттерну на точном масштабе, как вторая гипотеза; 0 — не повторять | | `directSampling` | false | Брать яркости модулей прямо из исходника, минуя холст выпрямления | | `useLPatternFinder` / `useTextureFinder` | true / true | Источники кандидатов; выключение нужно для сравнения детекторов на одном корпусе | | `collectDetails` | false | Собирать разбор прочитанного и отказа в `ScanResult::details` (около 2 МБ на результат) | ## Диагностика Библиотека не пишет в консоль — приёмник журнала передаётся явно, и без него строки даже не собираются (из окружения она читает только язык сообщений, см. ниже): ```cpp using LogSink = std::function; void setLogSink(LogSink sink); // пустой — выключить void setVerbose(bool on); // по умолчанию выключена bool isVerbose(); // включена И есть приёмник std::ostream& dbg(); // поток выкладки: в приёмник, иначе «в никуда» ``` ```cpp dmr::setLogSink([](std::string_view line) { std::cerr << line; }); dmr::setVerbose(true); ``` Разбор причины конкретного чтения или отказа — отдельно и по запросу, строкой, а не в журнал. Собирается, только если при чтении был включён `Settings::collectDetails`: ```cpp struct ExplainOptions { int moduleCount = 0; // размер, заданный при чтении; 0 — определялся сам int forcedRotation = -1; // поворот, заданный при чтении bool images = false; // сохранять отладочные PNG рядом с рабочим каталогом std::string imagePath; // имя снимка — для подписей отладочных файлов }; std::string explain(const Details& details, const ExplainOptions& opt = {}); ``` ```cpp auto s = dmr::Settings::stream(); s.collectDetails = true; // около 2 МБ на результат const dmr::ScanResult r = dmr::Scanner(s).scan(frame); if (r.details) std::cerr << dmr::explain(*r.details); ``` > **explain() не выносит вердикт.** Статус кадра уже объявил сканер. Когда символ найден, но не прочитан, разбирать нечего — тогда вырезка заново выпрямляется и декодируется, чтобы отказу было чем объясниться, и это стоит примерно столько же, сколько само чтение кадра. ### Язык сообщений Тексты для человека — состояние лицензии (`status().message`), сообщения об ошибках, выкладка — на русском или английском. По умолчанию язык берётся из переменной окружения `LIBSCANNER_LANG` (`ru` или `en`), без неё — из языка системы: русская система — русский, любая другая (и локаль `C` в контейнере) — английский. Машиночитаемые поля и коды не переводятся. ```cpp bool setMessageLanguage(std::string_view lang); // "ru" или "en"; иначе false std::string_view messageLanguage(); // "ru" или "en" ``` ```cpp dmr::setMessageLanguage("en"); // на весь процесс ``` ## Версия библиотеки ```cpp #define DMR_VERSION_MAJOR 1 #define DMR_VERSION_MINOR 0 #define DMR_VERSION_PATCH 13 #define DMR_VERSION_STRING "1.0.13" #define DMR_VERSION_AT(ma, mi, pa) ((ma) * 10000 + (mi) * 100 + (pa)) #define DMR_VERSION DMR_VERSION_AT(DMR_VERSION_MAJOR, DMR_VERSION_MINOR, DMR_VERSION_PATCH) namespace dmr { std::string version(); } ``` Макросы говорят про заголовок, с которым скомпилировались; `dmr::version()` — про библиотеку, с которой слинковались. Разойтись они могут: библиотека ставится в префикс и живёт там дольше, чем помнит собиравший. ```cpp #if DMR_VERSION >= DMR_VERSION_AT(1, 1, 0) // код под заголовок 1.1.0 и новее #endif ``` ## Лицензия Сканер читает кадры, пока действует пробный период (месяц с первого запуска на машине, без сети и регистрации) или лицензия. Иначе `Scanner::scan()` сразу отвечает `Status::Unlicensed` и кадр не разбирает. Лицензия привязана к оборудованию (материнская плата, процессор, системный диск), а не к установке ОС. ```cpp #include const dmr::LicenseStatus s = dmr::license::status(); if (!s.canScan()) show(s.message); // «Пробный период закончился. Нужна лицензия» dmr::LicenseResult r = dmr::license::activate(keyFromUser); // «DMR-XXXXX-XXXXX-XXXXX-XXXXX» if (!r.ok) show(r.message); ``` ### Файл ключа license.key Вместо вызова `activate()` из своей программы ключ можно положить в текстовый файл `license.key`. Библиотека ищет его, берётся первый найденный: 1. рядом с модулем, где живёт библиотека: `dmr_c.dll` / `libdmr_c.so` для C ABI и Python (`dmr/_lib/`), сама программа — для статической `libdmr.a`; 2. рядом с исполняемым файлом программы. Текущий каталог и переменные окружения не просматриваются. Формат: ключ — первая строка, которая не пуста и не комментарий `#`, в том виде, в каком выдан — `DMR-XXXXX-XXXXX-XXXXX-XXXXX` (регистр, пробелы и дефисы не важны; BOM UTF-8 допустим; файлы больше 4 КБ не читаются). Файл читается при старте библиотеки (первый `Scanner` или вызов `license::` — этот вызов один раз ждёт сервер) и снова фоновой проверкой примерно раз в час: ключ, положенный при работающей программе, подхватится в течение часа. Действующую лицензию файл с другим ключом не заменяет; ключ, отвязанный `deactivate()`, из файла снова не активируется. Если ключ не принят, об этом скажет `status().message`. ### Где хранится состояние пробного периода и лицензии Windows: `%ProgramData%\dmr\license.dat`, `%LOCALAPPDATA%\dmr\license.dat` и значение реестра `HKCU\Software\dmr`. Linux: `/var/lib/dmr/license.dat` (пишет только root) и `~/.config/dmr/license.dat` пользователя, от которого работает процесс (по базе пользователей, а не по `$HOME`). Копий несколько, берётся самое раннее начало пробного периода. У контейнера нет ни данных DMI, ни своего диска, поэтому лицензию к нему не привязать — запускайте программу на хосте или в виртуальной машине. ### LicenseState | Значение | Смысл | | --- | --- | | `Trial` | Идёт пробный период | | `Active` | Лицензия действует | | `RefreshDue` | Действует, но связи с сервером давно не было; автосверка повторяет попытки раз в час | | `TrialExpired` | Пробный период кончился, лицензии нет | | `Expired` | Оплаченный срок лицензии кончился; продление в личном кабинете автосверка подхватит сама | | `NetworkRequired` | Без связи с сервером дальше нельзя; как только сервер ответит, автосверка вернёт лицензию | | `WrongMachine` | Лицензия выдана другой машине | | `Invalid` | Запись лицензии не проходит проверку подписи | ### LicenseStatus | Поле | Смысл | | --- | --- | | `state` | `LicenseState` | | `canScan()` | Читает ли сейчас сканер — true при `Trial`, `Active` или `RefreshDue` | | `validUntil` | Конец пробного периода или оплаченного срока, unix-секунды UTC | | `refreshAfter` | С какого момента токен считается устаревшим и лицензия переходит в RefreshDue; 0 — не устаревает | | `offlineUntil` | Дольше этого без связи с сервером лицензия не работает; 0 — без ограничения | | `daysLeft` | Целых суток до ближайшей из границ выше; 0 — граница пройдена | | `licenseId` / `activationId` | Идентификаторы лицензии и конкретной активации | | `offlineMode` | `limited \| extended \| full`; пусто без лицензии | | `clockRollback` | Системные часы отстают от времени, которое машина уже видела — откат часов срок не продлевает | | `message` | Состояние словами, для показа пользователю — на языке сообщений (ниже) | ### Активация: онлайн и офлайн Онлайн — ключ из личного кабинета, машина сама идёт на сервер лицензий: ```cpp dmr::LicenseResult r = dmr::license::activate("DMR-XXXXX-XXXXX-XXXXX-XXXXX"); ``` Офлайн — для машины без интернета: файл запроса переносится на машину с сетью и загружается в личный кабинет, в ответ приходит `license.lic`: ```cpp dmr::license::writeActivationRequest("request.json"); // → загрузить в кабинет, получить license.lic dmr::license::importLicenseFile("license.lic"); // тем же вызовом продлевается: новый файл заменяет старый ``` Активированная по сети лицензия раз в сутки сама сверяется с сервером в фоновом потоке — обновляет токен, приносит отзыв и продление срока. Вызывать для этого ничего не нужно, а чтение кадра сеть не ждёт никогда; сервер недоступен — повтор через час, лицензия работает, пока не выйдет срок работы без связи. ### Отвязка и остальные функции ```cpp namespace dmr::license { void configure(const LicenseOptions& options); // до первого сетевого вызова; можно не звать LicenseStatus status(); LicenseResult activate(const std::string& licenseKey); LicenseResult refresh(); // сверить сейчас, не дожидаясь суток LicenseResult deactivate(const std::string& reason = {}); // расписка уходит на сервер LicenseResult deactivateToFile(const std::string& receiptPath, const std::string& reason = {}); LicenseResult writeActivationRequest(const std::string& path); LicenseResult importLicenseFile(const std::string& path); std::string machineFingerprint(); // для обращений в поддержку } ``` Переезд на другую машину — `deactivate()` на старой: место освобождается на сервере распиской, подписанной секретом этой машины. Без сети — `deactivateToFile()`: расписка ложится в файл, отправить его можно с любой машины. Все функции потокобезопасны; функции с сетью блокируют вызывающего до ответа или до истечения `LicenseOptions::timeoutMs` (по умолчанию 15000 мс). Каждый ответ сервера лицензий привязан к своему запросу: библиотека отправляет случайное одноразовое число, сервер возвращает его в подписанном ответе, и записанный раньше ответ повторно не примется. Расписка о сдаче места тоже одноразовая — в ней номер из последнего полученного от сервера ответа, поэтому расписку из восстановленной копии старого состояния машины сервер не примет. Секрет для подписи расписок сервер выдаёт только при первой активации машины. У лицензий с полной автономией (`full`) сдача места расходует лимит переносов; когда он исчерпан, перенос делает поддержка. Самостоятельно освободить место по лицензии можно не больше двух раз за 30 дней в режиме limited и не больше одного раза — в extended; дальше перенос делает поддержка. > **Ключ активации.** Ключ активации для `activate()` выдаётся из личного кабинета после оформления тарифа в [каталоге](https://libscanner.com/). ## C ABI Та же библиотека за плоским интерфейсом на C99 — заголовок `dmr/dmr_c.h`, библиотека `dmr_c.dll` / `libdmr_c.so` (как подключить — «Подключение»). Для C, MSVC и любого языка с вызовом функций C (C#, Go, Delphi, Rust…). Правила границы: - Через неё ходят только простые типы C: целые, `double`, указатели на буферы известного размера, перечисления как `int` и непрозрачные дескрипторы. Исключение C++ границу не пересекает — внутренний сбой превращается в `NULL`или код ошибки. - Владение: всё, что функция *возвращает* (дескриптор, строка), принадлежит вызывающему и освобождается своей функцией (`dmr_image_free`,`dmr_scan_result_free`, `dmr_free_string`; сканер — `dmr_scanner_destroy`). Указатели из функций-доступа (`dmr_scan_result_code_text` и подобных) живут не дольше объекта, из которого взяты, и отдельно не освобождаются. - `dmr_settings_t` начинается с `struct_size` и метки `abi_tag`: их выставляет вызывающий **до** пресета — в C проще всего `dmr_settings_t s = DMR_SETTINGS_INIT;`, в C++ — `dmr_settings_prepare(&s)`. Пресет пишет не дальше `struct_size`; поля, которых вызывающий не знает (более новая библиотека), остаются по умолчанию. Чужая метка или размер вне допустимого — отказ: пресет вернёт 0, `dmr_scanner_create` — `NULL`. Программу, собранную с заголовком ABI 1.x или 2.0 (dmr до 1.0.8), нужно пересобрать. - Прочитанный текст хранит разделитель GS байтом `0x1D` (в отличие от утилиты`dmr`, которая печатает ``) и может содержать нулевой байт — длину брать у `dmr_scan_result_code_text_len`, а не`strlen()`. - Пути — в системной кодировке процесса, как `argv` (в Windows это не UTF-8, если кодовая страница процесса не переключена на UTF-8). - Статусы — те же, что `dmr::Status` (раздел «Результат чтения»), плюс`DMR_STATUS_INTERNAL_ERROR = -1` на сбой самого слоя C.`dmr_scanner_scan` возвращает `NULL` только при внутренней ошибке; «кода нет» — это результат со статусом `DMR_STATUS_NOT_FOUND`. - Функции лицензии возвращают тот же JSON, что `dmr --license … --json` (раздел «Командная строка»): один контракт для оболочки, C ABI и Python. ### Полный пример: все коды со снимка ```cpp #include #include /* Все коды со снимка: 0 — прочитаны, 1 — нет кода, 2 — ошибка. */ int main(int argc, char** argv) { if (argc < 2) return 2; dmr_settings_t settings = DMR_SETTINGS_INIT; /* размер и метка — до пресета */ dmr_settings_multi(&settings, 0); /* все коды на снимке */ settings.mc_hint_run = 0; /* отдельный файл: без подсказки прошлых кадров, */ settings.max_size_tries = 24; /* все размеры — как single() */ dmr_image_t* image = NULL; if (dmr_load_image_ex(argv[1], DMR_LOAD_GRAY, &settings, &image) != DMR_LOAD_OK) return 2; /* нет файла, не снимок, слишком большой */ dmr_scanner_t* scanner = dmr_scanner_create(&settings); if (!scanner) { dmr_image_free(image); return 2; } dmr_image_view_t view = dmr_image_view(image); dmr_scan_result_t* r = dmr_scanner_scan(scanner, &view, 0); /* 0 — размер сам */ int rc = 2; if (r) { /* NULL — внутренняя ошибка */ dmr_status_t st = dmr_scan_result_status(r); if (st == DMR_STATUS_OK) { for (int i = 0; i < dmr_scan_result_code_count(r); ++i) { /* в тексте бывают 0x1D (GS) и даже 0x00 — длина, а не strlen() */ fwrite(dmr_scan_result_code_text(r, i), 1, dmr_scan_result_code_text_len(r, i), stdout); putchar('\n'); } rc = 0; } else if (st == DMR_STATUS_NOT_FOUND || st == DMR_STATUS_NOT_DECODED) { rc = 1; } else if (st == DMR_STATUS_UNLICENSED) { char* why = dmr_license_status_json(); /* {"state":"trial_expired",...} */ fprintf(stderr, "%s\n", why ? why : "unlicensed"); dmr_free_string(why); } dmr_scan_result_free(r); } dmr_scanner_destroy(scanner); dmr_image_free(image); return rc; } ``` В Linux: `cc main.c -o read $(pkg-config --cflags --libs dmr_c)`, запуск —`LD_LIBRARY_PATH=/куда/распакован/lib ./read этикетка.png`. На коде с пачки «Честного знака» печатает строку с настоящими байтами `0x1D` между элементами. ### Справочник Все объявления `dmr_c.h` (ABI 3.1, библиотека 1.0.13); комментарии сокращены — полные в самом заголовке. ```cpp /* ── версии ── */ #define DMR_C_ABI_VERSION_MAJOR 3 #define DMR_C_ABI_VERSION_MINOR 0 size_t dmr_c_abi_version(char* buf, size_t buf_size); /* "3.1" */ size_t dmr_version(char* buf, size_t buf_size); /* версия библиотеки, "1.0.13" */ /* ── кадр ── */ typedef enum { DMR_PIXEL_GRAY8 = 0, DMR_PIXEL_BGR8 = 1, DMR_PIXEL_RGB8 = 2, DMR_PIXEL_BGRA8 = 3, DMR_PIXEL_RGBA8 = 4 } dmr_pixel_format_t; typedef struct { const uint8_t* data; /* буфер вызывающего, жив весь вызов scan */ int width; int height; int stride; /* байт на строку; 0 — плотно */ dmr_pixel_format_t format; } dmr_image_view_t; typedef struct dmr_image dmr_image_t; /* непрозрачный, буфер библиотеки */ typedef struct dmr_settings dmr_settings_t; /* определена ниже */ typedef enum { DMR_LOAD_COLOR = 0, DMR_LOAD_GRAY = 1 } dmr_load_as_t; typedef enum { DMR_LOAD_OK = 0, DMR_LOAD_NOT_FOUND = 1, DMR_LOAD_UNREADABLE = 2, DMR_LOAD_TOO_LARGE = 3, DMR_LOAD_TIMED_OUT = 4, DMR_LOAD_INVALID_ARGUMENT = 5 } dmr_load_status_t; dmr_load_status_t dmr_load_image(const char* path, dmr_load_as_t as, dmr_image_t** out); dmr_load_status_t dmr_load_image_ex(const char* path, dmr_load_as_t as, const dmr_settings_t* settings, /* NULL — по умолчанию */ dmr_image_t** out); dmr_image_view_t dmr_image_view(const dmr_image_t* image); void dmr_image_free(dmr_image_t* image); /* ── настройки: те же поля и умолчания, что dmr::Settings ── */ struct dmr_settings { uint32_t struct_size; /* sizeof(dmr_settings_t), ставит вызывающий ПЕРВЫМ */ uint32_t abi_tag; /* DMR_SETTINGS_TAG, тоже до пресета */ int max_codes; /* 1 — один, 0 — все, N — не больше N */ int require_gs1; /* 0/1 */ int forced_rotation; /* -1 — сам */ double budget_ms; /* 0 — без ограничения */ int row_parallel; /* 0/1 */ int max_size_tries; int warp_scales[16]; /* DMR_MAX_WARP_SCALES */ int warp_scales_count; /* значения вне 1..32 пропускаются */ int max_candidates; double reader_cache_mb; int mc_hint_run; double max_pattern_error; double lpat_retry_scale; int direct_sampling; /* 0/1 */ int use_lpattern_finder; /* 0/1 */ int use_texture_finder; /* 0/1 */ int collect_details; /* 0/1 */ int max_image_side; /* 0 — потолок 65535; в пресетах 16384 */ int64_t max_image_pixels; /* 0 — потолок 2^29; в пресетах 100 000 000 */ double timeout_ms; /* предохранитель, в пресетах 10000; 0 — потолок 60 с */ }; #define DMR_SETTINGS_TAG 0x33524D44u /* 'D' 'M' 'R' '3' */ #define DMR_SETTINGS_INIT { .struct_size = (uint32_t)sizeof(dmr_settings_t), .abi_tag = DMR_SETTINGS_TAG } #define DMR_SETTINGS_MAX_SIZE 4096u static inline void dmr_settings_prepare(dmr_settings_t* s); /* обнуляет, ставит размер и метку */ int dmr_settings_stream(dmr_settings_t* out); /* 1 — заполнено; 0 — NULL, чужая */ int dmr_settings_single(dmr_settings_t* out); /* метка или размер */ int dmr_settings_multi(dmr_settings_t* out, int max_codes); /* ── сканер ── */ typedef struct dmr_scanner dmr_scanner_t; /* непрозрачный */ dmr_scanner_t* dmr_scanner_create(const dmr_settings_t* settings); /* NULL — stream() */ void dmr_scanner_destroy(dmr_scanner_t* scanner); int dmr_scanner_module_count_hint(const dmr_scanner_t* scanner); void dmr_scanner_reset_stream(dmr_scanner_t* scanner); /* ── результат ── */ typedef enum { DMR_STATUS_OK = 0, DMR_STATUS_NOT_FOUND = 1, DMR_STATUS_NOT_DECODED = 2, DMR_STATUS_UNLICENSED = 3, DMR_STATUS_IMAGE_TOO_LARGE = 4, DMR_STATUS_INVALID = 5, DMR_STATUS_FAILED = 6, DMR_STATUS_INTERNAL_ERROR = -1 } dmr_status_t; typedef struct dmr_scan_result dmr_scan_result_t; /* непрозрачный */ dmr_scan_result_t* dmr_scanner_scan(dmr_scanner_t* scanner, const dmr_image_view_t* frame, int module_count); /* 0 — сам; NULL — внутренняя ошибка */ /* ABI 3.1: снимок с диска и коды на нём под одним сроком сканера; итог * загрузки — в *load_status (NULL — не нужен) */ dmr_scan_result_t* dmr_scanner_scan_file(dmr_scanner_t* scanner, const char* path, int module_count, dmr_load_status_t* load_status); void dmr_scan_result_free(dmr_scan_result_t* result); dmr_status_t dmr_scan_result_status(const dmr_scan_result_t* result); int dmr_scan_result_timed_out(const dmr_scan_result_t* result); double dmr_scan_result_elapsed_ms(const dmr_scan_result_t* result); int dmr_scan_result_code_count(const dmr_scan_result_t* result); const char* dmr_scan_result_code_text(const dmr_scan_result_t* result, int index); size_t dmr_scan_result_code_text_len(const dmr_scan_result_t* result, int index); void dmr_scan_result_code_box(const dmr_scan_result_t* result, int index, int* x, int* y, int* width, int* height); void dmr_scan_result_code_corners(const dmr_scan_result_t* result, int index, float corners_xy[8]); /* x0,y0 … x3,y3 */ int dmr_scan_result_code_module_count(const dmr_scan_result_t* result, int index); /* ── диагностика ── */ typedef void (*dmr_log_sink_t)(const char* line, size_t len, void* user); /* line без завершающего нуля */ void dmr_set_log_sink(dmr_log_sink_t sink, void* user); void dmr_set_verbose(int on); int dmr_is_verbose(void); /* ── лицензия: JSON как у "dmr --license … --json"; освобождать dmr_free_string, NULL — внутренняя ошибка ── */ void dmr_license_configure(const char* server_url, const char* app_version, int timeout_ms, int auto_check); char* dmr_license_status_json(void); char* dmr_license_activate_json(const char* key); char* dmr_license_refresh_json(void); char* dmr_license_deactivate_json(const char* reason); /* reason может быть NULL */ char* dmr_license_deactivate_to_file_json(const char* path, const char* reason); char* dmr_license_write_activation_request_json(const char* path); char* dmr_license_import_license_file_json(const char* path); char* dmr_license_fingerprint_json(void); void dmr_free_string(char* s); ``` ## Командная строка В поставку вместе с библиотекой входит готовая оболочка командной строки `dmr` — для быстрой проверки без своего кода: разовый снимок, папка, список путей или поток для внешнего сервиса. ``` dmr <снимок> [число_модулей] [--verbose] [--rot 0..3] [--budget-ms N] [--codes N] dmr --batch <список> [число_модулей] [--verbose] dmr --folder <каталог> [число_модулей] [--verbose] dmr --serve [число_модулей] [--verbose] dmr --license status | activate <ключ> | refresh | deactivate | fingerprint [--json] ``` ### Разовый снимок ``` $ dmr этикетка.jpg запуск: 4.6 мс обработка: 24.9 мс код: 0104627191145677215n5Gr,pHTGIU&93CuXU ``` Сообщения утилиты — на языке системы (на любой не русской — по-английски: `startup`, `processing`, `code`); ключ `--lang ru|en` или переменная `LIBSCANNER_LANG` задают его явно (язык сообщений). Строки `--batch`/`--serve` и вывод `--json` от языка не зависят. По строке `код:` на каждый код; в тексте GS показан как ``, TAB — как ``, переводы строк — пробелами. Если бюджет оборвал перебор, следом идёт строка `бюджет: исчерпан`. Ошибки — в stderr. Результат — код возврата: | Код | Смысл | | --- | --- | | 0 | Код прочитан (и когда бюджет оборвал перебор, но коды есть) | | 1 | Код не найден; также нет файла, не JPEG/PNG, вышел срок декодирования | | 2 | Символ найден, но не прочитан | | 3 | Нет пробного периода и лицензии | | 4 | Снимок больше предела | | 5 | Описание снимка некорректно | | 6 | Внутренняя ошибка | ### Пакетный режим и поток `--batch` и `--folder` — один процесс на весь заранее известный набор снимков, результат построчно на stdout: `путь · код возврата · миллисекунды · текст` через TAB — по строке на код, одна строка с пустым текстом для снимка без кода; во втором столбце — коды из таблицы выше. `--corners` добавляет пятый столбец `x0,y0 x1,y1 x2,y2 x3,y3`. Сводка — в stderr. Без лицензии прогон останавливается с кодом 3. `--serve` — тот же формат строки, но снимки не известны заранее: путь приходит по одному, по строке из stdin, и обрабатывается сразу же — процесс держит сканер живым между запросами внешнего сервиса, не платя за инициализацию на каждый. Результат сбрасывается сразу (`std::flush`), не дожидаясь, пока закроют весь ввод, — так демо-раздел этого сайта ([/scan](https://libscanner.com/scan)) держит один процесс на все запросы, вместо процесса на снимок. После строк каждого снимка идёт строка `#END#` — по ней видно, что ответ закончен, даже когда кодов несколько. Процесс завершается по концу ввода. ``` $ mkfifo queue $ dmr --serve < queue & $ echo /путь/к/снимку.jpg > queue /путь/к/снимку.jpg 0 91.2 0104627191145677215n5Gr,pHTGIU&93CuXU ``` ### Лицензия: машиночитаемый вывод У всех подкоманд `--license` есть флаг `--json` — тот же результат, что и в человекочитаемом выводе, но в виде одной JSON-строки на stdout, для скриптов и админ-панелей: ``` $ dmr --license status --json {"state":"trial","can_scan":true,"valid_until":1792831857,"refresh_after":0, "offline_until":0,"days_left":28,"license_id":"","activation_id":"", "offline_mode":"","clock_rollback":false,"message":"Пробный период: осталось 28 дн."} ``` Поле `state` — устойчивый машиночитаемый код (`trial`, `active`, `refresh_due`, `trial_expired`, `expired`, `network_required`, `wrong_machine`, `invalid`) — часть формата вывода, не переводится и не меняется словами. Поле `message` — текст для человека на языке сообщений. --- # Чтение DataMatrix 1.0 — Python > Распознавание кодов DataMatrix структуры GS1 с кадров промышленной камеры — для конвейеров с маркировкой «Честного знака». Страница на сайте: https://libscanner.com/docs/datamatrix-gs1/1.0?lang=python ## Обзор Пакет `dmr` читает коды **DataMatrix (ECC200)** с кадров производственной камеры — например, коробки с маркировкой «Честного знака», где символ занимает на кадре около сотни пикселей. Это обёртка на `ctypes` поверх плоского C ABI (`dmr_c`) той же библиотеки, что описана в основной документации — прямой вызов в процессе, без накладных расходов на запуск внешнего процесса на каждый кадр. Внешних зависимостей у пакета нет — только стандартная библиотека (`ctypes`). Сама библиотека (`dmr_c.dll` на Windows, `libdmr_c.so` на Linux) лежит внутри колеса уже собранной — собирать её не нужно, пакет только вызывает готовую. > **Когда библиотеки под рукой нет.** Если колеса под вашу платформу нет (например, на Linux с glibc старше 2.36), есть второй путь — вызывать готовую утилиту `dmr` (тоже входит в поставку) внешним процессом (`subprocess`), совсем без этого пакета. Он медленнее (накладные расходы на процесс), но всегда доступен — см. раздел «Через subprocess» ниже. ## Установка ```bash pip install libscanner-dmr ``` Пакет на PyPI называется `libscanner-dmr`, импортируется как `dmr`. pip сам выберет колесо своей платформы: Windows x86-64, Linux x86-64, ARM64 и ARMv7 (Raspberry Pi OS 64 и 32 бит). Колёса для Linux собраны под glibc 2.36 и новее (Debian 12, Ubuntu 23.04+, Raspberry Pi OS Bookworm). Вместе с пакетом ставится команда `dmr` (и `python -m dmr`). Python 3.9 и новее. Без доступа к PyPI — то же колесо из раздела «Загрузки» личного кабинета: **Windows:** ```bash pip install .\libscanner_dmr-1.0.13-py3-none-win_amd64.whl ``` **Linux:** ```bash pip install ./libscanner_dmr-1.0.13-py3-none-manylinux_2_36_x86_64.whl # x86-64 pip install ./libscanner_dmr-1.0.13-py3-none-manylinux_2_36_aarch64.whl # ARM64 pip install ./libscanner_dmr-1.0.13-py3-none-manylinux_2_36_armv7l.whl # ARMv7 ``` Библиотека лежит внутри пакета, в `dmr/_lib/`, и грузится только оттуда, по полному пути; системного поиска нет. Переменная `DMR_C_LIBRARY` (полный путь к файлу) нужна только пакету, собранному из исходников, где `_lib` нет. ## Быстрый старт Минимальная программа: снимок с диска, чтение, вывод текста: ```python import dmr img = dmr.Image("кадр.jpg", dmr.LoadAs.GRAY) # сканеру цвет не нужен scanner = dmr.Scanner() # поток кадров, умолчание result = scanner.scan(img.view()) if result.ok: print(result.text.decode("utf-8")) elif result.status == dmr.Status.NOT_FOUND: print("символа на кадре нет") else: print("символ есть, прочитать не вышло") ``` Кадр из чужого буфера — без копирования, видом на него (буфер обязан быть жив на время вызова `scan()`): ```python view = dmr.ImageView(buf, width, height, stride, dmr.PixelFormat.BGR8) result = scanner.scan(view) ``` > **Sканер создаётся один раз, а не на кадр.** Подсказка размера символа (`Settings.mc_hint_run`) копится по кадрам и обязана их пережить — создавайте `Scanner` один раз на поток кадров. Вызовы одного сканера из разных потоков идут по очереди; параллельно — по сканеру на поток. ## Модули Публичные имена собраны прямо в пакете `dmr` — импортировать из подмодулей напрямую не нужно, кроме `dmr.license` и `dmr.diagnostics`, у которых свои пространства имён: | Имя | Что это | | --- | --- | | `dmr.Image` / `dmr.ImageView` | Кадр — владеющий (с диска) и вид на чужой буфер | | `dmr.PixelFormat` / `dmr.LoadAs` / `dmr.LoadStatus` | Формат пикселя, режим и статус загрузки с диска | | `dmr.Scanner` / `dmr.scan` | Объект-сканер (поток кадров) и разовая свободная функция | | `dmr.ScanResult` / `dmr.Code` / `dmr.Status` | Результат чтения кадра и одного кода | | `dmr.Box` / `dmr.Point` | Геометрия найденного символа — поля `Code` | | `dmr.Settings` | Настройки сканера и пресеты | | `dmr.DmrError` | Исключение границы C ABI — не отказ чтения, а сбой самой библиотеки | | `dmr.license` | Подмодуль лицензии — `import dmr.license as license` | | `dmr.diagnostics` | Подмодуль журнала — `import dmr.diagnostics as diagnostics` | | `dmr.version()` / `dmr.abi_version()` | Версия собранной библиотеки и версия самой границы C ABI по отдельности | ## Scanner `Scanner` управляет ресурсом C ABI — освобождайте явно `close()` или используйте как контекстный менеджер (`with`); `__del__` подстраховывает, но порядок финализации при выходе из процесса гарантирован не всегда. ```python class Scanner: def __init__(self, settings: Settings | None = None): ... def scan(self, frame: ImageView, module_count: int = 0) -> ScanResult: """Прочитать коды на кадре. module_count — сторона символа в модулях, если известна заранее; 0 — определять самому.""" def scan_file(self, path, module_count: int = 0) -> ScanResult: """Снимок с диска и коды на нём под ОДНИМ сроком (budget_ms, timeout_ms). Не загрузился — status IMAGE_TOO_LARGE или INVALID, подробно — в load_status. С 1.0.10.""" @property def module_count_hint(self) -> int: """Размер, подсказанный предыдущими кадрами; 0 — подсказки нет.""" def reset_stream(self) -> None: """Забыть подсказку — этикетка сменилась и вы об этом знаете.""" def close(self) -> None: ... def __enter__(self) -> "Scanner": ... def __exit__(self, *exc) -> None: ... def scan(frame: ImageView, settings: Settings | None = None) -> ScanResult: """Разовое чтение без объекта — подсказка размера по потоку кадров здесь не работает: копить её нечему.""" def scan_file(path, settings: Settings | None = None) -> ScanResult: """Разовое чтение снимка с диска: загрузка и разбор под одним сроком.""" ``` Когда этикетка на конвейере сменилась, подсказку стоит сбросить явно: ```python scanner.reset_stream() ``` Несколько кодов на одном кадре: ```python scanner = dmr.Scanner(dmr.Settings.multi(0)) # 0 — сколько найдётся for code in scanner.scan(view).codes: print(code.text, code.box) ``` > **Контекстный менеджер.** `with dmr.Scanner() as scanner: ...` — `close()` вызовется даже при исключении внутри блока. ## Результат чтения ### Status ```python class Status(enum.IntEnum): OK = 0 # прочитан хотя бы один код NOT_FOUND = 1 # символ на кадре не найден NOT_DECODED = 2 # символ найден, но не прочитан UNLICENSED = 3 # кадр не разбирался: нет пробного периода и лицензии IMAGE_TOO_LARGE = 4 # кадр больше Settings.max_image_side / max_image_pixels INVALID = 5 # описание кадра противоречиво (stride, формат) FAILED = 6 # разбор оборвался внутренней ошибкой ``` `OK` всегда приходит хотя бы с одним кодом; при любом другом статусе `codes` пуст. Повторные находки одного символа уже слиты (одинаковый текст и пересекающиеся боксы); одинаковые этикетки в разных местах остаются разными кодами. ### ScanResult ```python @dataclass(frozen=True) class ScanResult: status: Status codes: list[Code] timed_out: bool # перебор оборвался по Settings.budget_ms или timeout_ms elapsed_ms: float load_status: LoadStatus | None = None # итог загрузки у scan_file(); у scan() — None @property def ok(self) -> bool: ... # status == Status.OK @property def text(self) -> bytes: ... # текст первого кода или b"" ``` `codes` пуст, если `status` не `Status.OK`; в порядке чтения — сверху вниз, при равном верхнем крае слева направо. ### Code ```python @dataclass(frozen=True) class Code: text: bytes # НЕ str — разделитель GS1 остаётся байтом 0x1D как есть box: Box # x, y, width, height — прямоугольник кандидата на кадре corners: tuple # четыре Point по кругу, точнее прямоугольника module_count: int # сторона символа в модулях class Box(NamedTuple): x: int y: int width: int height: int class Point(NamedTuple): x: float y: float ``` > **Байты, а не строка.** `Code.text` — `bytes`: разделитель GS1 остаётся байтом `0x1D`, без замены печатным видом. Декодируйте сами тем способом, который знает про ваш формат: > > ```python > text = result.codes[0].text.decode("utf-8") > ``` `box` нужен не для красоты: два кандидата с одинаковой строкой — это один символ, увиденный дважды, если боксы пересекаются, и две одинаковые этикетки, если нет. Углы `corners` точнее `box`: на конвейере символ снят под углом. ## Кадр ### PixelFormat ```python class PixelFormat(enum.IntEnum): GRAY8 = 0 # один канал: яркость; для сканера самый дешёвый BGR8 = 1 # три канала, порядок OpenCV RGB8 = 2 # три канала, порядок большинства остальных BGRA8 = 3 # четыре канала, порядок OpenCV и Windows RGBA8 = 4 # четыре канала ``` Сканер работает с яркостью и цветной кадр первым делом переводит в неё. Для монохромной камеры кадр стоит передавать как `GRAY8`, а не размноженным в три канала — ответ тот же, а кадр обрабатывается быстрее. ### ImageView Вид на чужой буфер — библиотека его не копирует, не освобождает и не переживает; буфер обязан быть жив всё время вызова. Годится любой объект с buffer-протоколом (`bytearray`, numpy-массив, `mmap`): ```python view = dmr.ImageView(buf, width, height, stride, dmr.PixelFormat.BGR8) print(view.width, view.height, view.stride, view.format) ``` Для **неписабельных** буферов (`bytes`, read-only `memoryview`) пакет делает одну явную копию — молча внутри библиотеки такого не происходит. ### Image Кадр, которым библиотека владеет сама — читается с диска. Освобождается при `close()`/выходе из `with`/сборке мусора; явный `close()` надёжнее — `__del__` не гарантирован по времени: ```python class LoadAs(enum.IntEnum): COLOR = 0 # три канала, PixelFormat.BGR8 GRAY = 1 # один канал, PixelFormat.GRAY8 class LoadStatus(enum.IntEnum): OK = 0 NOT_FOUND = 1 UNREADABLE = 2 TOO_LARGE = 3 # больше Settings.max_image_side / max_image_pixels TIMED_OUT = 4 # декодирование не уложилось в budget_ms / timeout_ms INVALID_ARGUMENT = 5 # настройки отвергнуты границей C ABI class Image: def __init__(self, path, as_: LoadAs = LoadAs.GRAY, settings: Settings | None = None): ... with dmr.Image("кадр.jpg", dmr.LoadAs.GRAY) as img: if not img: print("статус:", img.status.name) # NOT_FOUND, UNREADABLE, TOO_LARGE… else: result = scanner.scan(img.view()) ``` Неудачная загрузка не бросает исключение: объект ложен, причину говорит `status`; `view()` у такого объекта бросает `ValueError`. `settings` передаёт декодеру предел размера кадра и срок (`None` — умолчания библиотеки). Форматы: **JPEG и PNG**, определяются по содержимому, а не по расширению; CMYK JPEG не поддерживается; PNG 16 бит, с палитрой и альфой приводятся. BMP, GIF, TIFF и WebP дают `UNREADABLE` — раскодируйте их сами (например, Pillow) и передайте пиксели как `dmr.ImageView`. > **Кодировка путей на Windows.** Пути к файлам кодируются в системной кодировке процесса (как `argv`), не в UTF-8 — на Windows это обычно ANSI-кодировка (`cp1251` на русской локали). Пакет делает это преобразование сам; знать про него нужно, только если путь передаётся уже готовыми `bytes`, а не строкой Python. ## Настройки Пресеты берутся у самой библиотеки, а не хранятся в Python отдельно — значения по умолчанию за ними не выбор наугад, а результат тестирования на большом наборе кадров: ```python settings = dmr.Settings.stream() # поток кадров одной этикетки, умолчание settings = dmr.Settings.single() # разовые снимки: папка, ручная проверка settings = dmr.Settings.multi(0) # несколько кодов на кадре, 0 — сколько найдётся settings.budget_ms = 120.0 # своё поле — как обычный dataclass scanner = dmr.Scanner(settings) ``` > **stream() против single().** Разница не стилистическая, а в скорости. `single()` перебирает до 24 размеров символа, если размер заранее не известен, — на кадре, который так и не удалось прочитать (отказном), это заметно медленнее, чем без перебора, хотя и поднимает долю успешных чтений на несколько процентов. На потоке кадров одной этикетки размер от кадра к кадру не меняется, поэтому перебор не даёт выигрыша в точности вообще — только платит временем на каждом отказе. Именно поэтому в `stream()` он выключен, а в `single()` включён: для разрозненных снимков, где размер заранее не знаешь, эта плата оправдана. ### Поля Settings | Поле | Умолчание | Смысл | | --- | --- | --- | | `max_codes` | 1 | Сколько кодов искать: 1 — один, 0 — сколько найдётся, N — не больше N | | `require_gs1` | False | Принимать только коды структуры GS1 (AI 01, 14 цифр GTIN, затем AI 21) | | `forced_rotation` | -1 | Поворот символа, 0..3 — четверти по часовой; -1 — определять самому | | `budget_ms` | 0.0 | Бюджет времени на кадр, мс; 0 — без ограничения. Срок мягкий | | `timeout_ms` | 10000.0 | Предохранитель на кадр, мс; 0 и больше 60000 — потолок 60 с. Защита от враждебных кадров; действует меньший из него и `budget_ms` | | `max_image_side` / `max_image_pixels` | 16384 / 100000000 | Наибольший кадр к разбору; больше — `IMAGE_TOO_LARGE` без разбора (и `TOO_LARGE` при загрузке с этими настройками) | | `row_parallel` | True | Делить строки кадра между потоками пула, пока кадр идёт одним потоком | | `max_size_tries` | 1 | Сколько размеров символа пробовать; 1 — не перебирать | | `warp_scales` | [] | Масштабы холста выпрямления для последнего прохода, 1…32 пикселя на модуль (другие — ValueError); пусто — не перебирать | | `max_candidates` | 6 | Сколько кандидатов брать у детектора за один проход | | `reader_cache_mb` | 64.0 | Предел памяти, МБ на кадр, под повторное использование работы; 0 — ничего не хранить | | `mc_hint_run` | 5 | Сколько кадров подряд должны сойтись на одном размере, чтобы он проверялся первым | | `max_pattern_error` | 0.25 | Предел доли ошибок в служебных модулях, при котором результат вообще рассматривается | | `lpat_retry_scale` | 0.8 | Повторный поиск по L-паттерну на точном масштабе; 0 — не повторять | | `direct_sampling` | False | Брать яркости модулей прямо из исходника, минуя холст выпрямления | | `use_lpattern_finder` / `use_texture_finder` | True / True | Источники кандидатов; выключение нужно для сравнения детекторов на одном корпусе | > **collect_details не выведен.** В этой версии C ABI разбор отказа (`explain()`) не отдаётся структурно — поэтому поле не выведено в Python: незачем предлагать настройку, которая ничего не может изменить. Из Python разбор доступен только через утилиту `dmr` (раздел «Через subprocess» ниже). ## Диагностика Библиотека не пишет в консоль сама — приёмник строк лога передаётся явно, и без него строки не собираются: ```python import dmr.diagnostics as diagnostics diagnostics.set_log_sink(lambda line: print(line.decode("utf-8"), end="")) diagnostics.set_verbose(True) print(diagnostics.is_verbose()) # True — включена И есть приёмник ``` `sink` получает сырые `bytes` одной строки (сообщения из библиотеки в UTF-8) — декодировать их или нет, решает вызывающий.`set_log_sink(None)` выключает приёмник. ## Версия библиотеки ```python print(dmr.version()) # версия СОБРАННОЙ библиотеки dmr_c print(dmr.abi_version()) # версия самой границы C ABI, отдельно от версии алгоритма ``` ## Лицензия Сканер читает кадры, пока действует пробный период или лицензия. Отказ операции (неверный ключ, сеть недоступна) — не исключение, а обычный `dict` с понятной причиной; исключение поднимается, только если с границей C ABI что-то не так: ```python import dmr.license as license s = license.status() if not s["can_scan"]: print(s["message"]) # «Пробный период закончился. Нужна лицензия» r = license.activate("DMR-XXXXX-XXXXX-XXXXX-XXXXX") if not r["ok"]: print(r["error"], r["message"]) ``` Формат словарей — тот же JSON, что у `dmr --license ... --json`: один контракт для оболочки, C ABI и пакета. **Файл ключа.** Вместо вызова `activate()` ключ можно положить в текстовый файл `license.key` в `dmr/_lib/` установленного пакета (рядом с библиотекой; запасной вариант — каталог самого `python`). Ключ — первая непустая строка, не комментарий `#`, в том виде, в каком выдан; при первом использовании он уходит на сервер, а файл перечитывается примерно раз в час. Подробности — на странице C++, раздел «Лицензия». **Исключения.** `dmr.DmrError` (наследник `RuntimeError`) — только сбой самой границы C ABI. `ValueError` — неверные аргументы (значение вне диапазона, закрытый сканер, `view()` у незагруженного снимка). `OSError` — не загрузилась встроенная библиотека. Результаты чтения и отказы лицензии — значения, а не исключения. **Язык сообщений.** Тексты исключений и `message` лицензии — на русском или английском: переменная окружения `LIBSCANNER_LANG` (`ru` или `en`), иначе язык системы (на любой не русской — английский). Задавайте переменную до импорта пакета. ### Остальные функции ```python def configure(server_url=None, app_version=None, timeout_ms=None, auto_check=True) -> None: ... def status() -> dict: ... def activate(key: str) -> dict: ... def refresh() -> dict: ... # сверить сейчас, не дожидаясь суток def deactivate(reason: str | None = None) -> dict: ... # расписка уходит на сервер def deactivate_to_file(path, reason: str | None = None) -> dict: ... def write_activation_request(path) -> dict: ... def import_license_file(path) -> dict: ... # годится и для продления def fingerprint() -> str: ... # для обращений в поддержку ``` Переезд на другую машину — `deactivate()` на старой: место освобождается распиской этой машины. Каждый ответ сервера лицензий привязан к своему запросу: пакет отправляет случайное одноразовое число, сервер возвращает его в подписанном ответе, и записанный раньше ответ повторно не примется. Расписка тоже одноразовая — в ней номер из последнего полученного от сервера ответа, поэтому расписку из восстановленной копии старого состояния машины сервер не примет; секрет для её подписи сервер выдаёт только при первой активации машины. У лицензий с полной автономией (`full`) сдача места расходует лимит переносов; когда он исчерпан, перенос делает поддержка. Самостоятельно освободить место по лицензии можно не больше двух раз за 30 дней в режиме limited и не больше одного раза — в extended; дальше перенос делает поддержка. > **Ключ активации.** Ключ активации выдаётся из личного кабинета после оформления тарифа в [каталоге](https://libscanner.com/). ## Через subprocess Когда собрать или поставить `dmr_c` нельзя (или не хочется), утилиту `dmr` можно вызывать как внешний процесс — тот путь по-прежнему доступен и не требует никакой сборки на стороне Python: ```python import subprocess out = subprocess.run( ["dmr", "этикетка.jpg"], capture_output=True, text=True, encoding="utf-8", ) print(out.returncode) # 0 — прочитан, 1 — кода нет, 2 — не прочитан, 3 — нет лицензии (таблица на странице C++) print(out.stdout) # "запуск: … мс\nобработка: … мс\nкод: ……" — по строке «код:» на код # (на не русской системе — по-английски; язык задаёт LIBSCANNER_LANG=ru|en) # известный набор файлов — один процесс, строки через TAB «путь, код, мс, текст» out = subprocess.run(["dmr", "--batch", "список.txt"], capture_output=True, text=True, encoding="utf-8") ``` Для потока кадров одного конвейера — режим `--serve`: один живой процесс, снимки приходят по одному, по строке в `stdin`, конец каждого ответа отмечен строкой `#END#`. Ниже — готовая обёртка, собирающая это в одном месте: ```python import subprocess import threading from dataclasses import dataclass @dataclass class DmrCode: rc: int elapsed_ms: float text: str class DmrServe: """Обёртка над "dmr --serve": один живой процесс, один сканер. Не для параллельного вызова scan() из нескольких потоков одновременно — оболочка обрабатывает пути по одному; без своей блокировки ответы перепутаются между вызывающими. Для параллельной ОБРАБОТКИ кадров нужно несколько экземпляров DmrServe (несколько процессов dmr). """ def __init__(self, dmr_path: str = "dmr", extra_args: list[str] | None = None): self._dmr_path = dmr_path self._extra_args = extra_args or [] self._lock = threading.Lock() self._proc: subprocess.Popen | None = None self._start() def _start(self) -> None: self._proc = subprocess.Popen( [self._dmr_path, "--serve", *self._extra_args], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8", bufsize=1, ) def scan(self, image_path: str) -> list[DmrCode]: with self._lock: if self._proc.poll() is not None: # Процесс сам завершился — например, кончилась лицензия # (Unlicensed останавливает --serve) или его убили снаружи. raise RuntimeError(f"dmr --serve не запущен (код {self._proc.returncode})") self._proc.stdin.write(image_path + "\n") self._proc.stdin.flush() rows: list[DmrCode] = [] while True: line = self._proc.stdout.readline() if not line: raise RuntimeError("dmr --serve закрыл вывод") line = line.rstrip("\n") if line == "#END#": break _path, rc, ms, text = line.split("\t") rows.append(DmrCode(rc=int(rc), elapsed_ms=float(ms), text=text)) return rows def close(self) -> None: with self._lock: if self._proc and self._proc.poll() is None: self._proc.stdin.close() self._proc.wait(timeout=5) def __enter__(self) -> "DmrServe": return self def __exit__(self, *exc) -> None: self.close() # использование: with DmrServe() as reader: for code in reader.scan("кадр.jpg"): if code.rc == 0: print(code.text) ``` > **Кириллица в пути на Windows.** Путь к снимку в строке для `--serve` — латиницей или ASCII-safe: для кириллицы на Windows нужна перекодировка под ANSI-кодовую страницу процесса. В самом пакете (`dmr.Image`) эта перекодировка сделана автоматически. Лицензия тем же путём — подкоманды `--license ... --json` отдают тот же JSON, что и `dmr.license`: ```python import json, subprocess out = subprocess.run( ["dmr", "--license", "status", "--json"], capture_output=True, text=True, encoding="utf-8", ) status = json.loads(out.stdout) if not status["can_scan"]: print(status["message"]) ``` --- # Создание DataMatrix 1.0 — C++ и C ABI > Генерация символов DataMatrix ECC 200 по ГОСТ Р ИСО/МЭК 16022 с проверкой GS1 и профиля «Честного знака» — PNG и JPEG для печати маркировки. Страница на сайте: https://libscanner.com/docs/datamatrix-generator/1.0 ## Обзор Библиотека строит символы **DataMatrix ECC 200** по ГОСТ Р ИСО/МЭК 16022 (ISO/IEC 16022) и отдаёт их картинкой PNG или JPEG — в память или в файл. Поддерживаются все 24 квадратных размера, от 10×10 до 144×144, шесть схем кодирования (ASCII, C40, Text, X12, EDIFACT, Base256) и опция GS1 с проверкой строки по GS1 General Specifications и профилю «Честного знака». Каждый собранный символ библиотека читает обратно сама (самопроверка): рамки, кодовые слова, синдромы Рида–Соломона, разбор схем. Символ, который не прочитался, наружу не отдаётся. Публичный интерфейс — на C++20, заголовки в `include/dmgen/`. Публичный API не бросает исключений: всё, что может не получиться, возвращает `Result`. Для других языков есть плоский C ABI (`dmgen_c`) — годится для P/Invoke, JNA, cgo и ctypes. > **Платформы.** Windows x86-64 и Linux x86-64, ARM64 и ARMv7 — библиотека собирается и проверяется в том числе для одноплатных компьютеров вроде Raspberry Pi. Сторонний код встроен в поставку, внешних зависимостей нет. ## Подключение Библиотека приходит уже собранной — собирается только ваша программа. Подключить её можно двумя способами: **статически** — C++ API из `libdmgen.a` входит в вашу программу целиком, или **динамически** — через библиотеку `dmgen_c` с плоским интерфейсом на C (C ABI). Состав поставки: **Windows:** ``` include\dmgen\{Dmgen,Encoder,Gs1,Image,License,Options,Result,Symbol,Version}.h C++ API include\dmgen\dmgen_c.h C ABI lib\libdmgen.a статическая библиотека C++ lib\pkgconfig\dmgen.pc bin\dmgen_c.dll библиотека C ABI lib\dmgen_c.dll.a её библиотека импорта (MinGW) bin\dmgen.exe утилита командной строки share\doc\dmgen\licenses\ лицензии сторонних библиотек ``` **Linux:** ``` include/dmgen/{Dmgen,Encoder,Gs1,Image,License,Options,Result,Symbol,Version}.h C++ API include/dmgen/dmgen_c.h C ABI lib/libdmgen.a статическая библиотека C++ lib/pkgconfig/dmgen.pc lib/libdmgen_c.so -> libdmgen_c.so.1 -> libdmgen_c.so.1.0.3 библиотека C ABI bin/dmgen утилита командной строки share/doc/dmgen/licenses/ лицензии сторонних библиотек ``` Поставки `linux` (x86-64), `linux-arm64` и `linux-armhf` одинаковы по составу. | Ваша программа | Что брать | | --- | --- | | C++, собирается MinGW-w64 GCC (Windows) или GCC (Linux) | Статическая линковка — C++ API, `libdmgen.a` | | MSVC или другой компилятор; другой язык (C, C#, Java, Go…); обновление библиотеки без пересборки программы | Динамическая линковка — C ABI, `dmgen_c` | | Python | Колесо из «Загрузок» — [документация Python](https://libscanner.com/docs/datamatrix-generator/1.0/python.md) | ### Статическая линковка — C++ API `libdmgen.a` входит в вашу программу целиком: рядом с ней ничего класть не нужно, кроме файла ключа `dmgen.key` (см. «Лицензия»). Нужен компилятор **C++20** с той же стандартной библиотекой C++, что и у поставки (той же или более новой версии). **Windows:** `libdmgen.a` собрана MinGW-w64 GCC — MSVC её не слинкует, нужен тот же тулчейн: [MSYS2](https://www.msys2.org/), консоль **MINGW64** (не UCRT64), пакеты `pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkgconf`. Из MSVC — динамически, через dmgen_c. Строка линковки — из `dmgen.pc`; пути в нём считаются от места самого файла, так что каталог распаковки может быть любым: ```bash $env:PKG_CONFIG_PATH = "C:\куда\распакован\lib\pkgconfig" pkg-config --static --cflags --libs dmgen ``` Системные `winhttp`, `crypt32`, `shell32` и `advapi32` (проверка лицензии) уже прописаны в пакете. **Linux:** Поставка собрана GCC 12 (Debian 12): нужен GCC 12 или новее. Строка линковки — из `dmgen.pc`, каталог распаковки может быть любым: ```bash PKG_CONFIG_PATH=/куда/распакован/lib/pkgconfig pkg-config --static --cflags --libs dmgen ``` Нужны `-pthread -ldl` — они уже в пакете. В meson — зависимость по имени пакета: ```cpp dmgen_dep = dependency('dmgen', required: true, static: true) executable('мояпрограмма', 'main.cpp', dependencies: [dmgen_dep], link_args: ['-static']) # Windows: рантайм MinGW внутрь программы ``` Под Windows `-static` вкомпоновывает в программу и рантайм компилятора: без него она потребует рядом `libstdc++-6.dll`, `libgcc_s_seh-1.dll` и `libwinpthread-1.dll` из MSYS2. Под Linux он не нужен. ### Динамическая линковка — C ABI `dmgen_c` — та же библиотека за плоским интерфейсом на C (заголовок `dmgen/dmgen_c.h`, подробно — C ABI). Рантайм компилятора вкомпонован в неё статически, поэтому она не зависит ни от компилятора вызывающего, ни от его стандартной библиотеки C++. Отдельного `.pc` для неё нет — пути указываются напрямую. **Windows:** **MinGW-w64 GCC** — через библиотеку импорта `lib\dmgen_c.dll.a`: ```bash gcc main.c -o мояпрограмма.exe -I C:/куда/распакован/include -L C:/куда/распакован/lib -ldmgen_c ``` **MSVC** — файла `.lib` в поставке нет, его делают из самой DLL. В консоли MSYS2 — описание экспорта: ```bash gendef dmgen_c.dll # пакет mingw-w64-x86_64-tools; пишет dmgen_c.def ``` затем в Developer Command Prompt for VS (x64) — библиотека импорта и сборка: ```bash lib /def:dmgen_c.def /machine:x64 /out:dmgen_c.lib cl /I C:\куда\распакован\include main.c dmgen_c.lib ``` При запуске `dmgen_c.dll` должна лежать рядом с вашим `.exe` (или в каталоге из `PATH`), а файл ключа `dmgen.key` — рядом с `dmgen_c.dll`. **Linux:** ```bash cc main.c -o мояпрограмма -I/куда/распакован/include -L/куда/распакован/lib -ldmgen_c ``` При запуске загрузчик ищет `libdmgen_c.so.1`: либо через `LD_LIBRARY_PATH`, либо положите её рядом с программой и пропишите путь поиска при линковке: ```bash LD_LIBRARY_PATH=/куда/распакован/lib ./мояпрограмма # или: библиотека рядом с программой cc main.c -o мояпрограмма -I/куда/распакован/include -L/куда/распакован/lib -ldmgen_c -Wl,-rpath,'$ORIGIN' cp -a /куда/распакован/lib/libdmgen_c.so.1* . ``` Файл ключа `dmgen.key` — рядом с `libdmgen_c.so`. Колесо Python — обёртка как раз над `dmgen_c`, библиотека лежит внутри пакета: [документация Python](https://libscanner.com/docs/datamatrix-generator/1.0/python.md). ## Быстрый старт Код «Честного знака» → файл PNG. Всё, что нужно, — один заголовок: ```cpp #include #include int main() { dmgen::EncodeOptions o; o.gs1 = dmgen::Gs1Mode::ChestnyZnak; // проверка и FNC1 первым словом auto sym = dmgen::encode("(01)04601234567893(21)5Abc12Xyz!-(93)dGVz", o); if (!sym) { std::cerr << sym.error().message << "\n"; // на языке сообщений («Ошибки») return 1; } dmgen::RenderOptions r; // 8 px на модуль, свободная зона 2 auto saved = dmgen::save(dmgen::render(sym->modules, r), "code.png", dmgen::ImageFormat::Png); return saved ? 0 : 1; } ``` Картинка в память — `dmgen::toPng(image)` или `dmgen::toJpeg(image)`, обе возвращают `Result>`. ## Кодирование ```cpp Result encode(std::string_view data, const EncodeOptions& options = {}); ``` Байты 128–255 кодируются через Upper Shift. Текст не перекодируется: строку UTF-8 символ хранит как UTF-8. ### EncodeOptions | Поле | По умолчанию | Смысл | | --- | --- | --- | | `side` | `0` | Сторона символа: 0 — наименьший подходящий, иначе 10, 12, 14, 16, 18, 20, 22, 24, 26, 32, 36, 40, 44, 48, 52, 64, 72, 80, 88, 96, 104, 120, 132, 144. Не из списка — `InvalidArgument`, не помещается — `SizeTooSmall`. | | `encodation` | `Auto` | `Auto` — схемы выбирает предпросмотр приложения P ГОСТ; `Minimal` — кратчайший поток (дольше считается); либо одна схема: `Ascii`, `C40`, `Text`, `X12`, `Edifact`, `Base256`. | | `gs1` | `Off` | `Off`, `Gs1`, `ChestnyZnak`. Строка проверяется до кодирования; нарушение — `Gs1Invalid`. | | `separator` | `Gs29` | Разделитель групп: `Gs29` — байт GS, как в «Честном знаке»; `Fnc1` — кодовое слово 232 (только в режиме `Gs1`). | | `gs1Input` | `Auto` | `Raw` — разделитель байтом 0x1D или записью ``, `{GS}`, `\x1D`; `Bracketed` — `(01)…(21)…`, разделители расставятся сами; `Auto` — по «(» в начале. | | `verify` | `true` | Самопроверка: собранный символ читается обратно и сравнивается со входом. | ### Symbol `info` — выбранный размер (`SymbolInfo`), `modules` — матрица модулей без свободной зоны (`BitMatrix`, 1 — тёмный), `codewords` — кодовые слова в порядке размещения, `segments` — какая схема на каком участке, `text` — строка, которую вернёт ридер. Таблица размеров — `dmgen::squareSizes()`. ## GS1 и «Честный знак» ```cpp auto rep = dmgen::gs1::validate("(01)04601234567894(21)ABC", dmgen::Gs1Mode::ChestnyZnak); for (const auto& i : rep.issues) std::cout << (i.warning ? "предупреждение: " : "ошибка: ") << i.message << "\n"; // ошибка: AI (01) GTIN: контрольная цифра 4, должна быть 3 // ошибка: «Честный знак»: нет кода проверки — нужен 93 или пара 91 и 92 ``` Проверяются известность AI, длины и наборы знаков, контрольные цифры GTIN/SSCC/GLN, даты, обязательные разделители после AI без предопределённой длины и повторы AI. Профиль «Честного знака»: 01 первым, 21 вторым, код проверки 93 (4 знака) или 91 (4) + 92 (44 или 88). `Report::normalized` — строка ровно в том виде, в каком её вернёт ридер (с байтом GS). Проверка GS1 лицензии не требует. ## Картинка | Функция | Что делает | | --- | --- | | `render(modules, RenderOptions)` | Растр Gray8: `modulePx` (8), `quietZone` (2; ГОСТ — не меньше 1), `invert`, `dark`/`light`, `dpi`. | | `RenderOptions::fromMm(мм, dpi)` | Модуль в миллиметрах при разрешении печати; DPI пишется в файл (pHYs, JFIF). | | `RenderOptions::jpegFriendly()` | Модуль кратен 8 px: блоки 8×8 JPEG однотонные, звона на краях нет. | | `toPng`, `toJpeg`, `save` | PNG (рекомендуется) или JPEG в память или в файл; путь — `std::filesystem::path`, Юникод на Windows работает. Качество JPEG ниже 90 для штрихкода не рекомендуется. | ```cpp auto r = dmgen::RenderOptions::fromMm(0.5, 300); // модуль 0,5 мм при 300 dpi dmgen::save(dmgen::render(sym->modules, r), "label.png", dmgen::ImageFormat::Png); ``` ## Ошибки `Result` — значение или `Error` с полями `code` и `message` (UTF-8, на языке сообщений, см. ниже). Кому удобнее исключения — `value()` бросает `std::runtime_error` с текстом ошибки. **Язык сообщений.** Сообщения — на русском или английском: переменная окружения `LIBSCANNER_LANG` (`ru` или `en`), иначе язык системы — русская система даёт русский, любая другая (и локаль `C` в контейнере) — английский. Из кода — `dmgen::setMessageLanguage("en")` (`dmgen/Language.h`, на весь процесс); `dmgen::messageLanguage()` возвращает текущий. Утилита принимает `--lang ru|en`. Коды ошибок не переводятся. | ErrorCode | Смысл | | --- | --- | | `InvalidArgument` | Неверная опция: размер не из таблицы, пустые данные… | | `DataTooLong` | Данные не помещаются даже в 144×144 | | `SizeTooSmall` | Данные не помещаются в явно заданный размер | | `UnencodableChar` | Знак нельзя закодировать выбранной схемой | | `Gs1Invalid` | Строка не прошла проверку GS1 (подробности — `gs1::validate`) | | `IoError` | Не удалось записать файл или изображение | | `InternalVerifyFailed` | Самопроверка не прочитала собранный символ — ошибка dmgen | | `Unlicensed` | Пробный период кончился, лицензии нет | ## C ABI Библиотека `dmgen_c.dll` / `libdmgen_c.so`, заголовок `dmgen_c.h`, экспортируются только `dmgen_*`. Только C-типы фиксированного размера, опции со `struct_size`, память освобождает `dmgen_free`; есть вариант с буфером вызывающего — `dmgen_make_image_into`. ```cpp dmgen_options o; dmgen_error e; uint8_t* png; size_t len; dmgen_options_init(&o); o.gs1 = DMGEN_GS1_CHESTNY_ZNAK; if (dmgen_make_image((const uint8_t*)data, n, &o, DMGEN_FORMAT_PNG, &png, &len, &e) == DMGEN_OK) { /* … */ dmgen_free(png); } ``` `dmgen_options_init` заполняет умолчания и сам ставит `struct_size` — вызовите её первой, потом меняйте поля. В строковом литерале C разделитель — отдельным литералом: `"…LM" "\x1D" "93ABCD"`, иначе в `"\x1D93"` компилятор заберёт в одну escape-последовательность все шестнадцатеричные цифры. При ошибке в `error` — код и сообщение в UTF-8 на языке сообщений («Ошибки»). В Linux у библиотеки SONAME `libdmgen_c.so.1` (файл `libdmgen_c.so.1.0.3`); файла `.pc` для `dmgen_c` нет — укажите `-I…/include -L…/lib -ldmgen_c` сами. Файл ключа `dmgen.key` — рядом с библиотекой или рядом с программой (раздел «Лицензия»). ### Справочник Все объявления `dmgen_c.h` (ABI 1), с комментариями самого заголовка (они по-английски): ```cpp #define DMGEN_ABI_VERSION 1 /* Return codes: 0 is success, the rest match dmgen::ErrorCode. */ typedef int32_t dmgen_status; enum { DMGEN_OK = 0, DMGEN_E_INVALID_ARGUMENT = 1, DMGEN_E_DATA_TOO_LONG = 2, DMGEN_E_SIZE_TOO_SMALL = 3, DMGEN_E_UNENCODABLE_CHAR = 4, DMGEN_E_GS1_INVALID = 5, DMGEN_E_IO = 6, DMGEN_E_INTERNAL_VERIFY = 7, DMGEN_E_UNLICENSED = 8, /* trial period over and no license */ DMGEN_E_BUFFER_TOO_SMALL = 100 /* dmgen_make_image_into only */ }; enum { DMGEN_FORMAT_PNG = 0, DMGEN_FORMAT_JPEG = 1 }; enum { DMGEN_GS1_OFF = 0, DMGEN_GS1_GS1 = 1, DMGEN_GS1_CHESTNY_ZNAK = 2 }; enum { DMGEN_SEPARATOR_GS = 0, DMGEN_SEPARATOR_FNC1 = 1 }; enum { DMGEN_INPUT_AUTO = 0, DMGEN_INPUT_RAW = 1, DMGEN_INPUT_BRACKETED = 2 }; enum { DMGEN_SCHEME_AUTO = 0, DMGEN_SCHEME_ASCII, DMGEN_SCHEME_C40, DMGEN_SCHEME_TEXT, DMGEN_SCHEME_X12, DMGEN_SCHEME_EDIFACT, DMGEN_SCHEME_BASE256, DMGEN_SCHEME_MINIMAL }; /* Encoding and rendering options. Initialize with dmgen_options_init. */ typedef struct dmgen_options { uint32_t struct_size; /* sizeof(dmgen_options) as compiled by the caller */ int32_t gs1; /* DMGEN_GS1_* */ int32_t separator; /* DMGEN_SEPARATOR_* */ int32_t gs1_input; /* DMGEN_INPUT_* */ int32_t scheme; /* DMGEN_SCHEME_* */ int32_t side; /* 0 — smallest that fits, otherwise a standard side 10…144 */ int32_t verify; /* 1 — self-check (default) */ int32_t module_px; /* pixels per module, default 8 */ int32_t quiet_zone; /* modules, default 2 */ int32_t invert; /* 0/1 */ int32_t dark; /* 0…255, default 0 */ int32_t light; /* 0…255, default 255 */ int32_t jpeg_quality; /* 1…100, default 100 */ int32_t reserved; /* alignment, 0 */ double dpi; /* 0 — not written to the file */ } dmgen_options; typedef struct dmgen_error { int32_t code; /* dmgen_status */ char message[512]; /* UTF-8, null-terminated */ } dmgen_error; typedef struct dmgen_gs1_element { char ai[8]; /* null-terminated */ char* value; /* UTF-8, null-terminated */ size_t value_len; } dmgen_gs1_element; typedef struct dmgen_gs1_issue { int32_t code; /* dmgen::gs1::IssueCode */ int32_t warning; /* 1 — warning */ int32_t position; char ai[8]; char message[256]; } dmgen_gs1_issue; typedef struct dmgen_gs1_report { int32_t ok; int32_t element_count; dmgen_gs1_element* elements; int32_t issue_count; dmgen_gs1_issue* issues; char* normalized; /* with 0x1D bytes, as a reader returns it */ size_t normalized_len; } dmgen_gs1_report; uint32_t dmgen_abi_version(void); const char* dmgen_version_string(void); void dmgen_options_init(dmgen_options* options); /* Data → PNG or JPEG in memory. Free *out with dmgen_free. */ dmgen_status dmgen_make_image(const uint8_t* data, size_t len, const dmgen_options* options, int32_t format, uint8_t** out, size_t* out_len, dmgen_error* error); /* Same, into a caller-provided buffer. If cap is too small, returns * DMGEN_E_BUFFER_TOO_SMALL and sets *needed to the required size; buf may be * NULL when cap = 0. */ dmgen_status dmgen_make_image_into(const uint8_t* data, size_t len, const dmgen_options* options, int32_t format, uint8_t* buf, size_t cap, size_t* needed, dmgen_error* error); /* Data → file (UTF-8 path; Unicode works on Windows). */ dmgen_status dmgen_save_image(const uint8_t* data, size_t len, const dmgen_options* options, int32_t format, const char* path_utf8, dmgen_error* error); /* Data → side×side module matrix (1 is dark, rows top to bottom). * Free *modules with dmgen_free. */ dmgen_status dmgen_encode_matrix(const uint8_t* data, size_t len, const dmgen_options* options, uint8_t** modules, int32_t* side, dmgen_error* error); /* GS1 validation. Free *report with dmgen_gs1_report_free. The return code * tells whether the call itself succeeded; whether the string is valid is the * report's ok field. */ dmgen_status dmgen_gs1_validate(const uint8_t* data, size_t len, int32_t mode, int32_t input, dmgen_gs1_report** report, dmgen_error* error); void dmgen_gs1_report_free(dmgen_gs1_report* report); /* Sides of the 24 standard square sizes in increasing order; returns their * count (24) and writes at most cap values. */ int32_t dmgen_symbol_sizes(int32_t* sides, int32_t cap); void dmgen_free(void* p); /* ── License (dmgen/License.h) ────────────────────────────────────────────── * * Symbol-producing functions return DMGEN_E_UNLICENSED while there is neither * a trial period nor a license. The primary activation method is a dmgen.key * file containing the key, placed next to dmgen_c.dll / libdmgen_c.so: no * calls are needed. * * Responses are UTF-8 JSON, the same as "dmgen --license ... --json": * status {"state","can_encode","valid_until","refresh_after", * "offline_until","days_left","license_id","activation_id", * "offline_mode","clock_rollback","message"} * operation {"ok","error","message","status":{...status...}} * state is one of trial, active, refresh_due, trial_expired, expired, * network_required, wrong_machine, invalid. Free the string with dmgen_free; * NULL means an internal error or NULL in a required argument. Paths are * UTF-8. Network functions wait for the server at most timeout_ms. */ /* Call before the first network call and the first encoding; optional. * server_url, app_version — NULL or "" for the default; timeout_ms <= 0 — * 15000; auto_check — daily background check with the server (1 — on). */ void dmgen_license_configure(const char* server_url, const char* app_version, int32_t timeout_ms, int32_t auto_check); char* dmgen_license_status_json(void); char* dmgen_license_activate_json(const char* key); char* dmgen_license_refresh_json(void); char* dmgen_license_deactivate_json(const char* reason); /* reason may be NULL */ char* dmgen_license_deactivate_to_file_json(const char* path_utf8, const char* reason); char* dmgen_license_write_activation_request_json(const char* path_utf8); char* dmgen_license_import_license_file_json(const char* path_utf8); /* {"fingerprint":""} */ char* dmgen_license_fingerprint_json(void); ``` ## Версия библиотеки `DMGEN_VERSION_STRING` — версия заголовков, `dmgen::versionString()` — версия, с которой собрана библиотека. По расхождению видно, что заголовки и `.a` от разных сборок. ## Лицензия Библиотека кодирует, пока идёт пробный период (месяц с первого запуска на машине, без сети и регистрации) или действует лицензия. Иначе `encode()` отвечает `ErrorCode::Unlicensed`. Проверка GS1 и рисование готовой матрицы лицензии не требуют. Лицензия привязана к оборудованию (материнская плата, процессор, системный диск), а не к установке ОС; пробный период и лицензия не зависят от библиотеки распознавания на той же машине. ```cpp #include dmgen::LicenseStatus s = dmgen::license::status(); if (!s.canEncode()) std::cerr << s.message; // «Пробный период закончился. Нужна лицензия» dmgen::license::activate("XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"); // онлайн dmgen::license::writeActivationRequest("request.json"); // офлайн: в личный кабинет … dmgen::license::importLicenseFile("license.lic"); // … и обратно dmgen::license::deactivate(); // переезд на другую машину ``` `deactivate()` сдаёт место распиской этой машины. Каждый ответ сервера лицензий привязан к своему запросу: библиотека отправляет случайное одноразовое число, сервер возвращает его в подписанном ответе, и записанный раньше ответ повторно не примется. Расписка тоже одноразовая — в ней номер из последнего полученного от сервера ответа, поэтому расписку из восстановленной копии старого состояния машины сервер не примет; секрет для её подписи сервер выдаёт только при первой активации машины. У лицензий с полной автономией (`full`) сдача места расходует лимит переносов; когда он исчерпан, перенос делает поддержка. Самостоятельно освободить место по лицензии можно не больше двух раз за 30 дней в режиме limited и не больше одного раза — в extended; дальше перенос делает поддержка. > **Основной способ — файл dmgen.key.** Положите текстовый файл `dmgen.key` с ключом рядом с `dmgen_c.dll` / `libdmgen_c.so` или с программой. При первом обращении к библиотеке ключ уйдёт на сервер — один раз; дальше лицензия раз в сутки сама сверяется с сервером в фоновом потоке, кодирование сети не ждёт никогда. ### Состояния | LicenseState | Кодирует | Смысл | | --- | --- | --- | | `Trial` | да | Идёт пробный период | | `Active` | да | Лицензия действует | | `RefreshDue` | да | Давно не было связи с сервером; библиотека сама пробует раз в час | | `TrialExpired` | нет | Пробный период кончился — нужна лицензия | | `Expired` | нет | Оплаченный срок кончился; продление в кабинете подхватится само | | `NetworkRequired` | нет | Срок работы без связи вышел — нужна связь с сервером | | `WrongMachine` | нет | Лицензия выдана другой машине | | `Invalid` | нет | Запись лицензии не проходит проверку подписи | Режим автономии задаёт тариф: `limited` — 7 дней без связи и ещё 7 после предупреждения, `extended` — 30 + 90, `full` — в сеть не нужно никогда. ## Командная строка ```bash dmgen "0104601234567893215Abc12Xyz!-93dGVz" --gs1=cz -o code.png dmgen --check-gs1 --gs1=cz "(01)04601234567893(21)5Abc12Xyz!-(93)dGVz" dmgen --all-sizes "Hello" --out-dir sizes/ dmgen --batch codes.txt --out-dir out/ --gs1=cz --mm=0.5 --dpi=300 dmgen --license status # состояние, срок, автономия dmgen --license activate <ключ> dmgen --license deactivate # переезд на другую машину ``` Ключи: `--size=`, `--scheme=`, `--separator=`, `--input=`, `--px=`, `--quiet=`, `--mm=` с `--dpi=`, `--invert`, `--jpeg-friendly`, `--quality=`. Без лицензии утилита завершается с кодом 4. --- # Создание DataMatrix 1.0 — Python > Генерация символов DataMatrix ECC 200 по ГОСТ Р ИСО/МЭК 16022 с проверкой GS1 и профиля «Честного знака» — PNG и JPEG для печати маркировки. Страница на сайте: https://libscanner.com/docs/datamatrix-generator/1.0?lang=python ## Обзор Пакет `dmgen` строит символы **DataMatrix ECC 200** по ГОСТ Р ИСО/МЭК 16022 и отдаёт их картинкой PNG или JPEG — байтами или файлом. Все 24 квадратных размера, шесть схем кодирования, проверка строки по GS1 и профилю «Честного знака». Каждый символ библиотека читает обратно сама, прежде чем отдать. Колесо платформенное: собранная библиотека лежит внутри пакета. Колёса есть для Windows x86-64 и Linux x86-64, ARM64 и ARMv7 (в том числе Raspberry Pi). Python 3.8 и новее, зависимостей нет. ## Установка ```bash pip install libscanner-dmgen ``` Пакет на PyPI называется `libscanner-dmgen`, импортируется как `dmgen`. pip сам выберет колесо своей платформы: Windows x86-64, Linux x86-64, ARM64 и ARMv7 (Raspberry Pi OS 64 и 32 бит). Колёса для Linux собраны под glibc 2.36 и новее (Debian 12, Ubuntu 23.04+, Raspberry Pi OS Bookworm). Вместе с пакетом ставится команда `dmgen`. Без доступа к PyPI — то же колесо из раздела «Загрузки» личного кабинета: **Windows:** ```bash pip install .\libscanner_dmgen-1.0.3-py3-none-win_amd64.whl ``` **Linux:** ```bash pip install ./libscanner_dmgen-1.0.3-py3-none-manylinux_2_36_x86_64.whl # x86-64 pip install ./libscanner_dmgen-1.0.3-py3-none-manylinux_2_36_aarch64.whl # ARM64 pip install ./libscanner_dmgen-1.0.3-py3-none-manylinux_2_36_armv7l.whl # ARMv7 ``` Библиотека лежит внутри пакета, в `dmgen/_lib/`, и грузится только оттуда, по полному пути; системного поиска нет. Переменная `DMGEN_LIB` (полный путь к файлу) нужна только пакету, собранному из исходников, где `_lib` пуст. ## Быстрый старт ```python import dmgen # код «Честного знака» → файл; формат — по расширению dmgen.save("(01)04601234567893(21)5Abc12Xyz!-(93)dGVz", "code.png", gs1="cz") # картинка в память png = dmgen.make_image("Hello", module_px=8) jpg = dmgen.make_image("Hello", fmt="jpeg", quality=100) ``` ## Функции | Функция | Что делает | | --- | --- | | `make_image(data, *, fmt="png", **kw)` | Данные → PNG или JPEG, `bytes` | | `save(data, path, *, fmt=None, **kw)` | Данные → файл; без `fmt` формат по расширению (.jpg/.jpeg — JPEG, иначе PNG) | | `encode_matrix(data, **kw)` | Матрица модулей: список строк сверху вниз, 1 — тёмный | | `validate_gs1(data, mode="gs1", gs1_input="auto")` | Проверка строки, `Report`. `mode`: `"gs1"` или `"cz"` (то же — `"chestny_znak"`); `None`/`"off"` здесь означают `"gs1"`. `gs1_input`: `"auto"`, `"raw"` или `"bracketed"` | | `version()` | Версия библиотеки `dmgen_c` | | `SIZES` | Все 24 стороны символа, 10…144 | `data` — `str` (кодируется в UTF-8), `bytes`, `bytearray` или `memoryview`. `fmt` — `"png"` или `"jpeg"` (также `"jpg"`), регистр не важен. Значения параметров — строки в любом регистре; неизвестное значение — `ValueError` до кодирования. ## Параметры | Ключ | По умолчанию | Смысл | | --- | --- | --- | | `gs1` | `None` | `None` (или `"off"`) — обычный DataMatrix, байты кодируются как есть; `"gs1"` — проверка GS1 и FNC1 первым словом; `"cz"` (то же — `"chestny_znak"`) — плюс профиль «Честного знака», разделитель только GS | | `separator` | `"gs"` | Разделитель групп: байт GS или `"fnc1"` (только с `gs1="gs1"`) | | `gs1_input` | `"auto"` | `"raw"` — разделитель байтом `\x1d` или одной из текстовых форм ``, `{GS}`, `\x1D`, `\u001D` (в любом регистре); ведущие `]d2` или GS отбрасываются с предупреждением. `"bracketed"` — `(01)…(21)…`. `"auto"` — скобочная запись, если строка начинается с `(`, иначе raw. Текстовые формы GS распознаются только при включённом `gs1`; без него `` кодируется как текст | | `scheme` | `"auto"` | `"minimal"` — кратчайший поток, либо одна схема: `ascii`, `c40`, `text`, `x12`, `edifact`, `base256` | | `side` | `0` | Сторона символа из `SIZES`; 0 — наименьшая подходящая | | `verify` | `True` | Самопроверка чтением обратно | | `module_px` | `8` | Пикселей на модуль, 1…1000 (меньше 1 считается 1) | | `quiet_zone` | `2` | Свободная зона, модулей, 0…100 (отрицательная считается 0); ГОСТ — не меньше 1. Картинка целиком — не больше 32768 пикселей по стороне, больше — `EncodeError` | | `invert`, `dark`, `light` | `False, 0, 255` | Инверсия и яркости модулей | | `dpi` | `0` | Разрешение в метаданных файла; 0 — не писать. На размер картинки в пикселях не влияет | | `quality` | `100` | Качество JPEG; ниже 90 для штрихкода не рекомендуется | **Размер в миллиметрах.** Ключа в миллиметрах в пакете нет (в C++ есть `RenderOptions::fromMm`, в командной строке — `--mm`): модуль под разрешение принтера считается вручную, то же разрешение пишется в файл: ```python module_mm, dpi = 0.5, 300 dmgen.save(code, "code.png", gs1="cz", module_px=round(module_mm / 25.4 * dpi), dpi=dpi) # 6 пикселей ``` Для картинки, которую читают из файла или с экрана, хватает значений по умолчанию (8 пикселей на модуль, свободная зона 2 модуля): ридер dmr читает коды от двух пикселей на модуль. Для JPEG модуль — кратный 8 пикселям, качество — не ниже 90. ## GS1 и «Честный знак» ```python rep = dmgen.validate_gs1("(01)04601234567894(21)ABC", mode="cz") print(rep.ok) # False for i in rep.issues: print("предупреждение:" if i.warning else "ошибка:", i.message) # ошибка: AI (01) GTIN: контрольная цифра 4, должна быть 3 # ошибка: «Честный знак»: нет кода проверки — нужен 93 или пара 91 и 92 ``` `Report`: `ok`, `elements` (пары `ai`/`value`), `issues`, `normalized` — строка с `\x1d`, как её вернёт ридер. Проверка лицензии не требует. `ok` — `True`, если нет ошибок; предупреждения (`issue.warning`) кодированию не мешают — `save(..., gs1="cz")` пишет код и только сообщает о них. Правила GS1 (известные AI, длины, наборы символов, контрольная цифра GTIN, повтор AI) действуют в обоих режимах; профиль «Честного знака» добавляет: - первый элемент — AI 01 (GTIN, 14 цифр с верной контрольной цифрой), второй — AI 21 (серийный номер, 1–20 символов набора GS1 82) — иначе ошибка; - серийный номер необычной длины — *предупреждение*, не ошибка: типичные длины 6, 7, 8, 11, 12, 13 и 20; - после AI 21 обязателен код проверки: AI 93 ровно из 4 символов или пара AI 91 (4 символа) и AI 92 (44 или 88 символов), 92 после 91; - AI 10, 11, 17, 240, 7003, 8005 и 310x допускаются без замечаний; любой другой AI — предупреждение. ## Ошибки Все исключения — наследники `dmgen.DmgenError` (сам он — наследник `RuntimeError`) с полями `code` — `int`, код ошибки C ABI (см. страницу C++, «Ошибки»), — и `message` (на языке сообщений: `LIBSCANNER_LANG` = `ru`/`en`, иначе язык системы). `Gs1Error` (код 5, строка не прошла проверку GS1/«Честного знака»; добавляет `report`), `UnlicensedError` (код 8) и `EncodeError` (все остальные коды) наследуют прямо от `DmgenError`. Неизвестное значение параметра или число вне диапазона C `int` — `ValueError`. ```python try: dmgen.save(code, "code.png", gs1="cz") except dmgen.Gs1Error as e: for i in e.report.issues: print(i.message) except dmgen.UnlicensedError as e: print(e.message) # пробный период кончился, лицензии нет except dmgen.EncodeError as e: print(e.message) # размер, недопустимый знак, запись файла… ``` ## Лицензия Пакет кодирует, пока идёт пробный период (месяц с первого запуска на машине, без сети и регистрации) или действует лицензия; иначе — `UnlicensedError`. Лицензия привязана к оборудованию, а не к установке ОС. Основной способ активации — текстовый файл `dmgen.key` с ключом в каталоге `dmgen/_lib/` установленного пакета: при первом обращении ключ уйдёт на сервер, дальше лицензия раз в сутки сама сверяется с сервером в фоне. ```python import dmgen.license as lic s = lic.status() if not s["can_encode"]: print(s["message"]) lic.activate("XXXXX-XXXXX-XXXXX-XXXXX-XXXXX") # онлайн lic.write_activation_request("request.json") # офлайн: в личный кабинет … lic.import_license_file("license.lic") # … и обратно lic.deactivate() # переезд на другую машину ``` `deactivate()` сдаёт место распиской этой машины. Каждый ответ сервера лицензий привязан к своему запросу: пакет отправляет случайное одноразовое число, сервер возвращает его в подписанном ответе, и записанный раньше ответ повторно не примется. Расписка тоже одноразовая — в ней номер из последнего полученного от сервера ответа, поэтому расписку из восстановленной копии старого состояния машины сервер не примет; секрет для её подписи сервер выдаёт только при первой активации машины. У лицензий с полной автономией (`full`) сдача места расходует лимит переносов; когда он исчерпан, перенос делает поддержка. Самостоятельно освободить место по лицензии можно не больше двух раз за 30 дней в режиме limited и не больше одного раза — в extended; дальше перенос делает поддержка. Состояние — словарь: `state` (`trial`, `active`, `refresh_due`, `trial_expired`, `expired`, `network_required`, `wrong_machine`, `invalid`), `can_encode`, `valid_until`, `days_left`, `offline_mode`, `message` и др. Операции возвращают `{"ok", "error", "message", "status"}` — отказ не исключение, а `ok: False` с кодом. --- # Библиотека чтения машиночитаемых кодов 1.0 — C++ и C ABI > Один вызов читает на кадре DataMatrix, GS1-128 (SSCC), Code 128, EAN-13, EAN-8, UPC-A и UPC-E — маркировку «Честного знака», логистические этикетки и товарные штрихкоды. Страница на сайте: https://libscanner.com/docs/universal-reader/1.0 ## Обзор Библиотека одним вызовом читает на кадре коды разных символик: **DataMatrix (ECC200)** — маркировку «Честного знака», **GS1-128** — SSCC логистических этикеток, **Code 128** и товарные штрихкоды **EAN-13, EAN-8, UPC-A и UPC-E**. Точность и скорость чтения — вверху страницы; ниже — только устройство интерфейса. Библиотека статическая, публичный интерфейс — на C++20, восемь заголовков в `include/unir/`, плюс плоский интерфейс на C для других языков (`unir_c.h`, раздел C ABI). Кадр передаётся видом на чужой буфер (`unir::ImageView`) — без копирования, из уже имеющегося буфера камеры или снимка. ## Подключение Библиотека приходит уже собранной — собирать её не нужно, собирается только ваша программа. Подключить её можно двумя способами: **статически** — C++ API из `libunir.a` входит в вашу программу целиком, вместе с обоими движками, или **динамически** — через библиотеку `unir_c` с плоским интерфейсом на C (C ABI). Поставки есть для Windows x64 и Linux (x86-64, ARM64 и ARMv7), состав одинаковый: **Windows:** ``` include\unir\{Unir,Geometry,Image,License,Scanner,Settings,Symbology,Version}.h C++ API include\unir\unir_c.h C ABI lib\libunir.a статическая библиотека C++ (с движками dmr и ssccr) lib\pkgconfig\unir.pc bin\unir_c.dll библиотека C ABI lib\unir_c.dll.a её библиотека импорта (MinGW) lib\pkgconfig\unir_c.pc bin\unir.exe утилита командной строки share\doc\unir\licenses\ лицензии сторонних библиотек ``` **Linux:** ``` include/unir/{Unir,Geometry,Image,License,Scanner,Settings,Symbology,Version}.h C++ API include/unir/unir_c.h C ABI lib/libunir.a статическая библиотека C++ (с движками dmr и ssccr) lib/pkgconfig/unir.pc lib/libunir_c.so библиотека C ABI lib/pkgconfig/unir_c.pc bin/unir утилита командной строки share/doc/unir/licenses/ лицензии сторонних библиотек ``` В «Загрузках» личного кабинета на каждую платформу два архива: `unir-1.0.1-<платформа>.zip` (Windows) или `.tar.gz` (Linux) — полная поставка с библиотеками и заголовками, и `unir-1.0.1-<платформа>-cli` с тем же расширением — только утилита `unir`, для проверок без программирования. Платформы: `windows-x64`, `linux-x64`, `linux-arm64` и `linux-armhf` — это ARMv7, 32-битная ОС (например, Raspberry Pi с 32-битной системой). Архив распаковывается в каталог с тем же именем, например `unir-1.0.1-linux-x64/`. | Ваша программа | Что брать | | --- | --- | | C++, собирается MinGW-w64 GCC (Windows) или GCC (Linux) | Статическая линковка — C++ API, `libunir.a` | | MSVC или другой компилятор; другой язык (C, C#, Delphi, Go…); обновление библиотеки без пересборки программы | Динамическая линковка — C ABI, `unir_c` | | Python | `pip install libscanner-unir` или колесо из «Загрузок» — [документация Python](https://libscanner.com/docs/universal-reader/1.0/python.md) | ### Статическая линковка — C++ API `libunir.a` входит в вашу программу целиком: рядом с ней ничего класть не нужно, кроме файла ключа `unir.key` — он рядом с программой (раздел «Лицензия»). Нужен компилятор **C++20**. API на C++, в результате возвращаются `std::string` и `std::vector`, поэтому компилятор и его стандартная библиотека должны быть теми же, что и у поставки (той же или более новой версии). **Windows:** > **Нужен MinGW-w64 GCC, не MSVC.** Поставка для Windows собрана MinGW-w64 GCC — `libunir.a` в его формате и с его ABI. MSVC такой статический архив не слинкует; вашей программе нужен тот же тулчейн (например, MSYS2/MinGW или CLion с профилем MinGW). Из MSVC — динамически, через unir_c. > > Тулчейн бесплатный: [MSYS2](https://www.msys2.org/) — установщик с сайта, затем в его консоли **MINGW64** (поставка собрана именно в этом окружении, не в UCRT64): > > ```bash > pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkgconf > ``` Пути включения и строка линковки берутся из `unir.pc`. Путь к `lib\pkgconfig` распакованной поставки — через переменную окружения (PowerShell); пути в `.pc` считаются от места самого файла, так что каталог распаковки может быть любым: ```bash $env:PKG_CONFIG_PATH = "C:\куда\распакован\lib\pkgconfig" pkg-config --static --cflags --libs unir ``` Системные `winhttp`, `crypt32`, `shell32` и `advapi32` (их использует проверка лицензии) уже прописаны в `Libs.private` пакета — отдельно подключать не нужно. **Linux:** Поставка собрана GCC 12 (Debian 12, glibc 2.36): нужен GCC 12 или новее. Пути включения и строка линковки — из `unir.pc`; путь к `lib/pkgconfig` распакованной поставки — через переменную окружения, каталог распаковки может быть любым: ```bash PKG_CONFIG_PATH=/куда/распакован/lib/pkgconfig pkg-config --static --cflags --libs unir ``` Нужны `-lpthread -ldl` — они уже в пакете. `-std=c++20` файл `.pc` не добавляет — укажите сами (у GCC 12 по умолчанию C++17). Без pkg-config та же строка вручную: ```bash g++ -std=c++20 main.cpp -o мояпрограмма -I/куда/распакован/include /куда/распакован/lib/libunir.a -pthread -ldl ``` OpenSSL в линковке не нужен: для `https://` сервера лицензий библиотека сама загружает `libssl.so.3` (или `libssl.so.1.1`) при работе. > **Запрос к pkg-config должен быть статическим.** Поставка — одна статическая библиотека, а значит вызывающему нужны и те библиотеки, на которых стоит она сама. Без `--static` их в командной строке не окажется, и линковка упадёт на неразрешённых символах. В meson — зависимость по имени пакета: ```cpp unir_dep = dependency('unir', required: true, static: true) executable('мояпрограмма', 'main.cpp', dependencies: [unir_dep], link_args: ['-static']) # Windows: рантайм MinGW внутрь программы ``` Под Windows `-static` вкомпоновывает в программу и рантайм компилятора: без него она потребует рядом `libstdc++-6.dll`, `libgcc_s_seh-1.dll` и `libwinpthread-1.dll` из MSYS2. Под Linux он не нужен. Полный `meson.build` — в «Быстром старте». ### Динамическая линковка — C ABI `unir_c` — та же библиотека за плоским интерфейсом на C (заголовок `unir/unir_c.h`): функции `unir_*` и непрозрачные указатели, без типов C++. Рантайм компилятора вкомпонован в неё статически, поэтому она не зависит ни от компилятора вызывающего, ни от его стандартной библиотеки C++ — годится для MSVC, Clang и любого языка с вызовом функций C. Всё, что библиотека отдаёт (результаты, кадры, строки), освобождается её же функциями: `unir_scan_result_free`, `unir_image_free`, `unir_free_string`. Правила интерфейса и все функции — в разделе C ABI. **Windows:** **MinGW-w64 GCC** — через `unir_c.pc` и библиотеку импорта `lib\unir_c.dll.a`, без `--static`: ```bash $env:PKG_CONFIG_PATH = "C:\куда\распакован\lib\pkgconfig" pkg-config --cflags --libs unir_c ``` Это даёт `-I…\include -L…\lib -lunir_c`. **MSVC** — файла `.lib` в поставке нет, его делают из самой DLL. В консоли MSYS2 — описание экспорта: ```bash gendef unir_c.dll # пакет mingw-w64-x86_64-tools; пишет unir_c.def ``` затем в Developer Command Prompt for VS (x64) — библиотека импорта и сборка: ```bash lib /def:unir_c.def /machine:x64 /out:unir_c.lib cl /I C:\куда\распакован\include main.c unir_c.lib ``` При запуске `unir_c.dll` должна лежать рядом с вашим `.exe` (или в каталоге из `PATH`), а файл ключа `unir.key` — рядом с `unir_c.dll` или рядом с `.exe` (раздел «Лицензия»). **Linux:** ```bash PKG_CONFIG_PATH=/куда/распакован/lib/pkgconfig pkg-config --cflags --libs unir_c ``` Это даёт `-I…/include -L…/lib -lunir_c`. При запуске загрузчик должен найти `libunir_c.so`: либо через `LD_LIBRARY_PATH`, либо положите её рядом с программой и пропишите путь поиска при линковке: ```bash LD_LIBRARY_PATH=/куда/распакован/lib ./мояпрограмма # или: libunir_c.so рядом с программой cc main.c -o мояпрограмма $(pkg-config --cflags --libs unir_c) -Wl,-rpath,'$ORIGIN' cp /куда/распакован/lib/libunir_c.so . ``` Файл ключа `unir.key` — рядом с `libunir_c.so` или рядом с программой (раздел «Лицензия»). В meson — так же по имени пакета, без `static`: ```cpp unir_c_dep = dependency('unir_c', required: true) executable('мояпрограмма', 'main.c', dependencies: [unir_c_dep]) ``` ## Быстрый старт Минимальная программа: снимок с диска, чтение, вывод символики и текста. Всё, что нужно, — один заголовок: ```cpp #include #include int main() { unir::Scanner scanner; // поток кадров, все символики сборки, один код unir::LoadStatus load = unir::LoadStatus::Ok; const unir::ScanResult r = scanner.scanFile("кадр.jpg", &load); // загрузка и разбор под одним сроком if (load != unir::LoadStatus::Ok) { std::cerr << "не удалось прочитать файл\n"; return 1; } if (r.ok()) { for (const unir::Code& c : r.codes) std::cout << unir::nameOf(c.symbology) << ": " << c.text << "\n"; } else if (r.status == unir::Status::NotFound || r.status == unir::Status::NotDecoded) { std::cout << "код не прочитан\n"; // NotDecoded — тоже «нет кода» } else { std::cout << "кадр не прочитан, статус " << static_cast(r.status) << "\n"; } } ``` Собрать пример можно Meson. Рядом с `main.cpp` — распакованная поставка (`unir-1.0.1-windows-x64` или `unir-1.0.1-linux-x64`) и файл `meson.build`: **Windows:** ```cpp project('test_unir', 'cpp', version : '0.1.0', default_options : ['cpp_std=c++20', 'warning_level=3', 'buildtype=release']) cxx = meson.get_compiler('cpp') # Готовая сборка unir лежит рядом с проектом; зависимость описана вручную — # так pkg-config не нужен вовсе. unir_root = meson.current_source_dir() / 'unir-1.0.1-windows-x64' unir_lib = cxx.find_library('unir', dirs : unir_root / 'lib', static : true) # Libs.private из unir.pc — нужны при статической компоновке unir_private = [] foreach l : ['winhttp', 'crypt32', 'shell32', 'advapi32'] unir_private += cxx.find_library(l) endforeach unir_dep = declare_dependency( include_directories : include_directories('unir-1.0.1-windows-x64/include'), dependencies : [unir_lib] + unir_private) executable('test_unir', 'main.cpp', dependencies : unir_dep, cpp_args : ['-finput-charset=UTF-8', '-fexec-charset=UTF-8'], link_args : ['-static']) ``` Сборка — в консоли MSYS2 MINGW64: ```bash meson setup build meson compile -C build ``` То же через pkg-config: `.pc`-файлы поставки перемещаемые, поэтому вместо ручного описания хватит одной строки в `meson.build` — `unir_dep = dependency('unir', static : true)` — и пути к распакованному `lib/pkgconfig` при настройке: ```bash meson setup build --pkg-config-path="$PWD/unir-1.0.1-windows-x64/lib/pkgconfig" meson compile -C build ``` **Linux:** ```cpp project('test_unir', 'cpp', version : '0.1.0', default_options : ['cpp_std=c++20', 'warning_level=3', 'buildtype=release']) cxx = meson.get_compiler('cpp') unir_root = meson.current_source_dir() / 'unir-1.0.1-linux-x64' # или linux-arm64, linux-armhf unir_lib = cxx.find_library('unir', dirs : unir_root / 'lib', static : true) # Libs.private из unir.pc: -lpthread -ldl unir_dep = declare_dependency( include_directories : include_directories('unir-1.0.1-linux-x64/include'), dependencies : [unir_lib, dependency('threads'), cxx.find_library('dl')]) executable('test_unir', 'main.cpp', dependencies : unir_dep) ``` Нужны GCC 12 или новее и Meson; `-static` под Linux не нужен. ```bash meson setup build meson compile -C build ``` Или через pkg-config — `unir_dep = dependency('unir', static : true)` и: ```bash meson setup build --pkg-config-path="$PWD/unir-1.0.1-linux-x64/lib/pkgconfig" meson compile -C build ``` Для кадра из своего конвейера захвата (SDK камеры, буфер OpenCV, Windows-битмап) диск не участвует — собирается вид на уже имеющийся буфер, без копирования: ```cpp const unir::ImageView view{ m.data, m.cols, m.rows, (int)m.step, unir::PixelFormat::Bgr8 }; const unir::ScanResult r = scanner.scan(view); ``` ## Заголовки Публичных заголовков C++ семь, все под `include/unir/`; восьмой, `unir/Unir.h`, включает их все разом. Девятый файл, `unir/unir_c.h`, — отдельный интерфейс на C (C ABI). Заголовков движков dmr и ssccr в поставке нет: их типы наружу не выходят. | Заголовок | Что в нём | | --- | --- | | `unir/Unir.h` | Агрегат — включает всё нижеперечисленное | | `unir/Image.h` | `ImageView`, `Image`, `PixelFormat`, `ImageLimits`, `loadImage` | | `unir/Scanner.h` | `Scanner`, `ScanResult`, `Code`, `Status` | | `unir/Settings.h` | Настройки и пресеты сканера | | `unir/Symbology.h` | `Symbology`, маска `Symbologies`, имена и префиксы AIM | | `unir/Geometry.h` | `Point`, `Box`, `Quad` — где на кадре расположен код | | `unir/License.h` | Проверка и активация лицензии, пространство `unir::license` | | `unir/Version.h` | Версия заголовка (макросы), собранной библиотеки и движков внутри неё | Заголовки не тянут лишнего: тому, кто только собирает вид на кадр, достаточно одного `Image.h`, а `Symbology.h` нужен только `` и ``. ## Scanner `Scanner` — объект, а не функция, и создавать его на каждый кадр не нужно. В конструкторе собирается цепочка движков под заданные символики, и там же проходит первая проверка лицензии — если рядом лежит ещё не активированный `unir.key`, конструктор один раз ждёт ответа сервера, а первый кадр этой задержки уже не платит. В режиме потока (`Settings::stream()`) DataMatrix-движок копит между кадрами подсказку размера символа, и ей нужно пережить кадр. ```cpp class Scanner { public: explicit Scanner(Settings s = Settings::stream()); ~Scanner(); Scanner(Scanner&&) noexcept; Scanner& operator=(Scanner&&) noexcept; Scanner(const Scanner&) = delete; Scanner& operator=(const Scanner&) = delete; // Разбор кадра. Не потокобезопасен на одном Scanner в режиме Stream // (подсказка dmr меняется от кадра к кадру); для потоков — по Scanner // на поток. ScanResult scan(ImageView frame) const; // Снимок с диска и его разбор под ОДНИМ сроком кадра (загрузка в него // входит). load — что вышло с чтением файла (может быть nullptr). ScanResult scanFile(const std::string& path, LoadStatus* load = nullptr) const; const Settings& settings() const; }; // Разовый разбор кадра без объекта: проба, скрипт, один файл. ScanResult scan(ImageView frame, const Settings& s = Settings::single()); ``` > **Один Scanner — один поток.** `scan()` объявлен `const`, но в режиме `Stream` он меняет подсказку размера DataMatrix от кадра к кадру. Если кадры приходят из нескольких потоков (несколько камер, пул обработчиков), заводите по своему `Scanner` на каждый поток, а не один общий. `scanFile()` — снимок с диска и коды на нём под общим сроком: загрузка входит в тот же `budgetMs` / `timeoutMs`, что и разбор, и тяжёлый файл не получает второго бюджета. Снимок читается в полутон (`LoadAs::Gray`) с пределами из `Settings::imageLimits`. Если файл не загрузился, в `*load` приходит причина (раздел «Кадр»), а статус результата — `ImageTooLarge` для слишком большого снимка и `InvalidImage` для всех остальных отказов; если декодирование оборвал срок, взводится и `timedOut`. Без лицензии файл даже не открывается: `*load` остаётся `Ok`, статус — `Unlicensed`. У `LoadStatus` в C++ четыре значения, и отдельного «не успел за срок» среди них нет: декодирование, оборванное сроком, — это `Unreadable` вместе с `timedOut = true`. В C ABI и Python тот же случай приходит отдельным значением `TIMED_OUT`; ещё одно значение, `INVALID_ARGUMENT` (настройки отвергнуты границей ABI), в C++ не нужно — тип `Settings` здесь ошибиться не даёт. | Что с файлом | `*load` | `status`, `timedOut` | C ABI / Python | | --- | --- | --- | --- | | Прочитан | `Ok` | итог разбора | `UNIR_LOAD_OK` / `OK` | | Файла нет | `NotFound` | `InvalidImage`, `false` | `UNIR_LOAD_NOT_FOUND` / `NOT_FOUND` | | Не JPEG/PNG или повреждён | `Unreadable` | `InvalidImage`, `false` | `UNIR_LOAD_UNREADABLE` / `UNREADABLE` | | Декодирование не уложилось в срок | `Unreadable` | `InvalidImage`, `true` | `UNIR_LOAD_TIMED_OUT` / `TIMED_OUT` | | Больше пределов по заголовку | `TooLarge` | `ImageTooLarge`, `false` | `UNIR_LOAD_TOO_LARGE` / `TOO_LARGE` | | Нет лицензии — файл не открывался | `Ok` | `Unlicensed`, `false` | `UNIR_LOAD_OK` / `OK` | > **Ошибки возвращаются, а не бросаются.** `scan()` и `scanFile()` ловят исключения внутри: внутренний сбой (например, нехватка памяти) — это `Status::Failed`, если ни одного кода к этому моменту не прочитано, и процесс живёт дальше. `loadImage()` сообщает о проблемах через `LoadStatus`. > **Move-only.** `Scanner` можно перемещать, но не копировать — конструктор и оператор копирования удалены явно. ## Результат чтения ### Status ```cpp // Номера те же, что у dmr и ssccr. Коды выхода утилиты unir — те же числа, кроме // отказов загрузки файла (раздел «Командная строка»). enum class Status { Ok = 0, // прочитан хотя бы один код NotFound = 1, // ни одного символа не найдено NotDecoded = 2, // символ найден, но не прочитан Unlicensed = 3, // нет лицензии unir, пробный период кончился; кадр не разбирался ImageTooLarge = 4, // кадр больше Settings::imageLimits, не разбирался InvalidImage = 5, // описание кадра противоречиво (длина строки, формат) Failed = 6, // разбор оборвался внутренней ошибкой; процесс жив }; ``` `Ok` приходит тогда и только тогда, когда в `codes` есть хотя бы один код — от любого движка. Если не прочитал ни один, статус — самый весомый из их ответов: `Failed`, затем `InvalidImage`, `ImageTooLarge`, `NotDecoded` и, наконец, `NotFound`. Так DataMatrix, найденный, но не прочитанный, не теряется за «штрихкода нет» от линейного движка. При `Status::Unlicensed` кадр не разбирался вовсе — причину даёт `unir::license::status()` (раздел «Лицензия»). ### Что на самом деле значит NotDecoded `NotDecoded` — не «код точно есть». Это значит: хотя бы один движок маски нашёл на кадре *кандидата*, похожего на символ своей символики, и не смог его прочитать. Кандидат — ещё не символ. Движок DataMatrix берёт в кандидаты всё, что похоже на сплошную L-рамку или на текстуру модулей, — ими бывают и штрихи линейного кода, и текст, и узор упаковки. Линейный движок отмечает участок штрихов, который частично подошёл под шаблон Code 128 или EAN/UPC, но не прошёл приёмку. Поэтому ответ 2 обычен и на кадрах, где кода этой символики нет вовсе: маска только DataMatrix на кадре с одним GS1-128, маска только EAN/UPC на кадре с одним DataMatrix. Статус считается только по движкам, которые есть в маске: код символики вне маски не ищется, и его штрихи или модули могут дать кандидата чужому движку — отсюда `NotDecoded`, а не `NotFound`. Линейный код, который движок прочитал, но маска не пропускает (SSCC при маске только `Code128`), отбрасывается и тоже не даёт `Ok`. Относитесь к `NotDecoded` как к «кода не прочитано»: на конвейере — берите следующий кадр, на разовом снимке — просите переснять или повторите разбор с бо́льшим сроком. Как признак «код есть, но плохой» (для отбраковки) его не используйте. Если вместе с `NotFound` или `NotDecoded` взведён `timedOut`, ответ не окончательный: разбор оборвал срок, и с бо́льшим сроком код мог бы прочитаться. Для своей программы статусы удобно свести к трём исходам: | Status | Исход | Что делать | | --- | --- | --- | | `Ok` | Есть код | Брать `codes`. При `timedOut` часть символик или кодов могла остаться непроверенной | | `NotFound` | Нет кода | Следующий кадр. При `timedOut` — не окончательно | | `NotDecoded` | Нет кода (не прочитан) | Следующий кадр или пересъёмка; не считать, что код на кадре есть | | `Unlicensed` | Ошибка | Нет лицензии: показать `license::status().message`; повторять кадр бесполезно до активации | | `ImageTooLarge` | Ошибка входа | Уменьшить кадр или поднять `imageLimits` | | `InvalidImage` | Ошибка входа | У `scan()` — ошибка в описании кадра (ваш код); у `scanFile()` — файл не прочитан, причина в `*load` | | `Failed` | Ошибка | Внутренний сбой (например, нехватка памяти); кадр можно повторить | ### ScanResult | Поле | Тип | Смысл | | --- | --- | --- | | `status` | `Status` | Чем кончился разбор кадра | | `codes` | `std::vector` | Прочитанные коды в порядке чтения (раздел «Символики»). Пуст, если `status` не `Ok`. Не длиннее `Settings::maxCodes`, если тот больше нуля | | `timedOut` | `bool` | Хоть один движок оборван по сроку кадра или не успел начать. Успех при взведённом флаге возможен: код прочитан, но часть символик или гипотез не проверена | | `elapsedMs` | `double` | Сколько занял вызов, мс. У `scanFile()` сюда входит и загрузка снимка | Методы: `bool ok() const` — сокращение для `status == Status::Ok`; `const std::string& text() const` — текст первого кода или пустая строка. ### Code | Поле | Тип | Смысл | | --- | --- | --- | | `symbology` | `Symbology` | Символика кода — ровно один бит из маски (раздел «Символики») | | `text` | `std::string` | Данные символа, байты как есть. Ведущий FNC1 (признак GS1) не передаётся, FNC1 внутри — разделитель GS, байт `0x1D`, а не его печатный вид | | `box` | `Box` | Где символ найден на исходном кадре — прямоугольник по осям кадра | | `corners` | `Quad` | Четыре угла символа там же, точнее бокса; смысл индексов свой у каждой символики (раздел «Геометрия») | | `moduleCount` | `int` | DataMatrix: число модулей по стороне символа; 0 — неизвестно или не DataMatrix | | `modulePx` | `float` | Линейные символики: ширина модуля, пикселей; 0 — неизвестно или не линейная символика | ### Кодировка текста `Code::text` — байты, которые несёт символ; библиотека их не перекодирует и кодировку не угадывает. - **DataMatrix.** Байты 0x00–0x7F — ASCII. Байты 0x80–0xFF (Upper Shift, режим Base 256) отдаются как есть. По стандарту DataMatrix такие байты по умолчанию — ISO 8859-1 (Latin-1), но многие генераторы кладут в Base 256 текст UTF-8, так что решать вам. ECI не поддерживается: переключение ECI не разбирается, и текст символа с ECI может содержать служебные байты; макросы 05/06 не разворачиваются в заголовок `[)>`, Structured Append не собирается — каждый символ читается отдельно. Коды «Честного знака» и GS1 DataMatrix ничего из этого не используют: в них только ASCII и разделитель `0x1D`. - **Code 128 и GS1-128.** Наборы A, B и C и SHIFT; текст всегда 7-битный, 0x00–0x7F (набор A даёт и управляющие знаки 0x00–0x1F). FNC2, FNC3 и FNC4 не поддерживаются: символ с ними не читается — расширенных знаков Latin-1 через FNC4 в тексте не бывает. - **EAN/UPC** — только цифры. Для показа на экране, где нужен UTF-8: текст линейных символик — уже ASCII; текст DataMatrix проверьте на корректный UTF-8 и, если он не такой, переведите из Latin-1 (каждый байт 0x80–0xFF — один знак U+0080–U+00FF). ### Несколько кодов на кадре ```cpp const unir::Scanner scanner(unir::Settings::multi(0)); // 0 — сколько найдётся for (const unir::Code& c : scanner.scan(frame).codes) use(c.symbology, c.text, c.box); ``` С одним кодом (`maxCodes = 1`, умолчание) разбор останавливается на первом прочитанном — любой символики из маски. Если на кадре рядом DataMatrix и SSCC, а нужны оба, задавайте `multi()`; если нужен только один из них — сузьте маску (раздел «Символики»). ## Символики Каждая символика — отдельный бит: `Settings::symbologies` — их маска, что искать, а `Code::symbology` — ровно один бит, что прочитано. ```cpp enum class Symbology : uint32_t { None = 0, DataMatrix = 1u << 0, // ECC200, ГОСТ Р ИСО/МЭК 16022 (движок dmr) Code128 = 1u << 1, // Code 128 без признака GS1 (движок ssccr) Gs1_128 = 1u << 2, // GS1-128: Code 128 с FNC1 в начале (движок ssccr) Ean13 = 1u << 3, // EAN/UPC — движок ssccr Ean8 = 1u << 4, UpcA = 1u << 5, UpcE = 1u << 6, }; using Symbologies = uint32_t; // маска символик constexpr Symbologies operator|(Symbology a, Symbology b); constexpr Symbologies operator|(Symbologies a, Symbology b); constexpr bool has(Symbologies mask, Symbology s); constexpr Symbologies kEanUpc = Ean13 | Ean8 | UpcA | UpcE; constexpr Symbologies kAllSymbologies = DataMatrix | Code128 | Gs1_128 | kEanUpc; // все символики этой сборки ``` ```cpp auto s = unir::Settings::stream(); s.symbologies = unir::Symbology::DataMatrix | unir::Symbology::Gs1_128; // «Честный знак» и SSCC const unir::Scanner scanner(s); ``` | Символика | Что читается | | --- | --- | | `DataMatrix` | DataMatrix ECC200 любой структуры — тем же движком и с теми же ответами, что у «Чтения DataMatrix» | | `Gs1_128` | Code 128 с FNC1 в начале. Если в маске нет `Code128`, читается только SSCC (AI 00); вместе с `Code128` — и прочие GS1-128 | | `Code128` | Code 128 без признака GS1. Приём строже, чем у SSCC: должны совпасть не меньше двух сканлайнов. Без `Gs1_128` в маске SSCC и прочие GS1-128 отбрасываются | | `Ean13`, `Ean8`, `UpcA`, `UpcE` | Приём — согласие двух сканлайнов и свободные зоны. UPC-A — 12 цифр (EAN-13 с первой цифрой 0), UPC-E — 8 цифр (система, 6 цифр, контрольная) без разворота в UPC-A. Движок читает все четыре вместе, но отдаются только те, что в маске | > **Умолчание — все символики сборки.** `Settings::symbologies` по умолчанию — `kAllSymbologies`, то есть всё, что умеет эта сборка. В следующих версиях добавятся QR и другие символики, и программа с умолчанием после обновления начнёт искать и их — а значит, и тратить на них время кадра. Если нужен фиксированный набор, задавайте маску явно. ### Имена и префиксы AIM ```cpp // Короткое имя: "datamatrix", "code128", "gs1-128", "ean-13", "ean-8", // "upc-a", "upc-e"; None — "none". const char* nameOf(Symbology s); // Идентификатор символики AIM (ISO/IEC 15424), без модификатора данных: // "]d", "]C", "]E". const char* aimPrefixOf(Symbology s); // Маска из списка имён через запятую: "dm,128,gs1-128", "all". // Неизвестное имя — false, mask не меняется. bool parseSymbologies(const std::string& list, Symbologies& mask); ``` `parseSymbologies` понимает, без учёта регистра: `datamatrix` | `dm`, `code128` | `128`, `gs1-128` | `gs1` | `sscc`, `ean-13` | `ean13`, `ean-8` | `ean8`, `upc-a` | `upca`, `upc-e` | `upce`, `ean` — все четыре EAN/UPC, и `all`. Пустая строка — тоже неизвестное имя. Удобно, когда набор символик приходит из конфигурации или командной строки — тот же разбор у ключа `--sym` утилиты. `aimPrefixOf` отдаёт идентификатор символики без модификатора: `]d` для DataMatrix, `]C` для Code 128 и GS1-128, `]E` для EAN/UPC. Признак GS1 (FNC1) у DataMatrix в этой версии наружу не отдаётся, поэтому модификатора вида `]d2` нет: GS1-структуру DataMatrix различайте по содержимому. Модификатор добавится, когда движки станут сообщать признак FNC1. ### Порядок разбора и порядок кодов Движки зовутся по очереди, и порядок зависит от того, сколько кодов искать: - **Один код** (`maxCodes = 1`): сначала быстрый проход линейного движка — SSCC и EAN/UPC, затем DataMatrix, затем прочий Code 128 (и прочий GS1-128) — он на кадре без штрихкода самый дорогой и поэтому последний. Первый прочитанный код и есть ответ: следующие движки уже не зовутся. - **Несколько кодов** (`maxCodes` 0 или больше 1): сначала все линейные символики, затем DataMatrix. В `codes` — сперва линейные коды, потом DataMatrix; разбор кончается, как только набралось `maxCodes`. Движки, которых нет в маске, в цепочку не попадают и времени не стоят. Прежде чем отдать кадр DataMatrix-движку, unir заливает уже прочитанные линейные коды вместе с их свободными зонами ровным тоном: штрихи не уводят его в перебор ложных кандидатов. Заливка строится по углам прочитанных кодов, чужого символа не задевает и на координаты в ответе не влияет. ## Кадр ### PixelFormat ```cpp enum class PixelFormat { Gray8, // один канал: яркость; для сканера самый дешёвый Bgr8, // три канала, порядок OpenCV Rgb8, Bgra8, // четыре канала, порядок OpenCV и Windows Rgba8, }; constexpr int channelsOf(PixelFormat f); // 1, 3 или 4 ``` Для монохромной камеры кадр стоит передавать как `Gray8`, а не размноженным в три канала. ### ImageView Вид на чужой буфер: библиотека его не копирует, не освобождает и не переживает — буфер обязан быть жив всё время вызова. Поля повторяют вид кадра у dmr и ssccr, поэтому движкам кадр уходит без копирования. ```cpp struct ImageView { const uint8_t* data = nullptr; int width = 0; int height = 0; int stride = 0; // байт в строке; 0 — строки вплотную PixelFormat format = PixelFormat::Gray8; bool empty() const; int channels() const; int rowBytes() const; // stride с уже разобранным нулём const uint8_t* row(int y) const; }; ``` `stride = 0` значит «строки вплотную» (`width * каналов`), а не «ноль байт в строке»: кадр камеры почти никогда не плотный — строки выравнивают, а вырезка наследует длину строки оригинала. Пустой вид или `stride` меньше длины строки — `Status::InvalidImage`, до движков такой кадр не доходит. ### Image Кадр, которым библиотека владеет сама, строки вплотную. Неявно приводится к `ImageView`, поэтому передаётся в `scan()` напрямую: ```cpp class Image { public: Image() = default; Image(int width, int height, PixelFormat format); void reset(int width, int height, PixelFormat format); // буфер под размер, заполнен нулями void clear(); int width() const; int height() const; int stride() const; PixelFormat format() const; bool empty() const; uint8_t* data(); const uint8_t* data() const; uint8_t* row(int y); const uint8_t* row(int y) const; ImageView view() const; operator ImageView() const; // неявно — для вызова scan(image) }; ``` ### Чтение с диска ```cpp struct ImageLimits { int maxSide = 16384; // 0 — kHardMaxSide long long maxPixels = 100'000'000; // 0 — kHardMaxPixels static constexpr int kHardMaxSide = 65535; static constexpr long long kHardMaxPixels = 1LL << 29; bool allows(long long w, long long h) const; }; enum class LoadStatus { Ok, // снимок прочитан NotFound, // файла нет Unreadable, // файл есть, но это не читаемый снимок (или не успел за срок) TooLarge, // размер из заголовка больше ImageLimits; пиксели не читались }; enum class LoadAs { Color, // PixelFormat::Bgr8 Gray, // PixelFormat::Gray8 — сканеру этого достаточно }; LoadStatus loadImage(const std::string& path, Image& out, LoadAs as = LoadAs::Gray, const ImageLimits& limits = ImageLimits{}); ``` Снимок декодируется теми же декодерами, что у dmr, — с пределами размера и защитой от «бомб»: форматы **JPEG и PNG**, определяются по первым байтам файла, а не по расширению. Путь — в UTF-8 (в Windows годится и системная кодировка, как у `argv`: строка, не составляющая корректный UTF-8, читается в ней). По умолчанию снимок читается в полутон: сканеру цвет не нужен. Пределы сверяются с заголовком файла до выделения памяти; передавайте сюда те же, что в `Settings::imageLimits`. При отказе `out` не меняется. ```cpp unir::Image frame; switch (unir::loadImage("кадр.jpg", frame)) { case unir::LoadStatus::Ok: break; case unir::LoadStatus::NotFound: /* файла нет */ break; case unir::LoadStatus::Unreadable: /* файл есть, но это не снимок */ break; case unir::LoadStatus::TooLarge: /* больше пределов */ break; } ``` Отдельный `loadImage()` идёт без срока кадра. Когда снимок пришёл из недоверенного источника, удобнее `Scanner::scanFile()`: там загрузка и разбор — под одним сроком. ## Геометрия ```cpp struct Point { float x = 0.0f; float y = 0.0f; }; struct Box { // прямоугольник по осям кадра int x = 0, y = 0, width = 0, height = 0; bool empty() const; }; struct Quad { // четыре угла символа по кругу Point corner[4]; }; ``` Все координаты — в пикселях исходного кадра. `Point` дробный: углы находятся точнее пикселя. `Box` — прямоугольник по осям кадра, `Quad` — четыре угла символа точнее, чем прямоугольником: на конвейере символ снят под углом. ```cpp for (const unir::Point& p : c.corners.corner) drawTo(p.x, p.y); ``` > **Смысл индексов — свой у каждой символики.** Углы `Quad` всегда идут по кругу, но что стоит под каким индексом, зависит от символики: > > - **DataMatrix** — под индексом 3 стоит угол, который детектор принял за угол сплошной L-рамки. Для разметки поверх кадра `corner[3]` годится, для выводов о повороте содержимого символа — нет. > - **Code 128 и GS1-128** — `corner[0]` на стороне старта, `corner[1]` на стороне стопа на том же краю штрихов, `corner[2]` и `corner[3]` — на противоположном краю. По ним видно направление чтения. > > Прежде чем читать смысл углов, смотрите на `Code::symbology`. ## Настройки Начинать стоит с пресета, а не с отдельных полей — имя пресета уже говорит, под какой сценарий он настроен. Пресет задаёт режим DataMatrix-движка; линейный движок от режима не зависит. | Пресет | Для чего | Чем отличается | | --- | --- | --- | | `Settings::stream()` | Поток кадров одной камеры — конвейер. Умолчание | `mode = Stream`: DataMatrix-движок копит подсказку размера символа между кадрами; один код | | `Settings::single()` | Разовые, не связанные между собой снимки — папка, ручная проверка | `mode = Single`: подсказки нет, DataMatrix-движок перебирает все стандартные размеры, если размер не удалось измерить; один код | | `Settings::multi(n)` | Несколько кодов на одном кадре | `mode = Multi`, `maxCodes = n` (по умолчанию 0 — сколько найдётся) | ```cpp auto s = unir::Settings::stream(); s.symbologies = unir::Symbology::DataMatrix | unir::kEanUpc; s.budgetMs = 120.0; // один срок на все символики; 0 — без ограничения const unir::Scanner scanner(s); ``` ### Все поля ```cpp // unir/Settings.h struct Settings { static Settings stream(); // поток кадров одной камеры (умолчание) static Settings single(); // кадры не связаны между собой static Settings multi(int maxCodes = 0); // до maxCodes кодов на кадре (0 — все) Symbologies symbologies = kAllSymbologies; // какие символики искать int maxCodes = 1; // 1 — до первого кода, N — до N, 0 — все double budgetMs = 0.0; // срок кадра, мс, один на все символики; 0 — без double timeoutMs = 10000.0; // предохранитель, мс; 0 — потолок 60 с ImageLimits imageLimits{}; // 16384 по стороне, 100 000 000 пикселей enum class Mode { Stream, Single, Multi }; Mode mode = Mode::Stream; // режим DataMatrix-движка }; ``` Пресеты меняют только `mode`, а `multi()` — ещё и `maxCodes`; остальные поля у всех трёх — умолчания из объявления. | Поле | Умолчание | Смысл | | --- | --- | --- | | `symbologies` | `kAllSymbologies` | Какие символики искать — маска (раздел «Символики»). Умолчание — все символики сборки; в следующих версиях их станет больше | | `maxCodes` | 1 | Сколько кодов искать: 1 — до первого прочитанного кода любой символики, N — до N, 0 — все | | `budgetMs` | 0.0 | Предел времени на кадр, мс, — **один на все символики**: движки делят его по очереди, и тот, кому не осталось времени, не начинается вовсе (взводится `timedOut`). 0 — без предела, действует только `timeoutMs`. Срок мягкий: время кадра может превысить его на длительность идущей стадии. DataMatrix-движку бюджет передаётся как есть, и, как в самом dmr, заданный бюджет меняет и способ перебора, а не только обрывает его | | `timeoutMs` | 10000.0 | Предохранитель, мс: тот же обрыв, что у `budgetMs` (из двух действует меньший), но способа работы движков не меняет. 0 и больше 60000 — потолок 60 с: дольше кадр не разбирается никогда. Защита от враждебных кадров, а не настройка задержки | | `imageLimits` | 16384 по стороне, 100 000 000 пикселей | Наибольший кадр, который сканер берётся разбирать (`ImageLimits` из `unir/Image.h`; 0 — жёсткий потолок 65535 и 2^29). Кадр больше сразу получает `Status::ImageTooLarge`. `scanFile()` сверяет те же пределы с заголовком файла до декодирования. Защита, а не настройка качества | | `mode` | `Mode::Stream` | Режим DataMatrix-движка: `Stream` — подсказка размера между кадрами, `Single` — одиночные кадры с перебором размеров, `Multi` — несколько кодов. Обычно выставляется пресетом | Тонких настроек dmr (поворот, проверка GS1, источники кандидатов и прочие) unir наружу не выводит: DataMatrix-движок работает с пресетом dmr, выбранным по `mode`, и с вашими `maxCodes`, сроками и пределами кадра. ### Скорость: режим, маска, срок Время кадра определяют три настройки, и все три стоит задавать явно, а не оставлять умолчания. - **Режим.** С одним кодом (`stream()`, `single()`) разбор кончается на первом прочитанном коде, а первым идёт самый дешёвый проход — SSCC и EAN/UPC: кадр с одним штрихкодом читается быстро, и до DataMatrix дело не доходит. `multi(0)` остановиться не может: каждый движок маски проходит весь кадр до конца, линейный ищет все Code 128 и EAN/UPC, DataMatrix-движок перебирает всех своих кандидатов. На кадре с одним кодом это в разы, а на слабой машине — на порядок дольше режима одного кода. Если число кодов известно, задавайте `multi(N)`: разбор кончится, как только их наберётся N. - **Маска.** Каждая символика вне маски — движок или проход, которого нет. Дороже всего обходятся «ничего не найдено» на кадре без кода: прочий `Code128` примеряется ко всему, что похоже на штрихи, а DataMatrix-движок уходит в глубокий перебор. SSCC читается и без `Code128` — по одному `Gs1_128`. Маска только из нужных символик заметно быстрее «всех», маска из одной DataMatrix — тем более. - **Срок.** `budgetMs` — один срок на кадр для всех символик. Движки тратят его по очереди; кончился — идущий движок обрывается на ближайшей стадии, следующие не начинаются, в результате взводится `timedOut`. Прочитанное до срока остаётся в `codes`. Без `budgetMs` кадр ограничен только предохранителем `timeoutMs` (10 с), и в `multi(0)` на трудном кадре это ощутимо. Практически: на конвейере срок — интервал между кадрами за вычетом своей обработки; для разового снимка, ответа на который ждёт человек, в `multi()` — столько, сколько он готов ждать, например `budgetMs = 3000`. У `scanFile()` в этот же срок входит загрузка файла. ## Версия библиотеки ```cpp #define UNIR_VERSION_MAJOR 1 #define UNIR_VERSION_MINOR 0 #define UNIR_VERSION_PATCH 1 #define UNIR_VERSION_STRING "1.0.1" #define UNIR_VERSION_AT(ma, mi, pa) ((ma) * 10000 + (mi) * 100 + (pa)) #define UNIR_VERSION UNIR_VERSION_AT(UNIR_VERSION_MAJOR, UNIR_VERSION_MINOR, UNIR_VERSION_PATCH) namespace unir { std::string version(); // "1.0.1" std::string engineVersions(); // версии движков внутри сборки: "dmr 1.0.12, ssccr 0.1.0" } ``` Макросы говорят про заголовок, с которым скомпилировались; `unir::version()` — про библиотеку, с которой слинковались. Разойтись они могут: библиотека ставится в префикс и живёт там дольше, чем помнит собиравший. `engineVersions()` нужен для обращений в поддержку и сверки с «Чтением DataMatrix»: по нему видно, какой ревизией dmr и ssccr собрана поставка. ```cpp #if UNIR_VERSION >= UNIR_VERSION_AT(1, 1, 0) // код под заголовок 1.1.0 и новее #endif ``` ## Лицензия Библиотека читает коды, пока действует пробный период (30 дней с первого запуска на машине, без сети и регистрации) или лицензия. Иначе `Scanner::scan()` и `scanFile()` сразу отвечают `Status::Unlicensed`: ни один движок не зовётся, файл снимка не открывается. Лицензия привязана к оборудованию (материнская плата, процессор, системный диск), а не к установке ОС: переустановка Windows и замена диска или процессора её не отбирают, замена материнской платы — отбирает. Пробный период и лицензия unir — свои, независимые от «Чтения DataMatrix» и «Создания DataMatrix» на той же машине: сервер лицензий тот же, но продукт другой, и лицензия одного продукта не открывает другой. ```cpp #include const unir::LicenseStatus s = unir::license::status(); if (!s.canScan()) show(s.message); // «Пробный период закончился. Нужна лицензия» unir::LicenseResult r = unir::license::activate(keyFromUser); // «UNIR-XXXXX-XXXXX-XXXXX-XXXXX» if (!r.ok) show(r.message); ``` ### Файл ключа unir.key Основной способ активации — вместо вызова `activate()` из своей программы положить ключ в текстовый файл `unir.key`. Имя своё, не `license.key`, как у «Чтения DataMatrix»: библиотеки разных продуктов могут лежать в одном каталоге, а ключи у них разные. Библиотека ищет файл, берётся первый найденный: 1. рядом с модулем, где живёт библиотека: `unir_c.dll` / `libunir_c.so` для C ABI, сама программа — для статической `libunir.a` и утилиты `unir`; 2. рядом с исполняемым файлом программы. Текущий каталог и переменные окружения не просматриваются. Формат: ключ — первая строка, которая не пуста и не комментарий `#`, в том виде, в каком выдан — `UNIR-XXXXX-XXXXX-XXXXX-XXXXX` (регистр, пробелы и дефисы не важны; BOM UTF-8 допустим; файлы больше 4 КБ не читаются). Ключ активируется по сети сам, вызывать ничего не нужно: при создании первого `Scanner` или первом вызове функции `license::` — этот вызов один раз ждёт ответа сервера. Не вышло — причина в `status().message`; без сети попытка повторяется раз в час, при отказе сервера — раз в сутки, в том числе между запусками. Файл, положенный при работающей программе, подхватывается в течение часа. Действующую лицензию файл с другим ключом не заменяет; ключ, отвязанный `deactivate()`, из файла снова не активируется. Какой файл библиотека нашла, скажет `license::keyFile()`. ### Где хранится состояние пробного периода и лицензии Windows: `%ProgramData%\unir\license.dat`, `%LOCALAPPDATA%\unir\license.dat` и значение `state` в разделе реестра `HKCU\Software\unir`. Linux: `/var/lib/unir/license.dat` и `~/.config/unir/license.dat` пользователя, от которого работает процесс (по базе пользователей, а не по `$HOME`). Копий несколько, берётся самое раннее начало пробного периода. У контейнера нет ни данных DMI, ни своего диска, поэтому лицензию к нему не привязать — запускайте программу на хосте или в виртуальной машине. На Linux общую копию в `/var/lib/unir` может создать только root: под обычным пользователем каталог не создаётся, и эта копия молча пропускается — состояние живёт в `~/.config/unir`. Ошибкой это не считается. Пробный период заводится при первом обращении к библиотеке на машине — первом `Scanner` или первом `license::status()` — и записывается сразу, а не при выходе из программы. ### LicenseState | Значение | Смысл | | --- | --- | | `Trial` | Идёт пробный период | | `Active` | Лицензия действует | | `RefreshDue` | Действует, но связи с сервером давно не было; автосверка повторяет попытки раз в час | | `TrialExpired` | Пробный период кончился, лицензии нет | | `Expired` | Оплаченный срок лицензии кончился; продление в личном кабинете автосверка подхватит сама | | `NetworkRequired` | Без связи с сервером дальше нельзя; как только сервер ответит, автосверка вернёт лицензию | | `WrongMachine` | Лицензия выдана другой машине | | `Invalid` | Запись лицензии не проходит проверку подписи | ### LicenseStatus ```cpp // unir/License.h enum class LicenseState { Trial = 0, Active = 1, RefreshDue = 2, TrialExpired = 3, Expired = 4, NetworkRequired = 5, WrongMachine = 6, Invalid = 7, }; struct LicenseStatus { LicenseState state = LicenseState::TrialExpired; bool canScan() const; // Trial, Active или RefreshDue int64_t validUntil = 0; // unix-секунды UTC int64_t refreshAfter = 0; // 0 — не устаревает int64_t offlineUntil = 0; // 0 — без ограничения int daysLeft = 0; std::string licenseId; std::string activationId; std::string offlineMode; // limited | extended | full; пусто без лицензии bool clockRollback = false; std::string message; // состояние словами, по-русски }; struct LicenseResult { bool ok = false; std::string error; // код отказа: LICENSE_NOT_FOUND, KEY_INVALID, NETWORK, ... std::string message; LicenseStatus status; // состояние после операции }; ``` Функции «`LicenseState` → строка» в C++ API нет: для пользователя есть `message`, а машинный код состояния (`trial`, `active`, …) отдают только JSON утилиты, C ABI и Python. Если он нужен в C++ — короткий `switch` с теми же именами: ```cpp const char* stateName(unir::LicenseState s) { switch (s) { case unir::LicenseState::Trial: return "trial"; case unir::LicenseState::Active: return "active"; case unir::LicenseState::RefreshDue: return "refresh_due"; case unir::LicenseState::TrialExpired: return "trial_expired"; case unir::LicenseState::Expired: return "expired"; case unir::LicenseState::NetworkRequired: return "network_required"; case unir::LicenseState::WrongMachine: return "wrong_machine"; case unir::LicenseState::Invalid: return "invalid"; } return "invalid"; } ``` | Поле | Смысл | | --- | --- | | `state` | `LicenseState` | | `canScan()` | Читает ли сейчас библиотека — true при `Trial`, `Active` или `RefreshDue` | | `validUntil` | Конец пробного периода или оплаченного срока, unix-секунды UTC | | `refreshAfter` | С какого момента токен считается устаревшим и лицензия переходит в `RefreshDue`; 0 — не устаревает (пробный период, режим `full`) | | `offlineUntil` | Дольше этого без связи с сервером лицензия не работает; 0 — без ограничения (режим `full`) | | `daysLeft` | Целых суток до ближайшей из границ выше; 0 — граница пройдена | | `licenseId` / `activationId` | Идентификаторы лицензии и конкретной активации | | `offlineMode` | `limited \| extended \| full`; пусто без лицензии | | `clockRollback` | Системные часы отстают от времени, которое машина уже видела — откат часов срок не продлевает | | `message` | Состояние словами, для показа пользователю | Функции, которые что-то делают с лицензией, возвращают `LicenseResult`: `ok`, код отказа `error` (ответ сервера как есть — `LICENSE_NOT_FOUND`, `ACTIVATION_LIMIT_REACHED`, `LICENSE_REVOKED` и другие — либо свой: `KEY_INVALID`, `NETWORK`, `WRONG_PRODUCT` и другие), `message` словами и `status` — состояние уже после операции. ### Активация: онлайн и офлайн Онлайн — ключ из личного кабинета, машина сама идёт на сервер лицензий: ```cpp unir::LicenseResult r = unir::license::activate("UNIR-XXXXX-XXXXX-XXXXX-XXXXX"); ``` Офлайн — для машины без интернета: файл запроса переносится на машину с сетью и загружается в личный кабинет, в ответ приходит `license.lic`: ```cpp unir::license::writeActivationRequest("request.json"); // → загрузить в кабинет, получить license.lic unir::license::importLicenseFile("license.lic"); // тем же вызовом продлевается: новый файл заменяет старый ``` Активированная по сети лицензия раз в сутки сама сверяется с сервером в фоновом потоке — обновляет токен (режимам `limited` и `extended` это нужно, чтобы работать дальше), приносит отзыв и продление срока. Вызывать для этого ничего не нужно, а чтение кадра сеть не ждёт никогда; сервер недоступен — повтор через час, лицензия работает, пока не выйдет срок работы без связи. ### Переезд и остальные функции ```cpp namespace unir::license { void configure(const LicenseOptions& options); // до первого Scanner; можно не звать LicenseStatus status(); LicenseResult activate(const std::string& licenseKey); LicenseResult refresh(); // сверить сейчас, не дожидаясь суток LicenseResult deactivate(const std::string& reason = {}); // расписка уходит на сервер LicenseResult deactivateToFile(const std::filesystem::path& receiptPath, const std::string& reason = {}); LicenseResult writeActivationRequest(const std::filesystem::path& path); LicenseResult importLicenseFile(const std::filesystem::path& path); std::string machineFingerprint(); // для обращений в поддержку std::filesystem::path keyFile(); // найденный unir.key; пусто — нет } struct LicenseOptions { std::string serverUrl; // пусто — вшитый при сборке std::string appVersion; // версия вашей программы — видна в кабинете рядом с машиной int timeoutMs = 15000; // предел ожидания одного запроса к серверу bool autoCheck = true; // суточная автосверка }; ``` `configure()` нужно звать до создания первого `Scanner`: активация ключа из `unir.key` идёт при первом обращении к библиотеке и берёт сервер и `appVersion` уже отсюда. Подменять сервер безопасно: ответы проверяются подписью по ключам, вшитым в библиотеку. Выключать `autoCheck` стоит, только если программа по своим правилам не должна выходить в сеть сама; тогда лицензии `limited` и `extended` продлевает её собственный вызов `refresh()`. У лицензии, принятой из файла, ключа нет, и `refresh()` ей не поможет — её продлевают новым `license.lic`. Переезд на другую машину — `deactivate()` на старой: место освобождается на сервере распиской, подписанной секретом этой машины; без связи лицензия остаётся на месте. Без сети — `deactivateToFile()`: лицензия удаляется с машины сразу, а расписка ложится в файл, отправить его можно с любой машины. Все функции потокобезопасны; функции с сетью блокируют вызывающего до ответа или до истечения `LicenseOptions::timeoutMs`. Каждый ответ сервера лицензий привязан к своему запросу: библиотека отправляет случайное одноразовое число, сервер возвращает его в подписанном ответе, и записанный раньше ответ повторно не примется. Расписка о сдаче места тоже одноразовая, поэтому расписку из восстановленной копии старого состояния машины сервер не примет. У лицензий с полной автономией (`full`) сдача места расходует лимит переносов; когда он исчерпан, перенос делает поддержка. Самостоятельно освободить место по лицензии можно не больше двух раз за 30 дней в режиме `limited` и не больше одного раза — в `extended`; дальше перенос делает поддержка. > **Ключ активации.** Ключ активации выдаётся в личном кабинете после оформления тарифа в [каталоге](https://libscanner.com/). ## C ABI Та же библиотека за плоским интерфейсом на C99 — заголовок `unir/unir_c.h`, библиотека `unir_c.dll` / `libunir_c.so` (как подключить — «Подключение»). Для C, MSVC и любого языка с вызовом функций C (C#, Go, Delphi, Rust…). Правила границы: - Через неё ходят только простые типы C: целые, `double`, указатели на буферы известного размера, перечисления как `int` и непрозрачные дескрипторы. Исключение C++ границу не пересекает — внутренний сбой превращается в `NULL` или код ошибки. - Владение: всё, что функция *возвращает* (дескриптор, строка), принадлежит вызывающему и освобождается своей функцией (`unir_image_free`, `unir_scan_result_free`, `unir_free_string`; сканер — `unir_scanner_destroy`). Указатели из функций доступа (`unir_scan_result_code_text` и подобных) живут не дольше объекта, из которого взяты, и отдельно не освобождаются. Строку `unir_symbology_name` не освобождают — она статическая. - `unir_settings_t` начинается с `struct_size` и метки `abi_tag`: их выставляет вызывающий **до** пресета — в C проще всего `unir_settings_t s = UNIR_SETTINGS_INIT;`, в C++ — `unir_settings_prepare(&s)`. Пресет пишет не дальше `struct_size`; поля, которых вызывающий не знает (более новая библиотека), остаются по умолчанию. Чужая метка или размер вне допустимого — отказ: пресет вернёт 0, `unir_scanner_create` — `NULL`. - Символики — биты маски `UNIR_SYM_*`, те же значения, что у `unir::Symbology` (раздел «Символики»); у прочитанного кода — ровно один бит (`unir_scan_result_code_symbology`), имя — `unir_symbology_name`. `UNIR_SYM_ALL` — все символики *этой* сборки: в следующих версиях в неё войдут новые (QR и другие), поэтому для неизменного набора задавайте маску явно. - Прочитанный текст хранит разделитель GS байтом `0x1D` и может содержать нулевой байт — длину брать у `unir_scan_result_code_text_len`, а не `strlen()`. - Пути — в UTF-8. В Windows годится и системная кодировка, как у `argv`: строка, не составляющая корректный UTF-8, читается в ней. Имя, которого нет в кодовой странице Windows, передавайте в UTF-8. - Статусы — те же, что `unir::Status` (раздел «Результат чтения»), плюс `UNIR_STATUS_INTERNAL_ERROR = -1` на сбой самого слоя C. `unir_scanner_scan` и `unir_scanner_scan_file` возвращают `NULL` только при внутренней ошибке; «кода нет» — это результат со статусом `UNIR_STATUS_NOT_FOUND`. Снимок, который не загрузился, — `UNIR_STATUS_IMAGE_TOO_LARGE` или `UNIR_STATUS_INVALID`, подробности — в `*load_status`. - Один сканер не вызывают из нескольких потоков одновременно — по сканеру на поток. - Функции лицензии возвращают тот же JSON, что `unir --license … --json` (раздел «Командная строка»): один контракт для утилиты, C ABI и Python. ### Полный пример: все коды со снимка ```cpp #include #include /* Все коды со снимка: 0 — прочитаны, 1 — нет кода, 2 — ошибка. */ int main(int argc, char** argv) { if (argc < 2) return 2; unir_settings_t settings = UNIR_SETTINGS_INIT; /* размер и метка — до пресета */ unir_settings_multi(&settings, 0); /* все коды на снимке */ settings.symbologies = UNIR_SYM_ALL; /* или, например, UNIR_SYM_DATAMATRIX | UNIR_SYM_EAN_UPC */ settings.budget_ms = 3000; /* один срок на загрузку и все символики */ unir_scanner_t* scanner = unir_scanner_create(&settings); if (!scanner) return 2; unir_load_status_t load = UNIR_LOAD_UNREADABLE; unir_scan_result_t* r = unir_scanner_scan_file(scanner, argv[1], &load); int rc = 2; if (r) { /* NULL — внутренняя ошибка */ unir_status_t st = unir_scan_result_status(r); if (st == UNIR_STATUS_OK) { for (int i = 0; i < unir_scan_result_code_count(r); ++i) { printf("%s\t", unir_symbology_name(unir_scan_result_code_symbology(r, i))); /* в тексте бывают 0x1D (GS) и даже 0x00 — длина, а не strlen() */ fwrite(unir_scan_result_code_text(r, i), 1, unir_scan_result_code_text_len(r, i), stdout); putchar('\n'); } rc = 0; } else if (st == UNIR_STATUS_NOT_FOUND || st == UNIR_STATUS_NOT_DECODED) { rc = 1; } else if (st == UNIR_STATUS_UNLICENSED) { char* why = unir_license_status_json(); /* {"state":"trial_expired",...} */ fprintf(stderr, "%s\n", why ? why : "unlicensed"); unir_free_string(why); } else { fprintf(stderr, "status %d, load %d\n", (int)st, (int)load); /* нет файла, не снимок, велик */ } unir_scan_result_free(r); } unir_scanner_destroy(scanner); return rc; } ``` В Linux: `cc main.c -o read $(pkg-config --cflags --libs unir_c)`, запуск — `LD_LIBRARY_PATH=/куда/распакован/lib ./read этикетка.png`. Печатает по строке на код: символику, табуляцию и текст — у DataMatrix «Честного знака» с настоящими байтами `0x1D` между элементами. ### Справочник Все объявления `unir_c.h` (ABI 1.0, библиотека 1.0.1); комментарии сокращены — полные в самом заголовке. ```cpp /* ── версии ── */ #define UNIR_C_ABI_VERSION_MAJOR 1 #define UNIR_C_ABI_VERSION_MINOR 0 size_t unir_c_abi_version(char* buf, size_t buf_size); /* "1.0" */ size_t unir_version(char* buf, size_t buf_size); /* версия библиотеки, "1.0.1" */ size_t unir_engine_versions(char* buf, size_t buf_size); /* "dmr 1.0.12, ssccr 0.1.0" */ /* ── символики: биты маски ── */ #define UNIR_SYM_NONE 0x00u #define UNIR_SYM_DATAMATRIX 0x01u #define UNIR_SYM_CODE128 0x02u /* Code 128 без признака GS1 */ #define UNIR_SYM_GS1_128 0x04u /* без UNIR_SYM_CODE128 в маске — только SSCC (AI 00) */ #define UNIR_SYM_EAN13 0x08u #define UNIR_SYM_EAN8 0x10u #define UNIR_SYM_UPCA 0x20u /* 12 цифр */ #define UNIR_SYM_UPCE 0x40u /* 8 цифр, без разворота в UPC-A */ #define UNIR_SYM_EAN_UPC (UNIR_SYM_EAN13 | UNIR_SYM_EAN8 | UNIR_SYM_UPCA | UNIR_SYM_UPCE) #define UNIR_SYM_ALL (UNIR_SYM_DATAMATRIX | UNIR_SYM_CODE128 | UNIR_SYM_GS1_128 | UNIR_SYM_EAN_UPC) const char* unir_symbology_name(uint32_t symbology); /* "datamatrix" … "upc-e"; 0 — "none"; * маска или чужой бит — "unknown" */ int unir_parse_symbologies(const char* list, uint32_t* mask); /* "dm,128,ean", "all"; 1 — готово, * 0 — неизвестное имя */ /* ── кадр ── */ typedef enum { UNIR_PIXEL_GRAY8 = 0, UNIR_PIXEL_BGR8 = 1, UNIR_PIXEL_RGB8 = 2, UNIR_PIXEL_BGRA8 = 3, UNIR_PIXEL_RGBA8 = 4 } unir_pixel_format_t; typedef struct { const uint8_t* data; /* буфер вызывающего, жив весь вызов scan */ int width; int height; int stride; /* байт на строку; 0 — плотно */ unir_pixel_format_t format; } unir_image_view_t; typedef struct unir_image unir_image_t; /* непрозрачный, буфер библиотеки */ typedef struct unir_settings unir_settings_t; /* определена ниже */ typedef enum { UNIR_LOAD_COLOR = 0, UNIR_LOAD_GRAY = 1 } unir_load_as_t; typedef enum { UNIR_LOAD_OK = 0, UNIR_LOAD_NOT_FOUND = 1, UNIR_LOAD_UNREADABLE = 2, UNIR_LOAD_TOO_LARGE = 3, UNIR_LOAD_TIMED_OUT = 4, UNIR_LOAD_INVALID_ARGUMENT = 5 } unir_load_status_t; /* JPEG и PNG; GRAY — GRAY8, COLOR — BGR8; *out — NULL при отказе */ unir_load_status_t unir_load_image(const char* path, unir_load_as_t as, unir_image_t** out); unir_load_status_t unir_load_image_ex(const char* path, unir_load_as_t as, const unir_settings_t* settings, /* NULL — по умолчанию */ unir_image_t** out); unir_image_view_t unir_image_view(const unir_image_t* image); void unir_image_free(unir_image_t* image); /* ── настройки: те же поля и умолчания, что unir::Settings ── */ typedef enum { UNIR_MODE_STREAM = 0, UNIR_MODE_SINGLE = 1, UNIR_MODE_MULTI = 2 } unir_mode_t; struct unir_settings { uint32_t struct_size; /* sizeof(unir_settings_t), ставит вызывающий ПЕРВЫМ */ uint32_t abi_tag; /* UNIR_SETTINGS_TAG, тоже до пресета */ uint32_t symbologies; /* маска UNIR_SYM_*; в пресетах UNIR_SYM_ALL */ int max_codes; /* 1 — до первого кода любой символики, N — до N, 0 — все */ int mode; /* unir_mode_t */ double budget_ms; /* ОДИН срок на все символики; 0 — без предела */ double timeout_ms; /* предохранитель, в пресетах 10000; 0 — потолок 60 с */ int max_image_side; /* 0 — потолок 65535; в пресетах 16384 */ int64_t max_image_pixels; /* 0 — потолок 2^29; в пресетах 100 000 000 */ }; #define UNIR_SETTINGS_TAG 0x31524E55u /* 'U' 'N' 'R' '1' */ #define UNIR_SETTINGS_INIT { .struct_size = (uint32_t)sizeof(unir_settings_t), .abi_tag = UNIR_SETTINGS_TAG } #define UNIR_SETTINGS_MAX_SIZE 4096u static inline void unir_settings_prepare(unir_settings_t* s); /* обнуляет, ставит размер и метку */ int unir_settings_stream(unir_settings_t* out); /* 1 — заполнено; 0 — NULL, чужая */ int unir_settings_single(unir_settings_t* out); /* метка или размер */ int unir_settings_multi(unir_settings_t* out, int max_codes); /* 0 — все коды */ /* ── сканер ── */ typedef struct unir_scanner unir_scanner_t; /* непрозрачный; по сканеру на поток */ unir_scanner_t* unir_scanner_create(const unir_settings_t* settings); /* NULL — stream() */ void unir_scanner_destroy(unir_scanner_t* scanner); /* ── результат ── */ typedef enum { UNIR_STATUS_OK = 0, UNIR_STATUS_NOT_FOUND = 1, UNIR_STATUS_NOT_DECODED = 2, UNIR_STATUS_UNLICENSED = 3, UNIR_STATUS_IMAGE_TOO_LARGE = 4, UNIR_STATUS_INVALID = 5, UNIR_STATUS_FAILED = 6, UNIR_STATUS_INTERNAL_ERROR = -1 } unir_status_t; typedef struct unir_scan_result unir_scan_result_t; /* непрозрачный */ unir_scan_result_t* unir_scanner_scan(unir_scanner_t* scanner, const unir_image_view_t* frame); /* NULL — внутренняя ошибка */ /* снимок с диска и коды на нём под одним сроком сканера; итог загрузки — * в *load_status (NULL — не нужен) */ unir_scan_result_t* unir_scanner_scan_file(unir_scanner_t* scanner, const char* path, unir_load_status_t* load_status); void unir_scan_result_free(unir_scan_result_t* result); unir_status_t unir_scan_result_status(const unir_scan_result_t* result); int unir_scan_result_timed_out(const unir_scan_result_t* result); double unir_scan_result_elapsed_ms(const unir_scan_result_t* result); int unir_scan_result_code_count(const unir_scan_result_t* result); uint32_t unir_scan_result_code_symbology(const unir_scan_result_t* result, int index); /* один бит */ const char* unir_scan_result_code_text(const unir_scan_result_t* result, int index); size_t unir_scan_result_code_text_len(const unir_scan_result_t* result, int index); void unir_scan_result_code_box(const unir_scan_result_t* result, int index, int* x, int* y, int* width, int* height); void unir_scan_result_code_corners(const unir_scan_result_t* result, int index, float corners_xy[8]); /* x0,y0 … x3,y3 */ int unir_scan_result_code_module_count(const unir_scan_result_t* result, int index); /* DataMatrix */ float unir_scan_result_code_module_px(const unir_scan_result_t* result, int index); /* линейные */ /* ── лицензия: JSON как у "unir --license … --json"; освобождать unir_free_string, NULL — внутренняя ошибка ── */ void unir_license_configure(const char* server_url, const char* app_version, int timeout_ms, int auto_check); /* timeout_ms <= 0 — 15000 */ char* unir_license_status_json(void); char* unir_license_activate_json(const char* key); char* unir_license_refresh_json(void); char* unir_license_deactivate_json(const char* reason); /* reason может быть NULL */ char* unir_license_deactivate_to_file_json(const char* path, const char* reason); char* unir_license_write_activation_request_json(const char* path); char* unir_license_import_license_file_json(const char* path); char* unir_license_fingerprint_json(void); /* {"fingerprint":"hex"} */ void unir_free_string(char* s); ``` ## Командная строка В поставку вместе с библиотекой входит утилита `unir` — для быстрой проверки без своего кода: разовый снимок, список путей, папка или поток для внешнего сервиса. Режимы и форматы — как у утилиты `dmr`, с колонкой символики. ``` unir <снимок> [ключи] unir --batch <список> [ключи] список путей, по одному на строку unir --folder <каталог> [ключи] все снимки каталога (без вложенных) unir --serve [ключи] путь на строку из stdin unir --license <подкоманда> лицензия unir --version версии unir и движков ``` | Ключ | Смысл | | --- | --- | | `--sym СПИСОК` | Символики через запятую, имена — как у `parseSymbologies`: `dm`, `128`, `gs1` (`sscc`), `ean` (`ean-13`, `ean-8`, `upc-a`, `upc-e`), `all` — умолчание | | `--codes N` | Сколько кодов искать: 1 — умолчание, 0 — все; любое значение, кроме 1, включает пресет `multi(N)` | | `--single` | Снимки не связаны между собой — пресет `single()`; умолчание — поток, `stream()` | | `--budget-ms N` | Предел времени на снимок, мс, один на все символики; 0 — без предела | | `--timeout-ms N` | Предохранитель, мс (10000); 0 — потолок 60 с | | `--max-side N` / `--max-pixels N` | Наибольшая сторона (16384) и площадь (100000000) снимка | | `--color` | Читать снимок в цвете; по умолчанию — в полутон | | `--corners` | В пакетных режимах — углы символа шестой колонкой | | `--root D` | В `--serve`: читать только снимки внутри каталога D | ### Разовый снимок ``` $ unir этикетка.jpg обработка: 24.9 мс код: datamatrix 0104627191145677215n5Gr,pHTGIU&93CuXU ``` Сообщения утилиты — на языке системы (на любой не русской — по-английски: `processing`, `code`); ключ `--lang ru|en` или переменная `LIBSCANNER_LANG` задают его явно. Строки `--batch`/`--serve` и вывод `--json` от языка не зависят. Та же переменная задаёт язык `status().message` и сообщений об ошибках в самой библиотеке. По строке `код: <символика> <текст>` на каждый код, символика — по `nameOf()`. В тексте GS показан как ``, TAB — как ``, переводы строк — пробелами. Если срок оборвал перебор, следом идёт строка `бюджет: исчерпан, перебор оборван`. Ошибки — в stderr. Результат — код возврата. Номера те же, что у `unir::Status`, с одним отличием: снимок, который не загрузился (нет файла, не JPEG/PNG, декодирование не уложилось в срок), даёт 1, а не 5 (`InvalidImage`), как вернул бы `scanFile()`; слишком большой снимок — 4, как и `ImageTooLarge`. Срок, оборвавший разбор уже загруженного снимка, кода не меняет: это 1 или 2 (или 0, если что-то успело прочитаться) и строка про бюджет. Неверные ключи командной строки — тоже 1. | Код | Смысл | | --- | --- | | 0 | Прочитан хотя бы один код (и когда срок оборвал перебор, но коды есть) | | 1 | Код не найден; также нет файла, не JPEG/PNG, вышел срок декодирования, неверные ключи; в `--serve` — путь вне каталога `--root` | | 2 | Найден кандидат, но не прочитан — это не значит, что код на снимке есть (раздел «Что на самом деле значит NotDecoded») | | 3 | Нет пробного периода и лицензии | | 4 | Снимок больше предела | | 5 | Противоречивый кадр; на снимках с диска утилита его не отдаёт — незагруженный файл даёт 1 | | 6 | Внутренняя ошибка; в `--serve` — ещё и неверный список символик в запросе | ### Пакетный режим и поток `--batch` и `--folder` — один процесс на весь заранее известный набор снимков, результат построчно на stdout, по строке на код: ``` путькод возвратамстекстсимволика[углы] ``` Первые четыре колонки — те же, что у утилиты `dmr`, так что разбор её вывода подходит и здесь. Снимок без кода — одна строка с пустым текстом и без колонки символики; во второй колонке — коды из таблицы выше. `--corners` добавляет шестую колонку `x0,y0 x1,y1 x2,y2 x3,y3`. Сводка — в stderr. Без лицензии прогон останавливается на первом снимке с кодом 3: остальные ответили бы так же. `--serve` — тот же формат строки, но снимки не известны заранее: путь приходит по одному, по строке из stdin, и обрабатывается сразу же — процесс держит сканер живым между запросами внешнего сервиса, не платя за инициализацию на каждый. После строк каждого снимка идёт строка `#END#`, и вывод сбрасывается сразу — по ней видно, что ответ закончен, даже когда кодов несколько. Строка запроса — путь или `путьsym=СПИСОК`: символики этого снимка в том же виде, что у `--sym`; на каждый набор заводится свой сканер со своей подсказкой DataMatrix. Без лицензии служба отвечает строкой снимка с кодом 3, `#END#` и выходит с кодом 3. Процесс завершается по концу ввода. ``` $ mkfifo queue $ unir --serve --codes 0 < queue & $ printf '/путь/к/снимку.jpg\tsym=dm,gs1\n' > queue /путь/к/снимку.jpg 0 91.24 0104627191145677215n5Gr,pHTGIU&93CuXU datamatrix #END# ``` ### Лицензия из командной строки ``` unir --license status [--json] unir --license activate <ключ> [--server URL] [--json] unir --license refresh [--server URL] [--json] unir --license request unir --license import unir --license deactivate [<файл>] [--server URL] [--json] unir --license fingerprint [--json] ``` `request` пишет файл запроса офлайн-активации, `import` принимает `license.lic` (и продление тоже), `deactivate` без файла отвязывает машину по сети, с файлом — пишет расписку для отправки с другой машины. Коды возврата: `status` — 0, если unir читает, и 3, если нет; остальные подкоманды — 0 (сделано) или 1 (не удалось). Флаг `--json` даёт тот же результат одной JSON-строкой на stdout — для скриптов и админ-панелей. Поля, их порядок и коды возврата — точно как у `dmr --license … --json`, поэтому сервис, который уже разбирает вывод dmr, может звать `unir` вместо него: ``` $ unir --license status --json {"state":"trial","can_scan":true,"valid_until":1792831857,"refresh_after":0, "offline_until":0,"days_left":28,"license_id":"","activation_id":"", "offline_mode":"","clock_rollback":false,"message":"Пробный период: осталось 28 дн."} ``` Операции (`activate`, `refresh`, `deactivate`) отвечают объектом `{"ok":…,"error":…,"message":…,"status":{…}}`, где `status` — состояние в том же виде, что выше; `fingerprint` — `{"fingerprint":"…"}`. Поле `state` — устойчивый машиночитаемый код (`trial`, `active`, `refresh_due`, `trial_expired`, `expired`, `network_required`, `wrong_machine`, `invalid`) — часть формата вывода, не переводится и не меняется словами. --- # Библиотека чтения машиночитаемых кодов 1.0 — Python > Один вызов читает на кадре DataMatrix, GS1-128 (SSCC), Code 128, EAN-13, EAN-8, UPC-A и UPC-E — маркировку «Честного знака», логистические этикетки и товарные штрихкоды. Страница на сайте: https://libscanner.com/docs/universal-reader/1.0?lang=python ## Обзор Пакет `unir` одним вызовом читает на кадре коды разных символик: **DataMatrix (ECC200)** — маркировку «Честного знака», **GS1-128** — SSCC логистических этикеток, **Code 128** и товарные штрихкоды **EAN-13, EAN-8, UPC-A и UPC-E**. Подходит для кадров камеры над конвейером и для фотографий этикеток. Это обёртка на `ctypes` поверх плоского C ABI библиотеки (`unir_c`) — прямой вызов в процессе, без запуска внешней программы на каждый кадр. Внешних зависимостей у пакета нет, только стандартная библиотека. Сама библиотека (`unir_c.dll` на Windows, `libunir_c.so` на Linux) и утилита командной строки `unir` лежат внутри колеса уже собранными — компилировать ничего не нужно. ## Установка ```bash pip install libscanner-unir ``` pip сам выберет колесо своей платформы; колесо одно на платформу и годится для любого Python 3.9 и новее. Без доступа к PyPI — то же колесо из раздела «Загрузки» личного кабинета: **Windows:** ```bash pip install .\libscanner_unir-1.0.1-py3-none-win_amd64.whl ``` **Linux:** ```bash pip install ./libscanner_unir-1.0.1-py3-none-manylinux_2_36_x86_64.whl # x86-64 pip install ./libscanner_unir-1.0.1-py3-none-manylinux_2_36_aarch64.whl # ARM64 pip install ./libscanner_unir-1.0.1-py3-none-manylinux_2_36_armv7l.whl # ARMv7 ``` Пакет называется `libscanner-unir`, импортируется как `unir`. Платформы: Windows x64, Linux x86-64, ARM64 и ARMv7 (Raspberry Pi OS 64 и 32 бит). Колёса для Linux требуют glibc 2.36 и новее: Debian 12, Ubuntu 23.04+, Raspberry Pi OS Bookworm. Вместе с пакетом в окружение ставится команда `unir` (и `python -m unir`) — см. раздел «Командная строка». Рядом с колёсами в «Загрузках» лежат и архивы на каждую платформу: `unir-1.0.1-<платформа>.zip` (Windows) или `.tar.gz` (Linux) — библиотека для программ на других языках, и `unir-1.0.1-<платформа>-cli` — только утилита `unir`, для проверок без Python. Для колёс они не нужны. ARMv7 (32-битная ОС) в «Загрузках» подписан платформой `linux-armhf`, его колесо — с тегом `manylinux_2_36_armv7l`. Библиотека лежит внутри пакета, в `unir/_lib/`, и грузится только оттуда, по полному пути: поиска по `PATH` и текущему каталогу нет намеренно, чтобы подложенная туда чужая `unir_c.dll` не исполнялась с правами вашей программы. Переменная окружения `UNIR_C_LIBRARY` (полный путь к файлу; относительный — `OSError`) нужна, только если в пакете нет `_lib` — у установленного колеса она ни на что не влияет. Библиотека другой старшей версии интерфейса отвергается при импорте тоже с `OSError`: пакет и библиотека должны быть из одной поставки. ## Быстрый старт Все коды на снимке с диска: ```python import unir result = unir.scan_file("label.jpg", unir.Settings.multi(0)) # 0 — сколько найдётся for code in result.codes: print(code.symbology_name, code.text) # datamatrix b'0104601234500470215...\x1d91...\x1d92...' # gs1-128 b'00246009834320769650' ``` Только нужные символики и предел времени на кадр: ```python s = unir.Settings.single() s.symbologies = unir.Symbology.DATAMATRIX | unir.Symbology.GS1_128 s.budget_ms = 200 scanner = unir.Scanner(s) # создаётся один раз, а не на кадр result = scanner.scan_file("frame.png") if result.ok: print(result.codes[0].symbology_name, result.text) elif result.status in (unir.Status.NOT_FOUND, unir.Status.NOT_DECODED): print("код не прочитан") # NOT_DECODED — тоже «нет кода» else: print("не прочитан:", result.status.name) ``` Кадр из чужого буфера — без копирования, видом на него (буфер обязан быть жив на время вызова `scan()`): ```python view = unir.ImageView(buf, width, height, stride, unir.PixelFormat.GRAY8) result = scanner.scan(view) ``` > **Сканер создаётся один раз, а не на кадр.** В режиме потока кадров (`Settings.stream()`) ридер DataMatrix запоминает размер символа от кадра к кадру, и эта подсказка обязана пережить кадр — создавайте `Scanner` один раз на поток кадров. При создании сканера же проходит первая проверка лицензии. Вызовы одного сканера из разных потоков идут по очереди; параллельно — по сканеру на поток. ## Модули Публичные имена собраны прямо в пакете `unir` — импортировать из подмодулей не нужно, кроме `unir.license`, у которого своё пространство имён: | Имя | Что это | | --- | --- | | `unir.Scanner` / `unir.scan` / `unir.scan_file` | Объект-сканер и разовые функции без объекта | | `unir.ScanResult` / `unir.Code` / `unir.Status` | Результат чтения кадра, один прочитанный код, итог кадра | | `unir.Box` / `unir.Point` | Геометрия прочитанного кода — поля `Code` | | `unir.Symbology` / `unir.symbology_name` / `unir.parse_symbologies` | Символики: биты маски, короткие имена, маска из строки | | `unir.Image` / `unir.ImageView` | Кадр — владеющий (прочитан с диска) и вид на чужой буфер | | `unir.PixelFormat` / `unir.channels_of` / `unir.LoadAs` / `unir.LoadStatus` | Формат пикселя и число его каналов, режим и итог загрузки с диска | | `unir.Settings` / `unir.Mode` | Настройки сканера, пресеты и режим разбора DataMatrix | | `unir.UnirError` | Исключение: сбой самой библиотеки, а не отказ чтения | | `unir.license` | Подмодуль лицензии — `import unir.license as license` | | `unir.version()` / `unir.engine_versions()` / `unir.abi_version()` | Версия библиотеки, версии движков внутри неё, версия интерфейса библиотеки | ## Scanner `Scanner` держит ресурс библиотеки — освобождайте его явно `close()` или используйте как контекстный менеджер (`with`); `__del__` подстраховывает, но время сборки мусора не гарантировано. ```python class Scanner: def __init__(self, settings: Settings | None = None): """None — Settings.stream(): поток кадров одной камеры.""" def scan(self, frame: ImageView) -> ScanResult: """Прочитать коды на кадре.""" def scan_file(self, path) -> ScanResult: """Снимок с диска (JPEG, PNG) и коды на нём под ОДНИМ сроком кадра (budget_ms, timeout_ms; загрузка в него входит). Снимок читается в полутоне. Не загрузился — status IMAGE_TOO_LARGE или INVALID, подробно — в load_status.""" def close(self) -> None: ... def __enter__(self) -> "Scanner": ... def __exit__(self, *exc) -> None: ... def scan(frame: ImageView, settings: Settings | None = None) -> ScanResult: """Разовое чтение кадра без объекта; None — Settings.single().""" def scan_file(path, settings: Settings | None = None) -> ScanResult: """Разовое чтение снимка с диска: загрузка и разбор под одним сроком; None — Settings.single().""" ``` Обратите внимание на умолчания: `Scanner()` без настроек — это поток кадров (`Settings.stream()`), а разовые `unir.scan()` и `unir.scan_file()` — несвязанные снимки (`Settings.single()`): копить подсказку между кадрами им нечему. Разовая `unir.scan_file()` сама создаёт сканер, разбирает снимок и закрывает сканер, а результат — обычные объекты Python без ресурсов библиотеки: освобождать после неё ничего не нужно. Для многих файлов подряд сканер выгоднее создать один раз — разовая функция заводит его на каждый вызов. Если снимок не загрузился, `scan_file()` не бросает исключение: причина — в `load_status`, статус результата — по таблице: | Что с файлом | `load_status` | `status`, `timed_out` | | --- | --- | --- | | Прочитан | `OK` | итог разбора | | Файла нет | `NOT_FOUND` | `INVALID`, `False` | | Не JPEG/PNG или повреждён | `UNREADABLE` | `INVALID`, `False` | | Декодирование не уложилось в срок | `TIMED_OUT` | `INVALID`, `True` | | Больше пределов по заголовку | `TOO_LARGE` | `IMAGE_TOO_LARGE`, `False` | | Нет лицензии — файл не открывался | `OK` | `UNLICENSED`, `False` | `INVALID_ARGUMENT` у `scan_file()` не встречается: настройки проверяются раньше, при создании сканера. ```python with unir.Scanner(unir.Settings.single()) as scanner: for path in ["photo-1.jpg", "photo-2.png"]: result = scanner.scan_file(path) print(path, result.status.name, [c.text for c in result.codes]) ``` `scan()` и `scan_file()` у закрытого сканера бросают `ValueError`. Настройки проверяются при создании сканера: значение вне допустимого диапазона или неизвестный бит символики — тоже `ValueError`. > **Потоки.** Вызовы одного `Scanner` из разных потоков пакет выстраивает в очередь блокировкой — так безопасно, но не параллельно. Чтобы кадры разбирались параллельно, заведите по сканеру на поток: объект дешёвый, создать его можно заранее. ## Результат чтения ### Status ```python class Status(enum.IntEnum): OK = 0 # прочитан хотя бы один код NOT_FOUND = 1 # ни одного символа не найдено NOT_DECODED = 2 # символ найден, но не прочитан UNLICENSED = 3 # нет лицензии, пробный период кончился; кадр не разбирался IMAGE_TOO_LARGE = 4 # кадр больше Settings.max_image_side / max_image_pixels INVALID = 5 # описание кадра противоречиво; у scan_file — снимок не прочитан FAILED = 6 # разбор оборвался внутренней ошибкой ``` `OK` всегда приходит хотя бы с одним кодом; при любом другом статусе `codes` пуст. При `UNLICENSED` кадр не разбирается вовсе, а `scan_file()` даже не открывает файл (`load_status` при этом `OK`). Номера статусов совпадают с кодами возврата утилиты `unir`, кроме снимка, который не загрузился: у `scan_file()` это `INVALID` (5), а утилита отвечает 1 (раздел «Командная строка»). Если ни один движок ничего не прочитал, статус — самый весомый из их ответов: `FAILED`, затем `INVALID`, `IMAGE_TOO_LARGE`, `NOT_DECODED` и, наконец, `NOT_FOUND`. ### Что на самом деле значит NOT_DECODED `NOT_DECODED` — не «код точно есть». Это значит: хотя бы один движок маски нашёл на кадре *кандидата*, похожего на символ своей символики, и не смог его прочитать. Кандидат — ещё не символ. Движок DataMatrix берёт в кандидаты всё, что похоже на сплошную L-рамку или на текстуру модулей, — ими бывают и штрихи линейного кода, и текст, и узор упаковки. Линейный движок отмечает участок штрихов, который частично подошёл под шаблон Code 128 или EAN/UPC, но не прошёл приёмку. Поэтому ответ 2 обычен и на кадрах, где кода этой символики нет вовсе: маска только `DATAMATRIX` на кадре с одним GS1-128, маска только `EAN_UPC` на кадре с одним DataMatrix. Статус считается только по движкам, которые есть в маске: код символики вне маски не ищется, и его штрихи или модули могут дать кандидата чужому движку — отсюда `NOT_DECODED`, а не `NOT_FOUND`. Линейный код, который движок прочитал, но маска не пропускает (SSCC при маске только `CODE128`), отбрасывается и тоже не даёт `OK`. Относитесь к `NOT_DECODED` как к «кода не прочитано»: на конвейере — берите следующий кадр, на разовом снимке — просите переснять или повторите разбор с бо́льшим сроком. Как признак «код есть, но плохой» (для отбраковки) его не используйте. Если вместе с `NOT_FOUND` или `NOT_DECODED` пришёл `timed_out = True`, ответ не окончательный: разбор оборвал срок, и с бо́льшим сроком код мог бы прочитаться. Для своей программы статусы удобно свести к трём исходам: | Status | Исход | Что делать | | --- | --- | --- | | `OK` | Есть код | Брать `codes`. При `timed_out` часть символик или кодов могла остаться непроверенной | | `NOT_FOUND` | Нет кода | Следующий кадр. При `timed_out` — не окончательно | | `NOT_DECODED` | Нет кода (не прочитан) | Следующий кадр или пересъёмка; не считать, что код на кадре есть | | `UNLICENSED` | Ошибка | Нет лицензии: показать `license.status()["message"]`; повторять кадр бесполезно до активации | | `IMAGE_TOO_LARGE` | Ошибка входа | Уменьшить кадр или поднять `max_image_side` / `max_image_pixels` | | `INVALID` | Ошибка входа | У `scan()` — ошибка в описании кадра; у `scan_file()` — файл не прочитан, причина в `load_status` | | `FAILED` | Ошибка | Внутренний сбой (например, нехватка памяти); кадр можно повторить | ```python def outcome(result: unir.ScanResult) -> str: """'code' — есть код, 'none' — нет кода, 'error' — ошибка.""" if result.ok: return "code" if result.status in (unir.Status.NOT_FOUND, unir.Status.NOT_DECODED): return "none" return "error" ``` ### ScanResult ```python @dataclass(frozen=True) class ScanResult: status: Status codes: list[Code] # в порядке чтения, см. «Символики» timed_out: bool # хоть один движок оборван по сроку кадра или не успел начать elapsed_ms: float load_status: LoadStatus | None = None # итог загрузки у scan_file(); у scan() — None @property def ok(self) -> bool: ... # status == Status.OK @property def text(self) -> bytes: ... # текст первого кода или b"" ``` Кадр, оборванный по сроку (`Settings.budget_ms` или `timeout_ms`), отдаёт то, что успели прочитать, с `timed_out = True`. ### Code ```python @dataclass(frozen=True) class Code: symbology: Symbology # ровно один бит text: bytes # НЕ str — FNC1 внутри данных остаётся байтом 0x1D box: Box # x, y, width, height — прямоугольник по осям, пиксели кадра corners: tuple # четыре Point по кругу module_count: int # DataMatrix: модулей по стороне (0 — неизвестно или линейный код) module_px: float # линейные: ширина модуля, пикселей (0 — неизвестно или DataMatrix) @property def symbology_name(self) -> str: ... # "datamatrix", "gs1-128", "ean-13"… class Box(NamedTuple): x: int y: int width: int height: int class Point(NamedTuple): x: float y: float ``` > **Байты, а не строка.** `Code.text` — `bytes`. Ведущий FNC1 (признак GS1) в текст не попадает, а FNC1 внутри данных — разделитель GS, байт `0x1D` как есть, без замены печатным видом. Текст может содержать и байт 0. Декодируйте сами тем способом, который подходит вашему формату: > > ```python > code.text.decode("ascii") # EAN/UPC и SSCC — только цифры > code.text.replace(b"\x1d", b"").decode("ascii") # код «Честного знака» для показа > ``` Что приходит в тексте: - **GS1** (DataMatrix с FNC1 в начале, GS1-128) — без ведущего FNC1, разделители — байт `0x1D`. Код маркировки выглядит так: `0104601234500470215xX…\x1d91…\x1d92…`. - **SSCC** — 20 цифр с «00» в начале: `00246009834320769650`. - **EAN-13** — 13 цифр, **EAN-8** — 8. - **UPC-A** — 12 цифр. EAN-13 с ведущим нулём — это UPC-A, и такой код всегда приходит как `upc-a`, 12 цифрами. - **UPC-E** — 8 цифр (система счисления, 6 цифр, контрольная), без разворота в UPC-A. - **DataMatrix без GS1** — байты символа как есть, без перекодирования. 0x00–0x7F — ASCII, байты 0x80–0xFF (Upper Shift, режим Base 256) отдаются как есть: по стандарту DataMatrix это ISO 8859-1 (Latin-1), но многие генераторы кладут в Base 256 текст UTF-8, и библиотека кодировку не угадывает. ECI не поддерживается: переключение ECI не разбирается, и текст символа с ECI может содержать служебные байты; макросы 05/06 не разворачиваются, Structured Append не собирается. - **Code 128 без GS1** — наборы A, B, C и SHIFT, текст всегда 7-битный (0x00–0x7F; набор A даёт и управляющие знаки). FNC2, FNC3 и FNC4 не поддерживаются: символ с ними не читается, так что расширенных знаков Latin-1 в тексте не бывает. Перевести текст в строку для показа: ```python def text_of(code: unir.Code) -> str: if code.symbology != unir.Symbology.DATAMATRIX: return code.text.decode("ascii") # Code 128, GS1-128, EAN/UPC — всегда 7 бит try: return code.text.decode("utf-8") # частый случай для Base 256 except UnicodeDecodeError: return code.text.decode("latin-1") # умолчание стандарта DataMatrix; не падает никогда ``` Разделитель `0x1D` после `decode()` остаётся знаком `"\x1d"`; для показа замените его, например, на `""`. Углы `corners` точнее `box`: символ на кадре может быть повёрнут. У DataMatrix `corners[3]` — угол, который детектор принял за угол сплошной L-рамки; для разметки годится, для выводов о повороте содержимого — нет. У Code 128, GS1-128 и EAN/UPC `corners[0]` — на стороне старта, `corners[1]` — на стороне стопа по тому же краю штрихов, `corners[2]` и `corners[3]` — на противоположном краю. ## Символики Каждая символика — отдельный бит: `Settings.symbologies` — их маска, что искать, а `Code.symbology` — ровно один бит, что прочитано. ```python class Symbology(enum.IntFlag): NONE = 0x00 DATAMATRIX = 0x01 # ECC200, ГОСТ Р ИСО/МЭК 16022 CODE128 = 0x02 # Code 128 без признака GS1 GS1_128 = 0x04 # GS1-128; без CODE128 в маске читается только SSCC (AI 00) EAN13 = 0x08 EAN8 = 0x10 UPCA = 0x20 # 12 цифр UPCE = 0x40 # 8 цифр, без разворота в UPC-A EAN_UPC = EAN13 | EAN8 | UPCA | UPCE ALL = DATAMATRIX | CODE128 | GS1_128 | EAN_UPC ``` ```python s = unir.Settings.stream() s.symbologies = unir.Symbology.DATAMATRIX | unir.Symbology.GS1_128 # «Честный знак» и SSCC s.symbologies = unir.Symbology.EAN_UPC # только товарные штрихкоды scanner = unir.Scanner(s) ``` | Символика | Что читается | | --- | --- | | `DATAMATRIX` | DataMatrix ECC200, квадратные символы от 10×10 до 144×144 — тем же движком, что у «Чтения DataMatrix». Прямоугольные DMRE не читаются: их нет в ГОСТ и в кодах маркировки «Честного знака» | | `GS1_128` | Code 128 с FNC1 в начале. Если в маске нет `CODE128`, читается только SSCC (AI 00) — это быстрый путь; вместе с `CODE128` — и прочие GS1-128 | | `CODE128` | Code 128 без признака GS1. Приём строже, чем у SSCC: должны совпасть не меньше двух сканлайнов. На кадре без штрихкода — самый дорогой поиск из всех: не включайте его без нужды | | `EAN13`, `EAN8`, `UPCA`, `UPCE` | Приём — согласие двух сканлайнов и свободные зоны. Движок читает все четыре вместе, но отдаются только те, что в маске. Дополнения EAN на 2 и 5 цифр не читаются | > **Умолчание — все символики сборки.** `Settings.symbologies` по умолчанию — `Symbology.ALL`, то есть всё, что умеет эта сборка. В следующих версиях добавятся QR и другие символики, и `ALL` начнёт искать и их — а значит, и тратить на них время кадра. Если нужен фиксированный набор, задавайте маску явно. ### Имена ```python unir.symbology_name(unir.Symbology.EAN13) # "ean-13" unir.symbology_name(unir.Symbology.EAN_UPC) # "unknown" — маска, а не один бит code.symbology_name # то же для прочитанного кода unir.parse_symbologies("dm,gs1") # Symbology.DATAMATRIX | Symbology.GS1_128 unir.parse_symbologies("ean") # Symbology.EAN_UPC unir.parse_symbologies("qr") # ValueError: неизвестная символика ``` Короткие имена: `datamatrix`, `code128`, `gs1-128`, `ean-13`, `ean-8`, `upc-a`, `upc-e`; для `0` — `none`. `parse_symbologies` принимает список через запятую — тот же, что у ключа `--sym` утилиты: `datamatrix` | `dm`, `code128` | `128`, `gs1-128` | `gs1` | `sscc`, `ean-13` | `ean13`, `ean-8` | `ean8`, `upc-a` | `upca`, `upc-e` | `upce`, `ean` — все четыре EAN/UPC, и `all`. Удобно, когда набор символик приходит из конфигурации. Признак GS1 (FNC1) у DataMatrix в этой версии наружу не отдаётся: GS1-структуру DataMatrix различайте по содержимому текста. ### Порядок разбора и порядок кодов - **Один код** (`max_codes = 1`): сначала быстрый проход по SSCC и EAN/UPC, затем DataMatrix, затем прочий Code 128. Первый прочитанный код и есть ответ — следующие движки уже не зовутся. - **Несколько кодов** (`max_codes` 0 или больше 1): сначала все линейные символики, затем DataMatrix. В `codes` — сперва линейные коды, потом DataMatrix. Символики, которых нет в маске, в разбор не попадают и времени не стоят — сузить маску под то, что действительно есть на ваших кадрах, это главный способ сэкономить время. ## Кадр ### PixelFormat ```python class PixelFormat(enum.IntEnum): GRAY8 = 0 # один канал: яркость; для сканера самый дешёвый BGR8 = 1 # три канала, порядок OpenCV RGB8 = 2 # три канала BGRA8 = 3 # четыре канала, порядок OpenCV и Windows RGBA8 = 4 # четыре канала unir.channels_of(unir.PixelFormat.BGR8) # 3 ``` Сканер работает с яркостью и цветной кадр переводит в неё сам. Если камера монохромная, отдавайте кадр как `GRAY8`, а не размноженным в три канала: ответ тот же, а обработка дешевле. ### ImageView Вид на чужой буфер — библиотека его не копирует, не освобождает и не переживает; буфер обязан быть жив всё время вызова `scan()`. Сам `ImageView` держит на него ссылку. Годится любой объект с buffer-протоколом: `bytearray`, numpy-массив, `mmap`. ```python class ImageView: def __init__(self, buf, width: int, height: int, stride: int, format: PixelFormat): ... # stride — байт на строку; 0 — строки вплотную (width * каналов) width: int; height: int; stride: int; format: PixelFormat # только чтение import numpy as np frame = np.zeros((480, 640), dtype=np.uint8) # кадр камеры в полутоне view = unir.ImageView(frame, 640, 480, frame.strides[0], unir.PixelFormat.GRAY8) result = scanner.scan(view) ``` Размеры проверяются до вызова библиотеки: буфер короче, чем нужно кадру с такими `width`, `height`, `stride` и форматом, `stride` меньше `width` × каналов или отрицательное значение — `ValueError`. Для неписабельных буферов (`bytes`, memoryview только для чтения) пакет делает одну явную копию; писабельные передаются без копирования. ### Image Кадр, которым владеет сама библиотека, — читается с диска. Форматы: **JPEG и PNG**. Другие форматы раскодируйте сами (например, Pillow) и передайте пиксели как `unir.ImageView`. ```python class LoadAs(enum.IntEnum): COLOR = 0 # PixelFormat.BGR8 GRAY = 1 # PixelFormat.GRAY8 — сканеру этого достаточно class LoadStatus(enum.IntEnum): OK = 0 NOT_FOUND = 1 # файла нет UNREADABLE = 2 # не снимок или повреждён TOO_LARGE = 3 # по заголовку больше Settings.max_image_side / max_image_pixels TIMED_OUT = 4 # декодирование не уложилось в срок (budget_ms / timeout_ms) INVALID_ARGUMENT = 5 # настройки отвергнуты библиотекой class Image: def __init__(self, path, as_: LoadAs = LoadAs.GRAY, settings: Settings | None = None): ... status: LoadStatus def view(self) -> ImageView: ... def close(self) -> None: ... with unir.Image("frame.png", unir.LoadAs.GRAY) as img: if not img: print("не загрузился:", img.status.name) else: view = img.view() result = scanner.scan(view) del view # вид держит кадр: close() при живом виде — RuntimeError ``` Неудачная загрузка не бросает исключение: объект ложен, причину говорит `status`, а `view()` у такого объекта бросает `ValueError`. Из `settings` берутся предел размера кадра и срок декодирования (меньший из `budget_ms` и `timeout_ms`); `None` — умолчания: 16384 по стороне, 100 млн пикселей, 10 с. Кадр больше предела не декодируется — размер проверяется по заголовку файла, до чтения пикселей. `close()`, пока жив хоть один вид от `view()`, бросает `RuntimeError`: вид указывает в память кадра. Если нужен только результат, `Scanner.scan_file()` проще: загрузка и разбор там идут под одним сроком кадра, а снимок читается сразу в полутоне. > **Кодировка путей.** Путь строкой Python или `pathlib.Path` пакет передаёт в библиотеку в UTF-8 — открываются имена с кириллицей и любыми другими знаками. Готовые `bytes` уходят как есть: в Windows это UTF-8 или системная кодовая страница (`cp1251` на русской локали), в Linux — байты имени файловой системы. ## Настройки Пресеты заполняет сама библиотека, а не повторённые в Python числа: умолчания живут в одном месте. Начинайте с пресета и меняйте поля, как у обычного dataclass: ```python settings = unir.Settings.stream() # поток кадров одной камеры; умолчание Scanner() settings = unir.Settings.single() # несвязанные снимки: папка, файл, фото с телефона settings = unir.Settings.multi(0) # несколько кодов на кадре; 0 — сколько найдётся settings.budget_ms = 120.0 scanner = unir.Scanner(settings) ``` | Пресет | Когда | Что делает | | --- | --- | --- | | `stream()` | Поток кадров одной камеры: конвейер, сканер в руке | Ридер DataMatrix запоминает размер символа между кадрами и не перебирает размеры заново; один код | | `single()` | Несвязанные снимки | Ридер DataMatrix перебирает стандартные размеры, если не смог измерить; один код | | `multi(max_codes)` | Нужно несколько кодов на кадре | До `max_codes` кодов, 0 — все | > **stream() — не для папки снимков.** Для набора несвязанных снимков режим `stream()` даёт заметно меньше чтений DataMatrix: подсказка размера от прошлого кадра к следующему снимку не подходит и мешает. Для файлов берите `single()` или `multi()`. ### Поля Settings | Поле | Умолчание | Смысл | | --- | --- | --- | | `symbologies` | `Symbology.ALL` | Маска символик, см. «Символики»; неизвестный бит — `ValueError` | | `max_codes` | 1 | 1 — до первого прочитанного кода любой символики, N — до N, 0 — все | | `mode` | `Mode.STREAM` | Режим разбора DataMatrix: `STREAM`, `SINGLE`, `MULTI`; пресеты ставят его сами | | `budget_ms` | 0.0 | Предел времени на кадр, мс, — один на все символики: движки делят его по очереди, загрузка файла в `scan_file()` входит в него же. 0 — без предела (действует `timeout_ms`) | | `timeout_ms` | 10000.0 | Предохранитель, мс: тот же обрыв, но способа работы движков не меняет; 0 — потолок 60 с | | `max_image_side` / `max_image_pixels` | 16384 / 100000000 | Наибольший кадр к разбору; больше — `Status.IMAGE_TOO_LARGE` без разбора. 0 — потолок библиотеки: 65535 по стороне, 2²⁹ пикселей | `budget_ms` и `timeout_ms` обрывают разбор одинаково. Разница в том, что заданный `budget_ms` меняет и способ работы ридера DataMatrix (выборка под срок), а `timeout_ms` — только страховка от кадра, который разбирается слишком долго. ### Скорость: режим, маска, срок Время кадра определяют три настройки, и все три стоит задавать явно, а не оставлять умолчания. - **Режим.** С одним кодом (`stream()`, `single()`) разбор кончается на первом прочитанном коде, а первым идёт самый дешёвый проход — SSCC и EAN/UPC: кадр с одним штрихкодом читается быстро, и до DataMatrix дело не доходит. `multi(0)` остановиться не может: каждый движок маски проходит весь кадр до конца, линейный ищет все Code 128 и EAN/UPC, движок DataMatrix перебирает всех своих кандидатов. На кадре с одним кодом это в разы, а на слабой машине — на порядок дольше режима одного кода. Если число кодов известно, задавайте `multi(n)`: разбор кончится, как только их наберётся n. - **Маска.** Каждая символика вне маски — движок или проход, которого нет. Дороже всего обходятся «ничего не найдено» на кадре без кода: прочий `CODE128` примеряется ко всему, что похоже на штрихи, а движок DataMatrix уходит в глубокий перебор. SSCC читается и без `CODE128` — по одному `GS1_128`. Маска только из нужных символик заметно быстрее `ALL`, маска из одной DataMatrix — тем более. - **Срок.** `budget_ms` — один срок на кадр для всех символик. Движки тратят его по очереди; кончился — идущий движок обрывается на ближайшей стадии, следующие не начинаются, в результате `timed_out = True`. Прочитанное до срока остаётся в `codes`. Без `budget_ms` кадр ограничен только предохранителем `timeout_ms` (10 с), и в `multi(0)` на трудном кадре это ощутимо. Практически: на конвейере срок — интервал между кадрами за вычетом своей обработки; для разового снимка, ответа на который ждёт человек, в `multi()` — столько, сколько он готов ждать. У `scan_file()` в этот же срок входит загрузка файла: ```python s = unir.Settings.multi(0) s.symbologies = unir.Symbology.DATAMATRIX | unir.Symbology.GS1_128 # только нужное s.budget_ms = 3000 # загрузка и разбор — не дольше ~3 с result = unir.scan_file("label.jpg", s) ``` ## Версия библиотеки ```python print(unir.version()) # "1.0.1" — версия собранной библиотеки print(unir.engine_versions()) # "dmr 1.0.12, ssccr 0.1.0" — движки внутри неё print(unir.abi_version()) # "1.0" — версия интерфейса библиотеки, отдельно от версии алгоритма ``` ## Лицензия Библиотека читает коды, пока идёт пробный период (30 дней с первого запуска на машине, без регистрации и без сети) или действует лицензия. Иначе `scan()` и `scan_file()` сразу отвечают `Status.UNLICENSED`. Лицензия привязана к оборудованию (материнская плата, процессор, системный диск), а не к установке ОС: переустановка Windows и замена диска или процессора её не отбирают, замена материнской платы — отбирает. Пробный период и лицензия unir — свои, независимые от других продуктов libscanner на той же машине. ```python import unir.license as license s = license.status() if not s["can_scan"]: print(s["message"]) # состояние словами, для показа пользователю r = license.activate("UNIR-XXXXX-XXXXX-XXXXX-XXXXX") if not r["ok"]: print(r["error"], r["message"]) ``` Отказ операции (неверный ключ, сеть недоступна) — не исключение, а обычный `dict` с причиной. `unir.UnirError` поднимается, только если сбой в самой библиотеке. ### Файл ключа unir.key Основной способ активации — вписать ключ лицензии (личный кабинет, страница лицензии — «Показать ключ») в текстовый файл `unir.key` и положить его в каталог `unir/_lib/` установленного пакета, рядом с библиотекой и утилитой (ключ у них общий). Каталог покажет: ```bash python -c "import unir, os; print(os.path.join(os.path.dirname(unir.__file__), '_lib'))" ``` Если там файла нет, библиотека ищет `unir.key` ещё рядом с исполняемым файлом процесса; текущий каталог и переменные окружения не просматриваются. Ключ — первая строка файла, которая не пуста и не комментарий `#`, в том виде, в каком выдан; файл больше 4 КБ не читается. Имя своё, не `license.key`, как у «Чтения DataMatrix»: ключи разных продуктов не путаются. Вызывать ничего не нужно: ключ активируется по сети сам при первом обращении к библиотеке — создании первого `Scanner` или первом вызове `unir.license`; этот вызов один раз ждёт ответа сервера. Не вышло — причина в `status()["message"]`; без сети попытка повторяется раз в час, при отказе сервера — раз в сутки. Файл, положенный при работающей программе, подхватывается в течение часа. ### Где хранится состояние Состояние пробного периода и лицензии лежит в нескольких копиях; берётся самое раннее начало пробного периода: **Windows:** ```bash %ProgramData%\unir\license.dat %LOCALAPPDATA%\unir\license.dat HKCU\Software\unir, значение state ``` **Linux:** ```bash /var/lib/unir/license.dat ~/.config/unir/license.dat ``` На Linux домашний каталог берётся из базы пользователей системы, а не из `$HOME`: подменой переменной новый пробный период не получить. Общую копию в `/var/lib/unir` может создать только root: под обычным пользователем каталог не создаётся, и эта копия молча пропускается — состояние живёт в `~/.config/unir`. Ошибкой это не считается. У контейнера нет ни данных DMI, ни своего диска, поэтому лицензию к нему не привязать — запускайте программу на хосте или в виртуальной машине. Пробный период заводится при первом обращении к библиотеке на машине — создании первого `Scanner` или первом `license.status()` — и записывается на диск сразу, а не при выходе из программы. ### Что возвращают функции `status()` — словарь состояния; функции, которые что-то делают с лицензией, — словарь операции, где `status` — состояние уже после неё. Это тот же JSON, что у `unir --license … --json`: ```python # license.status() {"state": "trial", "can_scan": True, "valid_until": 1795508197, "refresh_after": 0, "offline_until": 0, "days_left": 30, "license_id": "", "activation_id": "", "offline_mode": "", "clock_rollback": False, "message": "..."} # license.activate(...), refresh(), deactivate(), ... {"ok": False, "error": "KEY_INVALID", "message": "...", "status": {...}} ``` | Ключ | Смысл | | --- | --- | | `state` | `trial` — пробный период; `active` — лицензия действует; `refresh_due` — действует, но связи с сервером давно не было; `trial_expired`; `expired` — оплаченный срок кончился; `network_required` — дальше без связи с сервером нельзя; `wrong_machine` — лицензия другой машины; `invalid` — запись не проходит проверку подписи | | `can_scan` | Читает ли сейчас библиотека: `True` при `trial`, `active` и `refresh_due` | | `valid_until` | Конец пробного периода или оплаченного срока, unix-секунды UTC | | `refresh_after` / `offline_until` | С какого момента нужна сверка с сервером и дольше какого лицензия без связи не работает; 0 — без ограничения | | `days_left` | Целых суток до ближайшей из границ; 0 — граница пройдена | | `license_id` / `activation_id` / `offline_mode` | Лицензия, активация и режим работы без связи (`limited`, `extended`, `full`); пусто без лицензии | | `clock_rollback` | Часы машины отстают от уже виденного времени — откат часов срок не продлевает | | `message` | Состояние или итог операции словами, для показа пользователю | | `ok` / `error` | Итог операции и код отказа: ответ сервера как есть (`LICENSE_NOT_FOUND`, `ACTIVATION_LIMIT_REACHED`, `LICENSE_REVOKED` и другие) либо свой (`KEY_INVALID`, `NETWORK`, `NO_LICENSE`, `WRONG_PRODUCT` и другие) | ### Функции ```python def configure(server_url=None, app_version=None, timeout_ms=None, auto_check=True) -> None: ... def status() -> dict: ... def activate(key: str) -> dict: ... # онлайн, ключ UNIR-XXXXX-... def refresh() -> dict: ... # сверить сейчас, не дожидаясь суток def deactivate(reason: str | None = None) -> dict: ... # отвязать по сети def deactivate_to_file(path, reason: str | None = None) -> dict: ... # отвязать без сети def write_activation_request(path) -> dict: ... # запрос офлайн-активации def import_license_file(path) -> dict: ... # принять license.lic; годится и для продления def fingerprint() -> str: ... # отпечаток машины, hex — для поддержки ``` `configure()` вызывают до создания первого `Scanner`, если вообще нужно: активация ключа из `unir.key` идёт при первом обращении к библиотеке и берёт сервер и `app_version` (версия вашей программы, видна в кабинете рядом с машиной) уже отсюда. `timeout_ms` — предел ожидания одного запроса к серверу, по умолчанию 15 с. Активированная по сети лицензия раз в сутки сама сверяется с сервером в фоновом потоке; чтение кадра сеть не ждёт никогда. Выключать `auto_check` стоит, только если программа по своим правилам не должна выходить в сеть сама — тогда лицензии `limited` и `extended` продлевает её собственный вызов `refresh()`. Функции с сетью блокируют вызывающего до ответа или до истечения `timeout_ms`. ### Активация без интернета Для машины без сети: файл запроса переносится на машину с интернетом и загружается в личном кабинете («Лицензии» → «Активация без интернета»), в ответ приходит `license.lic`: ```python r = license.write_activation_request("request.json") # → в кабинет, получить license.lic r = license.import_license_file("license.lic") # тем же вызовом лицензия продлевается ``` У лицензии, принятой из файла, ключа нет, и `refresh()` ей не поможет — её продлевают новым `license.lic`. ### Переезд на другую машину `deactivate()` на старой машине освобождает место на сервере; без связи лицензия остаётся на месте. Без сети — `deactivate_to_file("receipt.json")`: лицензия удаляется с машины сразу, а расписка ложится в файл, и отправить её можно с любой машины. ### Исключения пакета `unir.UnirError` (наследник `RuntimeError`) — только сбой самой библиотеки. `ValueError` — неверные аргументы: значение вне диапазона, неизвестная символика, короткий буфер, закрытый сканер, `view()` у незагруженного снимка. `RuntimeError` — `Image.close()` при живом виде. `OSError` при `import unir` — не нашлась или не подошла встроенная библиотека. Результаты чтения и отказы лицензии — значения, а не исключения. > **Ключ активации.** Ключ активации выдаётся в личном кабинете после оформления тарифа в [каталоге](https://libscanner.com/). ## Командная строка Колесо ставит в окружение команду `unir` (то же — `python -m unir`): это собранная утилита из `unir/_lib/`, аргументы и код возврата передаются ей как есть. Справка — `unir --help`. Из Python утилиту удобно звать через `subprocess` — например, когда нужен отдельный процесс или готовый пакетный режим. Чтобы не зависеть от `PATH`, запускайте её текущим интерпретатором: ```python import subprocess, sys UNIR = [sys.executable, "-m", "unir"] out = subprocess.run([*UNIR, "--single", "label.jpg"], capture_output=True, text=True, encoding="utf-8") print(out.returncode) # Status: 0 прочитан, 1 не найден или файл не открыть, 2 не прочитан, 3 нет лицензии... print(out.stdout) # обработка: … мс # код: gs1-128 00246009834320769650 ``` В одиночном режиме в stdout — строка `обработка: … мс` и по строке `код: <символика> <текст>` на каждый код; если срок кадра вышел — ещё `бюджет: исчерпан, перебор оборван`. Байт GS в тексте печатается как ``. Код возврата — номер `Status`: 0 прочитан, 1 не найден или файл не открыть, 2 найден кандидат, но не прочитан (раздел «Что на самом деле значит NOT_DECODED»), 3 нет лицензии, 4 снимок больше предела, 5 противоречивый кадр, 6 внутренняя ошибка. Одно отличие от `Status`: снимок, который не загрузился (нет файла, не JPEG/PNG, декодирование не уложилось в срок), даёт 1, а не 5, как `INVALID` у `scan_file()`; поэтому 5 на снимках с диска утилита не отдаёт. Слишком большой снимок — 4, как `IMAGE_TOO_LARGE`. Срок, оборвавший разбор уже загруженного снимка, кода не меняет: 1 или 2 (или 0, если что-то успело прочитаться). Неверные ключи — тоже 1; в `--serve` путь вне `--root` даёт строку с кодом 1, а неверный список символик в запросе — с кодом 6. ### Ключи | Ключ | Что | | --- | --- | | `--batch СПИСОК` | Файл со списком путей, по одному на строку (UTF-8) | | `--folder КАТАЛОГ` | Все снимки JPEG и PNG каталога, без вложенных, по имени | | `--serve` | Путь на строку из stdin, пока ввод не закроют | | `--sym СПИСОК` | Символики через запятую, как у `parse_symbologies`: `dm`, `128`, `gs1` (`sscc`), `ean`, `all` (умолчание) | | `--codes N` | Сколько кодов искать: 1 (умолчание), 0 — все | | `--single` | Кадры не связаны между собой; умолчание — поток кадров. Для папки фотографий нужен этот ключ | | `--budget-ms N` / `--timeout-ms N` | Как `Settings.budget_ms` и `timeout_ms` | | `--max-side N` / `--max-pixels N` | Пределы размера снимка | | `--color` | Читать снимок в цвете; по умолчанию — в полутоне | | `--corners` | В пакетных режимах — углы символа шестой колонкой | | `--root КАТАЛОГ` | В `--serve` — только снимки внутри этого каталога | | `--version` | Версии unir и движков | ### Пакетный вывод В `--batch`, `--folder` и `--serve` — строка на каждый код, колонки через TAB: ```bash путь код возврата мс текст символика [углы] ``` Снимок без кодов даёт одну строку из четырёх колонок с пустым текстом и без символики. Байт GS в тексте — ``, табуляция — ``. Углы (`--corners`) — четыре пары `x,y` через пробел. Последняя строка stderr — сводка `[BATCH] снимков N, прочитано M, …`. Код возврата пакетного режима — 0, если пакет обработан; без лицензии разбор обрывается на первом снимке с кодом 3. ```python import subprocess, sys out = subprocess.run([sys.executable, "-m", "unir", "--batch", "list.txt", "--single"], capture_output=True, text=True, encoding="utf-8") for line in out.stdout.splitlines(): path, rc, ms, text, *rest = line.split("\t") symbology = rest[0] if rest else "" print(path, int(rc), float(ms), symbology, text) ``` ### Поток снимков: --serve Один живой процесс, снимки приходят по одному, по строке в stdin; после строк каждого снимка — строка `#END#`, вывод сбрасывается сразу. Строка запроса — путь или `путьsym=СПИСОК`: символики только этого снимка (как у `--sym`; неверный список — строка с кодом 6). Пути — в UTF-8. Без лицензии процесс отвечает строкой с кодом 3 и `#END#` и выходит с кодом 3. ```python import subprocess, sys, threading class UnirServe: """Один процесс "unir --serve" — один сканер. Вызовы scan() из разных потоков идут по очереди; для параллельной обработки — несколько процессов.""" def __init__(self, *args: str): self._lock = threading.Lock() self._proc = subprocess.Popen( [sys.executable, "-m", "unir", "--serve", *args], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, encoding="utf-8", bufsize=1, ) def scan(self, path: str, sym: str = "") -> list: """Строки ответа: (код возврата, символика, текст) на каждый код.""" with self._lock: if self._proc.poll() is not None: raise RuntimeError(f"unir --serve завершился (код {self._proc.returncode})") self._proc.stdin.write(path + (f"\tsym={sym}" if sym else "") + "\n") self._proc.stdin.flush() rows = [] while True: line = self._proc.stdout.readline() if not line: raise RuntimeError("unir --serve закрыл вывод") line = line.rstrip("\n") if line == "#END#": return rows _path, rc, _ms, text, *rest = line.split("\t") rows.append((int(rc), rest[0] if rest else "", text)) def close(self) -> None: with self._lock: if self._proc.poll() is None: self._proc.stdin.close() self._proc.wait(timeout=5) def __enter__(self) -> "UnirServe": return self def __exit__(self, *exc) -> None: self.close() with UnirServe("--single") as reader: for rc, symbology, text in reader.scan("frame.jpg"): if rc == 0: print(symbology, text) ``` ### Лицензия из командной строки ```bash unir --license status [--json] unir --license activate <ключ> [--json] unir --license refresh [--json] unir --license request unir --license import unir --license deactivate [] [--json] # с файлом — без сети, расписка в файл unir --license fingerprint [--json] ``` С `--json` подкоманды печатают тот же JSON, что возвращает `unir.license`. Код возврата `status` — 0, если библиотека читает, и 3, если нет; у операций — 0 сделано, 1 не удалось. Ключ `--server URL` подменяет сервер лицензий для этого вызова. ```python import json, subprocess, sys out = subprocess.run([sys.executable, "-m", "unir", "--license", "status", "--json"], capture_output=True, text=True, encoding="utf-8") status = json.loads(out.stdout) if not status["can_scan"]: print(status["message"]) ```