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

Чтение Датаматрикс

Распознавание кодов DataMatrix структуры GS1 с кадров промышленной камеры — для конвейеров с маркировкой «Честного знака».

100%кадров прочитаны верно (8773 читаемых кадров на конвейерной выборке)
от 5,3 мсминимальное время чтения кадра; медиана по корпусу — 28 мс
Версия 1.0

Обзор

Библиотека читает коды DataMatrix (ECC200) с кадров производственной камеры — например, коробки с маркировкой «Честного знака», где символ занимает на кадре около сотни пикселей. Ридер рассчитан на низкое разрешение съёмки — порядка двух пикселей на модуль символа — и проверяет каждую гипотезу геометрии кодовой коррекцией Рида-Соломона. Точность и скорость чтения — вверху страницы; ниже — только устройство интерфейса.

Библиотека статическая, публичный интерфейс — на C++20, восемь заголовков в include/dmr/. Кадр передаётся видом на чужой буфер (dmr::ImageView) — без копирования, из уже имеющегося буфера камеры или снимка.

Область применения

По умолчанию сканер читает DataMatrix любой структуры. Режим requireGs1 (раздел «Настройки») дополнительно проверяет формат содержимого — AI 01 (GTIN, 14 цифр со сходящейся контрольной суммой), затем AI 21 — и отбраковывает символы, не подходящие под структуру GS1.

Подключение

Библиотека приходит уже собранной — собирать её не нужно, собирается только ваша программа. От неё требуется одно: компилятор C++20. API на C++, в результате возвращаются std::string и std::vector, поэтому компилятор и его стандартная библиотека должны быть теми же, что и у поставки.

В поставке семь публичных заголовков, статическая библиотека, файл для pkg-config и лицензии — и ничего сверх. Состав одинаковый на обеих ОС, различаются только команды подключения:

include\dmr\{Dmr,Diagnostics,Geometry,Image,Scanner,Settings,Version}.h
lib\libdmr.a
lib\pkgconfig\dmr.pc
share\doc\dmr\licenses\
Нужен MinGW-w64 GCC, не MSVC

Поставка для Windows собрана MinGW-w64 GCC — libdmr.a в его формате и с его ABI. MSVC такой статический архив не слинкует; вашей программе нужен тот же тулчейн (например, MSYS2/MinGW или CLion с профилем MinGW).

Тулчейн бесплатный: MSYS2 — установщик с сайта, затем в его консоли MINGW64 (поставка собрана именно в этом окружении, не в UCRT64):

pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkgconf

Пути включения и строка линковки берутся из dmr.pc — под MSYS2/MinGW pkg-config уже есть в системе:

pkg-config --static --cflags --libs dmr

Если поставка распакована не в системный префикс — путь к ней явно, через переменную окружения (PowerShell):

$env:PKG_CONFIG_PATH = "C:\куда\распакован\lib\pkgconfig"
pkg-config --static --cflags --libs dmr
Запрос к pkg-config должен быть статическим

Поставка — одна статическая библиотека, а значит вызывающему нужны и те библиотеки, на которых стоит она сама. Без --static их в командной строке не окажется, и линковка упадёт на неразрешённых символах.

В meson — то же самое через зависимость по имени пакета, одинаково на обеих ОС:

dmr_dep = dependency('dmr', required: true, static: true)
executable('мояпрограмма', 'main.cpp', dependencies: [dmr_dep])

Под Windows дополнительно требуются системные winhttp, crypt32 и shell32 (используются проверкой лицензии) — они уже прописаны в Libs.private пакета, отдельно подключать не нужно.

Быстрый старт

Минимальная программа: снимок с диска, чтение, вывод текста. Всё, что нужно, — один заголовок:

#include <dmr/Dmr.h>
#include <iostream>

int main() {
    dmr::Image frame;
    if (dmr::loadImage("кадр.jpg", frame) != dmr::LoadStatus::Ok) {
        std::cerr << "не удалось прочитать файл\n";
        return 1;
    }

    dmr::Scanner scanner;                          // поток кадров одной этикетки
    const dmr::ScanResult r = scanner.scan(frame);  // Image неявно приводится к ImageView

    if (r.ok()) {
        std::cout << r.text() << "\n";
    } else if (r.status == dmr::Status::NotFound) {
        std::cout << "символа на кадре нет\n";
    } else {
        std::cout << "символ есть, прочитать не вышло\n";
    }
}

