Документация разработчика

Библиотека чтения машиночитаемых кодов

Один вызов читает на кадре DataMatrix, GS1-128 (SSCC), Code 128, EAN-13, EAN-8, UPC-A и UPC-E — маркировку «Честного знака», логистические этикетки и товарные штрихкоды.

7 символикDataMatrix, GS1-128, Code 128, EAN-13, EAN-8, UPC-A и UPC-E — на одном кадре одним вызовом
99,6 %кадров корпуса из 8959 снимков производственной линии с DataMatrix прочитаны — кадр в кадр как у «Чтения DataMatrix»
0 ложныхштрихкодов на тех же 8959 кадрах: линейный код принимается, только когда сходятся несколько строк

Для ИИ-ассистентов: эта страница в Markdown · вся документация одним файлом · llms.txt

Версия 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), состав одинаковый:

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
Pythonpip install libscanner-unir или колесо из «Загрузок» — документация Python

Статическая линковка — C++ API

libunir.a входит в вашу программу целиком: рядом с ней ничего класть не нужно, кроме файла ключа unir.key — он рядом с программой (раздел «Лицензия»). Нужен компилятор C++20. API на C++, в результате возвращаются std::string и std::vector, поэтому компилятор и его стандартная библиотека должны быть теми же, что и у поставки (той же или более новой версии).

Нужен MinGW-w64 GCC, не MSVC

Поставка для 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 пакета — отдельно подключать не нужно.

Запрос к pkg-config должен быть статическим

Поставка — одна статическая библиотека, а значит вызывающему нужны и те библиотеки, на которых стоит она сама. Без --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.hImageView, Image, PixelFormat, ImageLimits, loadImage
unir/Scanner.hScanner, ScanResult, Code, Status
unir/Settings.hНастройки и пресеты сканера
unir/Symbology.hSymbology, маска Symbologies, имена и префиксы AIM
unir/Geometry.hPoint, 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());
Один 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 здесь ошибиться не даёт.

Что с файлом*loadstatus, timedOutC ABI / Python
ПрочитанOkитог разбораUNIR_LOAD_OK / OK
Файла нетNotFoundInvalidImage, falseUNIR_LOAD_NOT_FOUND / NOT_FOUND
Не JPEG/PNG или повреждёнUnreadableInvalidImage, falseUNIR_LOAD_UNREADABLE / UNREADABLE
Декодирование не уложилось в срокUnreadableInvalidImage, trueUNIR_LOAD_TIMED_OUT / TIMED_OUT
Больше пределов по заголовкуTooLargeImageTooLarge, falseUNIR_LOAD_TOO_LARGE / TOO_LARGE
Нет лицензии — файл не открывалсяOkUnlicensed, falseUNIR_LOAD_OK / OK
Ошибки возвращаются, а не бросаются

scan() и scanFile() ловят исключения внутри: внутренний сбой (например, нехватка памяти) — это Status::Failed, если ни одного кода к этому моменту не прочитано, и процесс живёт дальше. loadImage() сообщает о проблемах через LoadStatus.

Move-only

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

ПолеТипСмысл
statusStatusЧем кончился разбор кадра
codesstd::vector<Code>Прочитанные коды в порядке чтения (раздел «Символики»). Пуст, если status не Ok. Не длиннее Settings::maxCodes, если тот больше нуля
timedOutboolХоть один движок оборван по сроку кадра или не успел начать. Успех при взведённом флаге возможен: код прочитан, но часть символик или гипотез не проверена
elapsedMsdoubleСколько занял вызов, мс. У scanFile() сюда входит и загрузка снимка

Методы: bool ok() const — сокращение для status == Status::Ok; const std::string& text() const — текст первого кода или пустая строка.

Code

ПолеТипСмысл
symbologySymbologyСимволика кода — ровно один бит из маски (раздел «Символики»)
textstd::stringДанные символа, байты как есть. Ведущий FNC1 (признак GS1) не передаётся, FNC1 внутри — разделитель GS, байт 0x1D, а не его печатный вид
boxBoxГде символ найден на исходном кадре — прямоугольник по осям кадра
cornersQuadЧетыре угла символа там же, точнее бокса; смысл индексов свой у каждой символики (раздел «Геометрия»)
moduleCountintDataMatrix: число модулей по стороне символа; 0 — неизвестно или не DataMatrix
modulePxfloatЛинейные символики: ширина модуля, пикселей; 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);
СимволикаЧто читается
DataMatrixDataMatrix ECC200 любой структуры — тем же движком и с теми же ответами, что у «Чтения DataMatrix»
Gs1_128Code 128 с FNC1 в начале. Если в маске нет Code128, читается только SSCC (AI 00); вместе с Code128 — и прочие GS1-128
Code128Code 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) — он на кадре без штрихкода самый дорогой и поэтому последний. Первый прочитанный код и есть ответ: следующие движки уже не зовутся.
  • Несколько кодов (maxCodes 0 или больше 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; остальные поля у всех трёх — умолчания из объявления.

ПолеУмолчаниеСмысл
symbologieskAllSymbologiesКакие символики искать — маска (раздел «Символики»). Умолчание — все символики сборки; в следующих версиях их станет больше
maxCodes1Сколько кодов искать: 1 — до первого прочитанного кода любой символики, N — до N, 0 — все
budgetMs0.0Предел времени на кадр, мс, — один на все символики: движки делят его по очереди, и тот, кому не осталось времени, не начинается вовсе (взводится timedOut). 0 — без предела, действует только timeoutMs. Срок мягкий: время кадра может превысить его на длительность идущей стадии. DataMatrix-движку бюджет передаётся как есть, и, как в самом dmr, заданный бюджет меняет и способ перебора, а не только обрывает его
timeoutMs10000.0Предохранитель, мс: тот же обрыв, что у budgetMs (из двух действует меньший), но способа работы движков не меняет. 0 и больше 60000 — потолок 60 с: дольше кадр не разбирается никогда. Защита от враждебных кадров, а не настройка задержки
imageLimits16384 по стороне, 100 000 000 пикселейНаибольший кадр, который сканер берётся разбирать (ImageLimits из unir/Image.h; 0 — жёсткий потолок 65535 и 2^29). Кадр больше сразу получает Status::ImageTooLarge. scanFile() сверяет те же пределы с заголовком файла до декодирования. Защита, а не настройка качества
modeMode::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»: библиотеки разных продуктов могут лежать в одном каталоге, а ключи у них разные. Библиотека ищет файл, берётся первый найденный:

  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

// 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";
}
ПолеСмысл
stateLicenseState
canScan()Читает ли сейчас библиотека — true при Trial, Active или RefreshDue
validUntilКонец пробного периода или оплаченного срока, unix-секунды UTC
refreshAfterС какого момента токен считается устаревшим и лицензия переходит в RefreshDue; 0 — не устаревает (пробный период, режим full)
offlineUntilДольше этого без связи с сервером лицензия не работает; 0 — без ограничения (режим full)
daysLeftЦелых суток до ближайшей из границ выше; 0 — граница пройдена
licenseId / activationIdИдентификаторы лицензии и конкретной активации
offlineModelimited | 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) — часть формата вывода, не переводится и не меняется словами.

Нужной функции нет в библиотеке или ваш случай за её границами — доработаем под вашу задачу. Напишите нам →