Обзор
Библиотека одним вызовом читает на кадре коды разных символик: 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), состав одинаковый:
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\ лицензии сторонних библиотекВ «Загрузках» личного кабинета на каждую платформу два архива: 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 |
Статическая линковка — C++ API
libunir.a входит в вашу программу целиком: рядом с ней ничего класть не нужно, кроме файла ключа unir.key — он рядом с программой (раздел «Лицензия»). Нужен компилятор C++20. API на C++, в результате возвращаются std::string и std::vector, поэтому компилятор и его стандартная библиотека должны быть теми же, что и у поставки (той же или более новой версии).
Поставка для Windows собрана MinGW-w64 GCC — libunir.a в его формате и с его ABI. MSVC такой статический архив не слинкует; вашей программе нужен тот же тулчейн (например, MSYS2/MinGW или CLion с профилем MinGW). Из MSVC — динамически, через unir_c.
Тулчейн бесплатный: MSYS2 — установщик с сайта, затем в его консоли MINGW64 (поставка собрана именно в этом окружении, не в UCRT64):
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkgconfПути включения и строка линковки берутся из unir.pc. Путь к lib\pkgconfig распакованной поставки — через переменную окружения (PowerShell); пути в .pc считаются от места самого файла, так что каталог распаковки может быть любым:
$env:PKG_CONFIG_PATH = "C:\куда\распакован\lib\pkgconfig"
pkg-config --static --cflags --libs unirСистемные winhttp, crypt32, shell32 и advapi32 (их использует проверка лицензии) уже прописаны в Libs.private пакета — отдельно подключать не нужно.
Поставка — одна статическая библиотека, а значит вызывающему нужны и те библиотеки, на которых стоит она сама. Без --static их в командной строке не окажется, и линковка упадёт на неразрешённых символах.
В meson — зависимость по имени пакета:
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.
MinGW-w64 GCC — через unir_c.pc и библиотеку импорта lib\unir_c.dll.a, без --static:
$env:PKG_CONFIG_PATH = "C:\куда\распакован\lib\pkgconfig"
pkg-config --cflags --libs unir_cЭто даёт -I…\include -L…\lib -lunir_c.
MSVC — файла .lib в поставке нет, его делают из самой DLL. В консоли MSYS2 — описание экспорта:
gendef unir_c.dll # пакет mingw-w64-x86_64-tools; пишет unir_c.defзатем в Developer Command Prompt for VS (x64) — библиотека импорта и сборка:
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 (раздел «Лицензия»).
В meson — так же по имени пакета, без static:
unir_c_dep = dependency('unir_c', required: true)
executable('мояпрограмма', 'main.c', dependencies: [unir_c_dep])Быстрый старт
Минимальная программа: снимок с диска, чтение, вывод символики и текста. Всё, что нужно, — один заголовок:
#include <unir/Unir.h>
#include <iostream>
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<int>(r.status) << "\n";
}
}Собрать пример можно Meson. Рядом с main.cpp — распакованная поставка (unir-1.0.1-windows-x64 или unir-1.0.1-linux-x64) и файл meson.build:
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:
meson setup build
meson compile -C buildТо же через pkg-config: .pc-файлы поставки перемещаемые, поэтому вместо ручного описания хватит одной строки в meson.build — unir_dep = dependency('unir', static : true) — и пути к распакованному lib/pkgconfig при настройке:
meson setup build --pkg-config-path="$PWD/unir-1.0.1-windows-x64/lib/pkgconfig"
meson compile -C buildДля кадра из своего конвейера захвата (SDK камеры, буфер OpenCV, Windows-битмап) диск не участвует — собирается вид на уже имеющийся буфер, без копирования:
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 нужен только <cstdint> и <string>.
Scanner
Scanner — объект, а не функция, и создавать его на каждый кадр не нужно. В конструкторе собирается цепочка движков под заданные символики, и там же проходит первая проверка лицензии — если рядом лежит ещё не активированный unir.key, конструктор один раз ждёт ответа сервера, а первый кадр этой задержки уже не платит. В режиме потока (Settings::stream()) DataMatrix-движок копит между кадрами подсказку размера символа, и ей нужно пережить кадр.
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());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.
Scanner можно перемещать, но не копировать — конструктор и оператор копирования удалены явно.
Результат чтения
Status
// Номера те же, что у 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<Code> | Прочитанные коды в порядке чтения (раздел «Символики»). Пуст, если 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).
Несколько кодов на кадре
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 — ровно один бит, что прочитано.
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; // все символики этой сборки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
// Короткое имя: "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) — он на кадре без штрихкода самый дорогой и поэтому последний. Первый прочитанный код и есть ответ: следующие движки уже не зовутся. - Несколько кодов (
maxCodes0 или больше 1): сначала все линейные символики, затем DataMatrix. Вcodes— сперва линейные коды, потом DataMatrix; разбор кончается, как только набралосьmaxCodes.
Движки, которых нет в маске, в цепочку не попадают и времени не стоят. Прежде чем отдать кадр DataMatrix-движку, unir заливает уже прочитанные линейные коды вместе с их свободными зонами ровным тоном: штрихи не уводят его в перебор ложных кандидатов. Заливка строится по углам прочитанных кодов, чужого символа не задевает и на координаты в ответе не влияет.
Кадр
PixelFormat
enum class PixelFormat {
Gray8, // один канал: яркость; для сканера самый дешёвый
Bgr8, // три канала, порядок OpenCV
Rgb8,
Bgra8, // четыре канала, порядок OpenCV и Windows
Rgba8,
};
constexpr int channelsOf(PixelFormat f); // 1, 3 или 4Для монохромной камеры кадр стоит передавать как Gray8, а не размноженным в три канала.
ImageView
Вид на чужой буфер: библиотека его не копирует, не освобождает и не переживает — буфер обязан быть жив всё время вызова. Поля повторяют вид кадра у dmr и ssccr, поэтому движкам кадр уходит без копирования.
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() напрямую:
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)
};Чтение с диска
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 не меняется.
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(): там загрузка и разбор — под одним сроком.
Геометрия
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 — четыре угла символа точнее, чем прямоугольником: на конвейере символ снят под углом.
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 — сколько найдётся) |
auto s = unir::Settings::stream();
s.symbologies = unir::Symbology::DataMatrix | unir::kEanUpc;
s.budgetMs = 120.0; // один срок на все символики; 0 — без ограничения
const unir::Scanner scanner(s);Все поля
// 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() в этот же срок входит загрузка файла.
Версия библиотеки
#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 собрана поставка.
#if UNIR_VERSION >= UNIR_VERSION_AT(1, 1, 0)
// код под заголовок 1.1.0 и новее
#endifЛицензия
Библиотека читает коды, пока действует пробный период (30 дней с первого запуска на машине, без сети и регистрации) или лицензия. Иначе Scanner::scan() и scanFile() сразу отвечают Status::Unlicensed: ни один движок не зовётся, файл снимка не открывается. Лицензия привязана к оборудованию (материнская плата, процессор, системный диск), а не к установке ОС: переустановка Windows и замена диска или процессора её не отбирают, замена материнской платы — отбирает.
Пробный период и лицензия unir — свои, независимые от «Чтения DataMatrix» и «Создания DataMatrix» на той же машине: сервер лицензий тот же, но продукт другой, и лицензия одного продукта не открывает другой.
#include <unir/License.h>
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»: библиотеки разных продуктов могут лежать в одном каталоге, а ключи у них разные. Библиотека ищет файл, берётся первый найденный:
- рядом с модулем, где живёт библиотека:
unir_c.dll/libunir_c.soдля C ABI, сама программа — для статическойlibunir.aи утилитыunir; - рядом с исполняемым файлом программы.
Текущий каталог и переменные окружения не просматриваются. Формат: ключ — первая строка, которая не пуста и не комментарий #, в том виде, в каком выдан — 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
// 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 с теми же именами:
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 — состояние уже после операции.
Активация: онлайн и офлайн
Онлайн — ключ из личного кабинета, машина сама идёт на сервер лицензий:
unir::LicenseResult r = unir::license::activate("UNIR-XXXXX-XXXXX-XXXXX-XXXXX");Офлайн — для машины без интернета: файл запроса переносится на машину с сетью и загружается в личный кабинет, в ответ приходит license.lic:
unir::license::writeActivationRequest("request.json"); // → загрузить в кабинет, получить license.lic
unir::license::importLicenseFile("license.lic"); // тем же вызовом продлевается: новый файл заменяет старыйАктивированная по сети лицензия раз в сутки сама сверяется с сервером в фоновом потоке — обновляет токен (режимам limited и extended это нужно, чтобы работать дальше), приносит отзыв и продление срока. Вызывать для этого ничего не нужно, а чтение кадра сеть не ждёт никогда; сервер недоступен — повтор через час, лицензия работает, пока не выйдет срок работы без связи.
Переезд и остальные функции
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; дальше перенос делает поддержка.
Ключ активации выдаётся в личном кабинете после оформления тарифа в каталоге.
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.
Полный пример: все коды со снимка
#include <unir/unir_c.h>
#include <stdio.h>
/* Все коды со снимка: 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); комментарии сокращены — полные в самом заголовке.
/* ── версии ── */
#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&<GS>93CuXUСообщения утилиты — на языке системы (на любой не русской — по-английски: processing, code); ключ --lang ru|en или переменная LIBSCANNER_LANG задают его явно. Строки --batch/--serve и вывод --json от языка не зависят. Та же переменная задаёт язык status().message и сообщений об ошибках в самой библиотеке.
По строке код: <символика> <текст> на каждый код, символика — по nameOf(). В тексте GS показан как <GS>, TAB — как <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, по строке на код:
путь<TAB>код возврата<TAB>мс<TAB>текст<TAB>символика[<TAB>углы]Первые четыре колонки — те же, что у утилиты dmr, так что разбор её вывода подходит и здесь. Снимок без кода — одна строка с пустым текстом и без колонки символики; во второй колонке — коды из таблицы выше. --corners добавляет шестую колонку x0,y0 x1,y1 x2,y2 x3,y3. Сводка — в stderr. Без лицензии прогон останавливается на первом снимке с кодом 3: остальные ответили бы так же.
--serve — тот же формат строки, но снимки не известны заранее: путь приходит по одному, по строке из stdin, и обрабатывается сразу же — процесс держит сканер живым между запросами внешнего сервиса, не платя за инициализацию на каждый. После строк каждого снимка идёт строка #END#, и вывод сбрасывается сразу — по ней видно, что ответ закончен, даже когда кодов несколько. Строка запроса — путь или путь<TAB>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&<GS>93CuXU datamatrix
#END#Лицензия из командной строки
unir --license status [--json]
unir --license activate <ключ> [--server URL] [--json]
unir --license refresh [--server URL] [--json]
unir --license request <request.json>
unir --license import <license.lic>
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) — часть формата вывода, не переводится и не меняется словами.
Нужной функции нет в библиотеке или ваш случай за её границами — доработаем под вашу задачу. Напишите нам →