Для кадра из своего конвейера захвата (SDK камеры, буфер OpenCV, Windows-битмап) диск не участвует — собирается вид на уже имеющийся буфер, без копирования:

const dmr::ImageView view{ m.data, m.cols, m.rows, (int)m.step, dmr::PixelFormat::Bgr8 };
const dmr::ScanResult r = scanner.scan(view);

Заголовки

Публичных заголовков семь, все под include/dmr/; восьмой, dmr/Dmr.h, включает их все разом.

ЗаголовокЧто в нём
dmr/Dmr.hАгрегат — включает всё нижеперечисленное
dmr/Image.hImageView, Image, PixelFormat, loadImage, пути
dmr/Scanner.hScanner, ScanResult, Code, Status
dmr/Settings.hНастройки и пресеты сканера
dmr/Geometry.hPoint, Box, Quad — где на кадре расположен код
dmr/Diagnostics.hexplain(), приёмник журнала (LogSink)
dmr/License.hПроверка и активация лицензии, пространство dmr::license
dmr/Version.hВерсия заголовка (макросы) и собранной библиотеки (dmr::version())

Заголовки не зависят друг от друга сверх необходимого: Settings.h и Scanner.h тянут за собой <functional> и <vector>, а тому, кто только собирает вид на кадр, достаточно одного Image.h.

Scanner

Scanner — объект, а не функция, и создавать его на каждый кадр не нужно: подсказка размера символа (Settings::mcHintRun) копится по кадрам потока и обязана их пережить, а число потоков пула — настройка процесса. При этом scan() объявлен const и не складывает в объект ничего, кроме этой подсказки: результат зависит от кадра, а не от истории чтений. Кандидаты внутри перебираются параллельно — свой пул потоков поверх scan() заводить не нужно.

class Scanner {
public:
    explicit Scanner(Settings s = Settings::stream());
    ~Scanner();

    Scanner(Scanner&&) noexcept;
    Scanner& operator=(Scanner&&) noexcept;
    Scanner(const Scanner&)            = delete;
    Scanner& operator=(const Scanner&) = delete;

    // Прочитать коды на кадре.
    //   frame        кадр целиком, цветной или полутоновый
    //   moduleCount  сторона символа в модулях; 0 — определять самому
    ScanResult scan(ImageView frame, int moduleCount = 0) const;

    // Размер символа, подсказанный предыдущими кадрами; 0 — подсказки нет.
    int moduleCountHint() const;

    // Забыть подсказку.
    void resetStream();

    const Settings& settings() const;
};

// Разовое чтение без объекта: проба, скрипт, один файл. Подсказка размера
// по потоку кадров здесь не работает — копить её нечему.
ScanResult scan(ImageView frame, const Settings& s = Settings::single());

Когда этикетка на конвейере сменилась, подсказку стоит сбросить явно, не дожидаясь, пока она погаснет сама:

scanner.resetStream();
Move-only

Scanner можно перемещать (Scanner&&), но не копировать — конструктор и оператор копирования удалены явно.

Результат чтения

Status

enum class Status {
    Ok         = 0,  // прочитан хотя бы один код
    NotFound   = 1,  // символ на кадре не найден
    NotDecoded = 2,  // символ найден, но не прочитан
    Unlicensed = 3,  // кадр не разбирался: нет пробного периода и лицензии
};

При Status::Unlicensed кадр не разбирался вовсе — причину даёт dmr::license::status() (раздел «Лицензия»).

ScanResult

ПолеТипСмысл
statusStatusЧем кончился разбор кадра
codesstd::vector<Code>Прочитанные коды, по одному на символ, в порядке чтения: сверху вниз, при равном верхнем крае — слева направо. Пуст, если status не Ok. При Settings::maxCodes = 1 длина не больше единицы
timedOutboolПеребор оборвался по Settings::budgetMs. Взводится, только когда бюджет и правда что-то отсёк — успех при взведённом флаге возможен: код прочитан, но часть гипотез не проверена
elapsedMsdoubleСколько занял разбор кадра, мс, без учёта загрузки снимка с диска
detailsstd::shared_ptr<const Details>Разбор для диагностики; пусто, если не запрошен через Settings::collectDetails

Методы: bool ok() const — сокращение для status == Status::Ok; const std::string& text() const — текст первого кода или пустая строка, чтобы не писать codes.empty() ? "" : codes[0].text на каждом вызове.

Code

ПолеТипСмысл
textstd::stringДекодированная строка. Байты как есть: разделители GS остаются байтом 0x1D, а не подставляются печатным видом
boxBoxГде символ найден на исходном кадре — прямоугольник кандидата
cornersQuadЧетыре угла символа там же, точнее бокса (раздел «Геометрия»)
moduleCountintСторона символа в модулях

box нужен не для красоты: два кандидата с одинаковой строкой — это один символ, увиденный дважды, если боксы пересекаются, и две одинаковые этикетки, если нет.

Несколько кодов на кадре

const dmr::Scanner scanner(dmr::Settings::multi(0));   // 0 — сколько найдётся
for (const dmr::Code& c : scanner.scan(frame).codes)
    use(c.text, c.box, c.moduleCount);

Кадр

PixelFormat

enum class PixelFormat {
    Gray8,   // один канал: яркость; для сканера самый дешёвый
    Bgr8,    // три канала, порядок OpenCV
    Rgb8,    // три канала, порядок большинства остальных
    Bgra8,   // четыре канала, порядок OpenCV и Windows
    Rgba8,   // четыре канала
};

Сканер работает с яркостью и цветной кадр первым делом переводит в неё. Для монохромной камеры кадр стоит передавать как Gray8, а не размноженным в три канала — ответ тот же, а кадр обрабатывается быстрее.

ImageView

Вид на чужой буфер: библиотека его не копирует, не освобождает и не переживает — буфер обязан быть жив всё время вызова.

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 * каналов), а не «ноль байт в строке»: кадр камеры почти никогда не плотный — строки выравнивают, а вырезка наследует длину строки оригинала. Собирается на месте вызова без копирования:

const dmr::ImageView view{ m.data, m.cols, m.rows, (int)m.step, dmr::PixelFormat::Bgr8 };

Image

Кадр, которым библиотека владеет сама — нужен там, где она его создаёт, то есть при чтении с диска. Неявно приводится к ImageView, поэтому передаётся в scan() напрямую, без .view():

class Image {
public:
    Image() = default;
    Image(int width, int height, PixelFormat format);

    void reset(int width, int height, PixelFormat format);  // плотная укладка
    void clear();

    int         width()    const;
    int         height()   const;
    int         stride()   const;
    PixelFormat format()   const;
    int         channels() const;
    bool        empty()    const;

    uint8_t*       data();
    uint8_t*       row(int y);

    ImageView view() const;
    operator ImageView() const;   // неявно — для вызова scan(image)
};

Чтение с диска

enum class LoadStatus {
    Ok,          // снимок прочитан
    NotFound,    // файла нет
    Unreadable,  // файл есть, но это не читаемый снимок
};

enum class LoadAs {
    Color,  // три канала, PixelFormat::Bgr8
    Gray,   // один канал, PixelFormat::Gray8
};

LoadStatus loadImage(const std::string& path, Image& out, LoadAs as = LoadAs::Color);

Для сканера Gray — лучший выбор: снимок монохромной камеры (полутон в трёхканальном JPEG) в этом виде читается дешевле, а ответ побайтово тот же, потому что каждый канал Color и есть эта яркость. При отказе out очищается — половина кадра от прошлой попытки хуже пустого.

dmr::Image frame;
switch (dmr::loadImage("кадр.jpg", frame, dmr::LoadAs::Gray)) {
    case dmr::LoadStatus::Ok:         break;
    case dmr::LoadStatus::NotFound:   /* файла нет */ break;
    case dmr::LoadStatus::Unreadable: /* файл есть, но это не снимок */ break;
}
Кодировка пути

loadImage сама разбирается с системной кодировкой пути — каталог с русским именем не роняет чтение. Для собственной работы с путями в заголовке есть toPath(const std::string&), fromPath(const std::filesystem::path&) (обратное преобразование) и printablePath(...) — путь в UTF-8, для журнала.

Геометрия

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 dmr::Point& p : c.corners.corner)
    drawTo(p.x, p.y);
Порядок углов

Углы Quad идут по кругу, и под индексом 3 стоит тот, который детектор принял за угол сплошной L-рамки — решение, принятое по плотности сторон до всякого чтения. Окончательный поворот символа выбирается позже и с ним может не совпасть. Для разметки поверх кадра corner[3] годится, для выводов о содержимом символа — нет.

Настройки

Значения по умолчанию — не выбор наугад, а результат тестирования на большом наборе кадров. Начинать стоит с пресета, а не с отдельных полей — имя пресета уже говорит, под какой сценарий он настроен.

ПресетДля чегоЧем отличается
Settings::stream()Поток кадров одной этикетки — конвейер, камера. УмолчаниеПодсказка размера по предыдущим кадрам включена, перебор размеров выключен
Settings::single()Разовые снимки — папка, ручная проверкаПодсказки нет, перебираются 24 размера символа
Settings::multi(n)Несколько кодов на одном кадреmaxCodes = n (0 — сколько найдётся); вчетверо дороже одиночного чтения
auto s = dmr::Settings::stream();
s.budgetMs   = 120.0;     // 0 — без ограничения
s.requireGs1 = true;      // принимать только коды структуры GS1
const dmr::Scanner scanner(s);
stream() против single()

Разница не стилистическая, а в скорости. single() перебирает до 24 размеров символа, если размер заранее не известен, — на кадре, который так и не удалось прочитать (отказном), это заметно медленнее, чем без перебора, хотя и поднимает долю успешных чтений на несколько процентов. На потоке кадров одной этикетки размер от кадра к кадру не меняется, поэтому перебор не даёт выигрыша в точности вообще — только платит временем на каждом отказе. Именно поэтому в stream() он выключен, а в single() включён: для разрозненных снимков, где размер заранее не знаешь, эта плата оправдана.

Все поля

ПолеУмолчаниеСмысл
maxCodes1Сколько кодов искать: 1 — один, 0 — сколько найдётся, N — не больше N
requireGs1falseПринимать только коды структуры GS1. Сошедшаяся коррекция Рида-Соломона сама по себе не доказывает верного чтения — проверка дополнительно смотрит на формат содержимого (AI 01, 14 цифр GTIN со сходящейся контрольной суммой, затем AI 21)
forcedRotation-1Поворот символа, 0..3 — четверти по часовой; -1 — определять самому
budgetMs0.0Бюджет времени на кадр, мс; 0 — без ограничения. Срок мягкий: проверка идёт между стадиями перебора, а не внутри них, поэтому время кадра может превысить бюджет на длительность идущей стадии
rowParalleltrueДелить строки ядер изображения между потоками пула, пока кадр идёт одним потоком. Снижает задержку кадра ценой процессорного времени — выключать, если на машине несколько сканеров или важнее пропускная способность, а не задержка
maxSizeTries1Сколько размеров символа пробовать, когда размеру верить нельзя; 1 — не перебирать
warpScalesпустоМасштабы холста выпрямления для последнего прохода, пикселей на модуль; пусто — не перебирать. Разбирается из строки вида "7,8,10" методом setWarpScales()
maxCandidates6Сколько кандидатов брать у детектора по L-паттерну за один проход
readerCacheMb64.0Предел памяти, МБ на кадр, под повторное использование работы; 0 — ничего не хранить. На ответ не влияет
mcHintRun5Сколько кадров подряд должны сойтись на одном размере символа, чтобы дальше он проверялся первым; 0 — не подсказывать
maxPatternError0.25Предел доли ошибок в служебных модулях (L-рамка, timing), при котором результат вообще рассматривается
lpatRetryScale0.8Повторный поиск по L-паттерну на точном масштабе, как вторая гипотеза; 0 — не повторять
directSamplingfalseБрать яркости модулей прямо из исходника, минуя холст выпрямления
useLPatternFinder / useTextureFindertrue / trueИсточники кандидатов; выключение нужно для сравнения детекторов на одном корпусе
collectDetailsfalseСобирать разбор прочитанного и отказа в ScanResult::details (около 2 МБ на результат)

Диагностика

Библиотека не пишет в консоль и не читает переменные окружения — приёмник журнала передаётся явно, и без него строки даже не собираются:

using LogSink = std::function<void(std::string_view line)>;

void setLogSink(LogSink sink);   // пустой — выключить
void setVerbose(bool on);        // по умолчанию выключена
bool isVerbose();                // включена И есть приёмник
std::ostream& dbg();             // поток выкладки: в приёмник, иначе «в никуда»
dmr::setLogSink([](std::string_view line) { std::cerr << line; });
dmr::setVerbose(true);

Разбор причины конкретного чтения или отказа — отдельно и по запросу, строкой, а не в журнал. Собирается, только если при чтении был включён Settings::collectDetails:

struct ExplainOptions {
    int moduleCount     = 0;      // размер, заданный при чтении; 0 — определялся сам
    int forcedRotation  = -1;     // поворот, заданный при чтении
    bool images         = false;  // сохранять отладочные PNG рядом с рабочим каталогом
    std::string imagePath;        // имя снимка — для подписей отладочных файлов
};

std::string explain(const Details& details, const ExplainOptions& opt = {});
auto s = dmr::Settings::stream();
s.collectDetails = true;                 // около 2 МБ на результат
const dmr::ScanResult r = dmr::Scanner(s).scan(frame);
if (r.details) std::cerr << dmr::explain(*r.details);
explain() не выносит вердикт

Статус кадра уже объявил сканер. Когда символ найден, но не прочитан, разбирать нечего — тогда вырезка заново выпрямляется и декодируется, чтобы отказу было чем объясниться, и это стоит примерно столько же, сколько само чтение кадра.

Версия библиотеки

#define DMR_VERSION_MAJOR 1
#define DMR_VERSION_MINOR 0
#define DMR_VERSION_PATCH 0
#define DMR_VERSION_STRING "1.0.0"

#define DMR_VERSION_AT(ma, mi, pa) ((ma) * 10000 + (mi) * 100 + (pa))
#define DMR_VERSION DMR_VERSION_AT(DMR_VERSION_MAJOR, DMR_VERSION_MINOR, DMR_VERSION_PATCH)

namespace dmr { std::string version(); }

Макросы говорят про заголовок, с которым скомпилировались; dmr::version() — про библиотеку, с которой слинковались. Разойтись они могут: библиотека ставится в префикс и живёт там дольше, чем помнит собиравший.

#if DMR_VERSION >= DMR_VERSION_AT(1, 1, 0)
    // код под заголовок 1.1.0 и новее
#endif

Лицензия

Сканер читает кадры, пока действует пробный период (месяц с первого запуска на машине, без сети и регистрации) или лицензия. Иначе Scanner::scan() сразу отвечает Status::Unlicensed и кадр не разбирает. Лицензия привязана к оборудованию (материнская плата, процессор, системный диск), а не к установке ОС.

#include <dmr/License.h>

const dmr::LicenseStatus s = dmr::license::status();
if (!s.canScan()) show(s.message);   // «Пробный период закончился. Нужна лицензия»

dmr::LicenseResult r = dmr::license::activate(keyFromUser);   // «DMR-XXXXX-XXXXX-XXXXX-XXXXX»
if (!r.ok) show(r.message);

LicenseState

ЗначениеСмысл
TrialИдёт пробный период
ActiveЛицензия действует
RefreshDueДействует, но связи с сервером давно не было; автосверка повторяет попытки раз в час
TrialExpiredПробный период кончился, лицензии нет
ExpiredОплаченный срок лицензии кончился; продление в личном кабинете автосверка подхватит сама
NetworkRequiredБез связи с сервером дальше нельзя; как только сервер ответит, автосверка вернёт лицензию
WrongMachineЛицензия выдана другой машине
InvalidЗапись лицензии не проходит проверку подписи

LicenseStatus

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

Активация: онлайн и офлайн

Онлайн — ключ из личного кабинета, машина сама идёт на сервер лицензий:

dmr::LicenseResult r = dmr::license::activate("DMR-XXXXX-XXXXX-XXXXX-XXXXX");

Офлайн — для машины без интернета: файл запроса переносится на машину с сетью и загружается в личный кабинет, в ответ приходит license.lic:

dmr::license::writeActivationRequest("request.json");   // → загрузить в кабинет, получить license.lic
dmr::license::importLicenseFile("license.lic");          // тем же вызовом продлевается: новый файл заменяет старый

Активированная по сети лицензия раз в сутки сама сверяется с сервером в фоновом потоке — обновляет токен, приносит отзыв и продление срока. Вызывать для этого ничего не нужно, а чтение кадра сеть не ждёт никогда; сервер недоступен — повтор через час, лицензия работает, пока не выйдет срок работы без связи.

Отвязка и остальные функции

namespace dmr::license {
    void configure(const LicenseOptions& options);   // до первого сетевого вызова; можно не звать
    LicenseStatus status();
    LicenseResult activate(const std::string& licenseKey);
    LicenseResult refresh();                                    // сверить сейчас, не дожидаясь суток
    LicenseResult deactivate(const std::string& reason = {});    // расписка уходит на сервер
    LicenseResult deactivateToFile(const std::string& receiptPath, const std::string& reason = {});
    LicenseResult writeActivationRequest(const std::string& path);
    LicenseResult importLicenseFile(const std::string& path);
    std::string machineFingerprint();                           // для обращений в поддержку
}

Переезд на другую машину — deactivate() на старой: место освобождается на сервере распиской, подписанной секретом этой машины. Без сети — deactivateToFile(): расписка ложится в файл, отправить его можно с любой машины. Все функции потокобезопасны; функции с сетью блокируют вызывающего до ответа или до истечения LicenseOptions::timeoutMs (по умолчанию 15000 мс).

Каждый ответ сервера лицензий привязан к своему запросу: библиотека отправляет случайное одноразовое число, сервер возвращает его в подписанном ответе, и записанный раньше ответ повторно не примется. Расписка о сдаче места тоже одноразовая — в ней номер из последнего полученного от сервера ответа, поэтому расписку из восстановленной копии старого состояния машины сервер не примет. Секрет для подписи расписок сервер выдаёт только при первой активации машины. У лицензий с полной автономией (full) сдача места расходует лимит переносов; когда он исчерпан, перенос делает поддержка.

Ключ активации

Ключ активации для activate() выдаётся из личного кабинета после оформления тарифа в каталоге.

Командная строка

В поставку вместе с библиотекой входит готовая оболочка командной строки dmr — для быстрой проверки без своего кода: разовый снимок, папка, список путей или поток для внешнего сервиса.

dmr <снимок> [число_модулей] [--verbose] [--rot 0..3] [--budget-ms N] [--codes N]
dmr --batch  <список>  [число_модулей] [--verbose]
dmr --folder <каталог> [число_модулей] [--verbose]
dmr --serve  [число_модулей] [--verbose]
dmr --license status | activate <ключ> | refresh | deactivate | fingerprint [--json]

Разовый снимок

$ dmr этикетка.jpg
запуск: 4.6 мс
обработка: 24.9 мс
код: 0104627191145677215n5Gr,pHTGIU&<GS>93CuXU

Пакетный режим и поток

--batch и --folder — один процесс на весь заранее известный набор снимков, результат построчно на stdout: путь · код возврата · миллисекунды · текст.

--serve — тот же формат строки, но снимки не известны заранее: путь приходит по одному, по строке из stdin, и обрабатывается сразу же — процесс держит сканер живым между запросами внешнего сервиса, не платя за инициализацию на каждый. Результат сбрасывается сразу (std::flush), не дожидаясь, пока закроют весь ввод, — так демо-раздел этого сайта (/scan) держит один процесс на все запросы, вместо процесса на снимок.

$ mkfifo queue
$ dmr --serve < queue &
$ echo /путь/к/снимку.jpg > queue
/путь/к/снимку.jpg	0	91.2	0104627191145677215n5Gr,pHTGIU&<GS>93CuXU

Лицензия: машиночитаемый вывод

У всех подкоманд --license есть флаг --json — тот же результат, что и в человекочитаемом выводе, но в виде одной JSON-строки на stdout, для скриптов и админ-панелей:

$ dmr --license status --json
{"state":"trial","can_scan":true,"valid_until":1792831857,"refresh_after":0,
 "offline_until":0,"days_left":28,"license_id":"","activation_id":"",
 "offline_mode":"","clock_rollback":false,"message":"Пробный период: осталось 28 дн."}

Поле state — устойчивый машиночитаемый код (trial, active, refresh_due, trial_expired, expired, network_required, wrong_machine, invalid) — часть формата вывода, не переводится и не меняется словами.

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