@ -1,11 +0,0 @@
|
||||
Архитектура чата (группового) |
||||
|
||||
1. сообщения чата (текст + ссылки на аттачи) хранятся в таблице: |
||||
id (primary key), timestamp, user_id, msg (utf8 text 4k max), sign |
||||
|
||||
2. реакции: key(msg_id, user_id), type, sign |
||||
3. аттачи: key(msg_id, user_id), filename, sign |
||||
|
||||
Структура каталогов: |
||||
chats.db - база |
||||
<caht_id> |
||||
@ -1,13 +1,201 @@
|
||||
Архитектура чата (группового) |
||||
!! пока мысли - не использовать это как готовое тз |
||||
|
||||
|
||||
1. сообщения чата (текст + ссылки на аттачи) хранятся в таблице: |
||||
id (primary key), timestamp, user_id, msg (utf8 text 4k max), sign |
||||
|
||||
2. реакции: key(msg_id, user_id), type, sign |
||||
3. аттачи: key(msg_id, user_id), filename, sign |
||||
|
||||
Структура каталогов: |
||||
chats.db - база |
||||
<caht_id> |
||||
Архитектура чата (группового) |
||||
|
||||
База данных: SQLite3 (WAL mode), chats.db |
||||
|
||||
================================================================================ |
||||
Схема БД (10 таблиц) |
||||
================================================================================ |
||||
|
||||
-- 1. local_identity — текущий локальный узел (одна строка, приватный ключ здесь) |
||||
-- приватный ключ получаем от utun через MSG_RSP_PRIVKEY (только localhost) |
||||
CREATE TABLE local_identity ( |
||||
id INTEGER PRIMARY KEY CHECK (id = 1), |
||||
node_id INTEGER NOT NULL UNIQUE, |
||||
name TEXT NOT NULL, |
||||
x25519_pubkey BLOB NOT NULL, -- 32 bytes |
||||
x25519_privkey BLOB, -- 32 bytes (только на этом устройстве) |
||||
ed25519_pubkey BLOB, -- 32 bytes (derive из x25519 privkey) |
||||
created_at INTEGER DEFAULT (unixepoch()), |
||||
updated_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
|
||||
|
||||
-- 2. my_nodes — другие наши ноды (другие устройства, нет приватного ключа) |
||||
CREATE TABLE my_nodes ( |
||||
node_id INTEGER PRIMARY KEY, |
||||
name TEXT NOT NULL, |
||||
x25519_pubkey BLOB NOT NULL, -- 32 bytes |
||||
ed25519_pubkey BLOB, -- 32 bytes |
||||
created_at INTEGER DEFAULT (unixepoch()), |
||||
updated_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
|
||||
|
||||
-- 3. nodes — известные удалённые узлы (получаем через msg_transport от utun) |
||||
CREATE TABLE nodes ( |
||||
node_id INTEGER PRIMARY KEY, |
||||
name TEXT, |
||||
x25519_pubkey BLOB NOT NULL, -- 32 bytes |
||||
ed25519_pubkey BLOB, -- 32 bytes (для проверки подписи пира) |
||||
is_online INTEGER DEFAULT 0, |
||||
last_seen_at INTEGER DEFAULT (unixepoch()), |
||||
created_at INTEGER DEFAULT (unixepoch()), |
||||
updated_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
|
||||
|
||||
-- 3a. accounts — UI-информация об узлах для отображения сообщений |
||||
CREATE TABLE accounts ( |
||||
node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id), |
||||
display_name TEXT NOT NULL, -- "Alice" |
||||
avatar_color TEXT DEFAULT '#4A90E2', -- hex RGB |
||||
avatar_letter TEXT NOT NULL, -- "A" |
||||
is_contact INTEGER DEFAULT 1, -- показывать в боковой панели |
||||
created_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
|
||||
|
||||
-- 4. channels — каналы (DM и групповые) |
||||
CREATE TABLE channels ( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
channel_id TEXT NOT NULL UNIQUE, -- "dm:nodeA_nodeB" (A<B) или "group:hash" |
||||
name TEXT NOT NULL, |
||||
owner_node_id INTEGER, -- создатель (NULL для DM) |
||||
is_dm INTEGER DEFAULT 0, |
||||
last_message TEXT, |
||||
last_msg_at INTEGER, -- unix timestamp ms |
||||
created_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
CREATE UNIQUE INDEX idx_channel_cid ON channels(channel_id); |
||||
|
||||
|
||||
-- 5. channel_members — участники канала |
||||
CREATE TABLE channel_members ( |
||||
channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE, |
||||
node_id INTEGER NOT NULL, |
||||
joined_at INTEGER DEFAULT (unixepoch()), |
||||
PRIMARY KEY (channel_id, node_id) |
||||
); |
||||
CREATE INDEX idx_cm_node ON channel_members(node_id); |
||||
|
||||
|
||||
-- 6. messages — сообщения чата |
||||
-- signature = Ed25519 отправителя над (channel_id, author_node_id, content, timestamp) |
||||
CREATE TABLE messages ( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE, |
||||
author_node_id INTEGER NOT NULL, |
||||
content TEXT NOT NULL CHECK(length(content) <= 4096), |
||||
content_type TEXT DEFAULT 'text/plain', |
||||
timestamp INTEGER NOT NULL, -- unix milliseconds |
||||
signature BLOB NOT NULL CHECK(length(signature) == 64), |
||||
is_outgoing INTEGER DEFAULT 0, |
||||
is_read INTEGER DEFAULT 0, |
||||
created_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
CREATE INDEX idx_msg_channel_time ON messages(channel_id, timestamp); |
||||
CREATE INDEX idx_msg_author ON messages(author_node_id); |
||||
-- дедупликация: один автор не может отправить два сообщения в один ms в один канал |
||||
CREATE UNIQUE INDEX idx_msg_dedup ON messages(channel_id, author_node_id, timestamp); |
||||
|
||||
|
||||
-- 7. reactions — реакции на сообщения |
||||
-- signature = Ed25519 отправителя над (message_id, node_id, emoji) |
||||
CREATE TABLE reactions ( |
||||
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE, |
||||
node_id INTEGER NOT NULL, |
||||
emoji TEXT NOT NULL, -- unicode codepoint или :shortcode: |
||||
signature BLOB NOT NULL CHECK(length(signature) == 64), |
||||
created_at INTEGER DEFAULT (unixepoch()), |
||||
PRIMARY KEY (message_id, node_id, emoji) |
||||
); |
||||
|
||||
|
||||
-- 8. attachments — вложения к сообщениям |
||||
-- signature = Ed25519 отправителя над (message_id, filename, content_type, size, sha256) |
||||
CREATE TABLE attachments ( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE, |
||||
filename TEXT NOT NULL, |
||||
content_type TEXT NOT NULL, -- MIME type |
||||
size INTEGER NOT NULL, -- bytes |
||||
sha256 BLOB NOT NULL, -- 32 bytes |
||||
file_path TEXT, -- локальный путь к сохранённому файлу |
||||
signature BLOB NOT NULL CHECK(length(signature) == 64), |
||||
is_upload INTEGER DEFAULT 0, -- 0=скачано, 1=наше |
||||
created_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
CREATE INDEX idx_att_msg ON attachments(message_id); |
||||
|
||||
|
||||
-- 9. ui_state — сохранение состояния UI (последний канал, позиция скролла и т.д.) |
||||
CREATE TABLE ui_state ( |
||||
key TEXT PRIMARY KEY, |
||||
value TEXT |
||||
);================================================================================ |
||||
Основные запросы |
||||
================================================================================ |
||||
|
||||
-- Текущая нода (приватный ключ — только на этом устройстве) |
||||
SELECT node_id, name, x25519_pubkey, x25519_privkey, ed25519_pubkey |
||||
FROM local_identity WHERE id = 1; |
||||
|
||||
-- Другие наши ноды (без приватных ключей) |
||||
SELECT node_id, name, x25519_pubkey, ed25519_pubkey FROM my_nodes; |
||||
|
||||
-- Список каналов с последним сообщением |
||||
SELECT c.channel_id, c.name, c.is_dm, c.last_message, c.last_msg_at |
||||
FROM channels c |
||||
JOIN channel_members cm ON c.channel_id = cm.channel_id |
||||
WHERE cm.node_id = (SELECT node_id FROM local_identity WHERE id = 1) |
||||
ORDER BY c.last_msg_at DESC; |
||||
|
||||
-- Сообщения канала (последние 50) |
||||
SELECT id, author_node_id, content, content_type, timestamp, |
||||
signature, is_outgoing, is_read |
||||
FROM messages |
||||
WHERE channel_id = ? |
||||
ORDER BY timestamp DESC |
||||
LIMIT 50; |
||||
|
||||
-- Кто в канале |
||||
SELECT cm.node_id, n.name |
||||
FROM channel_members cm LEFT JOIN nodes n ON cm.node_id = n.node_id |
||||
WHERE cm.channel_id = ?; |
||||
|
||||
-- DM между двумя узлами |
||||
-- channel_id = "dm:" || min(a,b) || "_" || max(a,b) |
||||
|
||||
-- Реакции сообщения |
||||
SELECT node_id, emoji, signature FROM reactions WHERE message_id = ? ORDER BY created_at; |
||||
|
||||
-- Вложения сообщения |
||||
SELECT id, filename, content_type, size, sha256, file_path |
||||
FROM attachments WHERE message_id = ?; |
||||
|
||||
|
||||
================================================================================ |
||||
Примечания |
||||
================================================================================ |
||||
|
||||
- local_identity — одна строка (id=1), содержит x25519_privkey. |
||||
Приватный ключ получаем от utun через MSG_RSP_PRIVKEY (доступен только с localhost). |
||||
|
||||
- my_nodes — другие наши устройства, только pubkey-информация (нет приватных ключей). |
||||
|
||||
- Адреса и подсети (node_addresses, node_subnets) — зона ответственности utun. |
||||
Чат не занимается сетевым уровнем. |
||||
|
||||
- Все timestamp: INTEGER (unix epoch). messages.timestamp — миллисекунды, |
||||
created_at/updated_at/joined_at — секунды. |
||||
|
||||
- signature везде NOT NULL Ed25519 (64 bytes) — подпись автора контента: |
||||
messages : sign(edPriv, channel_id || author_node_id || content || timestamp) |
||||
reactions : sign(edPriv, message_id || node_id || emoji) |
||||
attachments: sign(edPriv, message_id || filename || content_type || size || sha256) |
||||
|
||||
- channel_id для DM: "dm:nodeA_nodeB" где A < B (канонический порядок). |
||||
|
||||
- channel_id для групп: "group:" + SHA256(owner_node_id || name || timestamp). |
||||
|
||||
- deleted_at / soft delete не добавлены — можно добавить позже при необходимости. |
||||
|
||||
@ -0,0 +1,167 @@
|
||||
Архитектура чата (группового) |
||||
|
||||
База данных: SQLite3 (WAL mode), chats.db |
||||
|
||||
================================================================================ |
||||
Схема БД (7 таблиц) |
||||
================================================================================ |
||||
|
||||
-- 1. my_nodes — наши собственные ноды (разные устройства / инстансы utun) |
||||
CREATE TABLE my_nodes ( |
||||
node_id INTEGER PRIMARY KEY, |
||||
name TEXT NOT NULL, -- human-readable имя устройства |
||||
x25519_pubkey BLOB NOT NULL, -- 32 bytes |
||||
ed25519_pubkey BLOB, -- 32 bytes (derive из x25519 privkey) |
||||
is_current INTEGER DEFAULT 0, -- 1 = текущий инстанс (к которому подключён GUI) |
||||
created_at INTEGER DEFAULT (unixepoch()), |
||||
updated_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
|
||||
|
||||
-- 2. nodes — известные удалённые узлы (получаем через msg_transport от utun) |
||||
CREATE TABLE nodes ( |
||||
node_id INTEGER PRIMARY KEY, |
||||
name TEXT, |
||||
x25519_pubkey BLOB NOT NULL, -- 32 bytes |
||||
ed25519_pubkey BLOB, -- 32 bytes (для проверки подписи пира) |
||||
is_online INTEGER DEFAULT 0, |
||||
last_seen_at INTEGER DEFAULT (unixepoch()), |
||||
created_at INTEGER DEFAULT (unixepoch()), |
||||
updated_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
|
||||
|
||||
-- 3. channels — каналы (DM и групповые) |
||||
CREATE TABLE channels ( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
channel_id TEXT NOT NULL UNIQUE, -- "dm:nodeA_nodeB" (A<B) или "group:hash" |
||||
name TEXT NOT NULL, |
||||
owner_node_id INTEGER, -- создатель (NULL для DM) |
||||
is_dm INTEGER DEFAULT 0, |
||||
last_message TEXT, |
||||
last_msg_at INTEGER, -- unix timestamp ms |
||||
created_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
CREATE UNIQUE INDEX idx_channel_cid ON channels(channel_id); |
||||
|
||||
|
||||
-- 4. channel_members — участники канала |
||||
CREATE TABLE channel_members ( |
||||
channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE, |
||||
node_id INTEGER NOT NULL, |
||||
joined_at INTEGER DEFAULT (unixepoch()), |
||||
PRIMARY KEY (channel_id, node_id) |
||||
); |
||||
CREATE INDEX idx_cm_node ON channel_members(node_id); |
||||
|
||||
|
||||
-- 5. messages — сообщения чата |
||||
CREATE TABLE messages ( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE, |
||||
author_node_id INTEGER NOT NULL, |
||||
content TEXT NOT NULL CHECK(length(content) <= 4096), |
||||
content_type TEXT DEFAULT 'text/plain', |
||||
timestamp INTEGER NOT NULL, -- unix milliseconds |
||||
signature BLOB CHECK(signature IS NULL OR length(signature) == 64), |
||||
is_outgoing INTEGER DEFAULT 0, |
||||
is_read INTEGER DEFAULT 0, |
||||
created_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
CREATE INDEX idx_msg_channel_time ON messages(channel_id, timestamp); |
||||
CREATE INDEX idx_msg_author ON messages(author_node_id); |
||||
-- дедупликация: один автор не может отправить два сообщения в один ms в один канал |
||||
CREATE UNIQUE INDEX idx_msg_dedup ON messages(channel_id, author_node_id, timestamp); |
||||
|
||||
|
||||
-- 6. reactions — реакции на сообщения |
||||
CREATE TABLE reactions ( |
||||
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE, |
||||
node_id INTEGER NOT NULL, |
||||
emoji TEXT NOT NULL, -- unicode codepoint или :shortcode: |
||||
signature BLOB, -- Ed25519 (64 bytes) |
||||
created_at INTEGER DEFAULT (unixepoch()), |
||||
PRIMARY KEY (message_id, node_id, emoji) |
||||
); |
||||
|
||||
|
||||
-- 7. attachments — вложения к сообщениям |
||||
CREATE TABLE attachments ( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE, |
||||
filename TEXT NOT NULL, |
||||
content_type TEXT NOT NULL, -- MIME type |
||||
size INTEGER NOT NULL, -- bytes |
||||
sha256 BLOB NOT NULL, -- 32 bytes |
||||
file_path TEXT, -- локальный путь к сохранённому файлу |
||||
signature BLOB, -- Ed25519 (64 bytes) |
||||
is_upload INTEGER DEFAULT 0, -- 0=скачано, 1=наше |
||||
created_at INTEGER DEFAULT (unixepoch()) |
||||
); |
||||
CREATE INDEX idx_att_msg ON attachments(message_id); |
||||
|
||||
|
||||
================================================================================ |
||||
Основные запросы |
||||
================================================================================ |
||||
|
||||
-- Текущая нода |
||||
SELECT node_id, name, x25519_pubkey, ed25519_pubkey |
||||
FROM my_nodes WHERE is_current = 1; |
||||
|
||||
-- Все свои ноды |
||||
SELECT node_id, name, is_current FROM my_nodes ORDER BY created_at; |
||||
|
||||
-- Список каналов с последним сообщением (для текущего пользователя) |
||||
SELECT c.channel_id, c.name, c.is_dm, c.last_message, c.last_msg_at |
||||
FROM channels c |
||||
JOIN channel_members cm ON c.channel_id = cm.channel_id |
||||
JOIN my_nodes mn ON cm.node_id = mn.node_id |
||||
WHERE mn.is_current = 1 |
||||
ORDER BY c.last_msg_at DESC; |
||||
|
||||
-- Сообщения канала (последние 50) |
||||
SELECT id, author_node_id, content, content_type, timestamp, |
||||
signature, is_outgoing, is_read |
||||
FROM messages |
||||
WHERE channel_id = ? |
||||
ORDER BY timestamp DESC |
||||
LIMIT 50; |
||||
|
||||
-- Кто в канале |
||||
SELECT cm.node_id, n.name |
||||
FROM channel_members cm LEFT JOIN nodes n ON cm.node_id = n.node_id |
||||
WHERE cm.channel_id = ?; |
||||
|
||||
-- DM между двумя узлами |
||||
-- channel_id = "dm:" || min(a,b) || "_" || max(a,b) |
||||
|
||||
-- Реакции сообщения |
||||
SELECT node_id, emoji, signature FROM reactions WHERE message_id = ? ORDER BY created_at; |
||||
|
||||
-- Вложения сообщения |
||||
SELECT id, filename, content_type, size, sha256, file_path |
||||
FROM attachments WHERE message_id = ?; |
||||
|
||||
|
||||
================================================================================ |
||||
Примечания |
||||
================================================================================ |
||||
|
||||
- Адреса и подсети (node_addresses, node_subnets) — зона ответственности utun. |
||||
Чат не занимается сетевым уровнем. При необходимости utun отдаёт эту информацию |
||||
через control_server (etcpmon протокол). |
||||
|
||||
- Все timestamp: INTEGER (unix epoch). messages.timestamp — миллисекунды, |
||||
created_at/updated_at/joined_at — секунды. |
||||
|
||||
- signature везде Ed25519 (64 bytes) — NULL если не подписано. |
||||
|
||||
- channel_id для DM: "dm:nodeA_nodeB" где A < B (канонический порядок). |
||||
|
||||
- channel_id для групп: "group:" + SHA256(owner_node_id || name || timestamp). |
||||
|
||||
- local_seq (из предыдущей версии) убран — дедупликация через UNIQUE индекс |
||||
(channel_id, author_node_id, timestamp) достаточна. |
||||
|
||||
- deleted_at / soft delete не добавлены — можно добавить позже при необходимости. |
||||
@ -0,0 +1,100 @@
|
||||
#include "invite_link.h" |
||||
#include "debug_ui.h" |
||||
#include <QByteArray> |
||||
|
||||
static QByteArray encodeAddresses(const QList<NodeAddr>& addrs) { |
||||
QByteArray buf; |
||||
for (const auto& a : addrs) { |
||||
buf.append(static_cast<char>(a.family)); |
||||
buf.append(a.address); |
||||
buf.append(static_cast<char>((a.port >> 8) & 0xFF)); |
||||
buf.append(static_cast<char>(a.port & 0xFF)); |
||||
} |
||||
return buf; |
||||
} |
||||
|
||||
static QList<InviteAddr> decodeAddresses(const uint8_t* data, int len, |
||||
int count) { |
||||
QList<InviteAddr> list; |
||||
int off = 0; |
||||
for (int i = 0; i < count && off < len; i++) { |
||||
if (off + 1 > len) break; |
||||
InviteAddr a; |
||||
a.family = data[off++]; |
||||
int alen = (a.family == 4) ? 4 : 16; |
||||
if (off + alen + 2 > len) break; |
||||
a.address = QByteArray(reinterpret_cast<const char*>(data + off), alen); |
||||
off += alen; |
||||
a.port = (static_cast<quint16>(data[off]) << 8) | data[off + 1]; |
||||
off += 2; |
||||
list.append(a); |
||||
} |
||||
return list; |
||||
} |
||||
|
||||
QString encodeInviteLink(const QString& channelId, const QByteArray& pubkey, |
||||
const QList<NodeAddr>& addrs) { |
||||
QByteArray cid = channelId.toUtf8(); |
||||
if (cid.size() > 65535) { |
||||
GUI_ERROR("encodeInviteLink: channelId too long (%d bytes)", cid.size()); |
||||
return {}; |
||||
} |
||||
if (pubkey.size() != 32) { |
||||
GUI_ERROR("encodeInviteLink: pubkey must be 32 bytes (got %d)", pubkey.size()); |
||||
return {}; |
||||
} |
||||
|
||||
QByteArray addrsEnc = encodeAddresses(addrs); |
||||
int addrCount = addrs.size(); |
||||
if (addrCount > 65535) addrCount = 65535; |
||||
|
||||
QByteArray blob; |
||||
blob.reserve(1 + 2 + cid.size() + 32 + 2 + addrsEnc.size()); |
||||
blob.append(static_cast<char>(INVITE_LINK_VERSION)); |
||||
blob.append(static_cast<char>((cid.size() >> 8) & 0xFF)); |
||||
blob.append(static_cast<char>(cid.size() & 0xFF)); |
||||
blob.append(cid); |
||||
blob.append(pubkey); |
||||
blob.append(static_cast<char>((addrCount >> 8) & 0xFF)); |
||||
blob.append(static_cast<char>(addrCount & 0xFF)); |
||||
blob.append(addrsEnc); |
||||
|
||||
return QString::fromLatin1("utun://") + QString::fromLatin1(blob.toBase64()); |
||||
} |
||||
|
||||
InviteData decodeInviteLink(const QString& link) { |
||||
InviteData d; |
||||
if (!link.startsWith(QLatin1String("utun://"))) { |
||||
GUI_ERROR("decodeInviteLink: invalid prefix"); |
||||
return d; |
||||
} |
||||
QByteArray blob = QByteArray::fromBase64( |
||||
link.mid(7).toLatin1()); |
||||
const auto* data = reinterpret_cast<const uint8_t*>(blob.constData()); |
||||
int len = blob.size(); |
||||
|
||||
if (len < 5) { |
||||
GUI_ERROR("decodeInviteLink: blob too short (%d bytes)", len); |
||||
return d; |
||||
} |
||||
int off = 0; |
||||
uint8_t ver = data[off++]; |
||||
if (ver != INVITE_LINK_VERSION) { |
||||
GUI_ERROR("decodeInviteLink: unsupported version %d", ver); |
||||
return d; |
||||
} |
||||
int cidLen = (static_cast<int>(data[off]) << 8) | data[off + 1]; |
||||
off += 2; |
||||
if (off + cidLen + 32 + 2 > len) { |
||||
GUI_ERROR("decodeInviteLink: truncated blob"); |
||||
return d; |
||||
} |
||||
d.channelId = QString::fromUtf8(reinterpret_cast<const char*>(data + off), cidLen); |
||||
off += cidLen; |
||||
d.pubkey = QByteArray(reinterpret_cast<const char*>(data + off), 32); |
||||
off += 32; |
||||
int addrCount = (static_cast<int>(data[off]) << 8) | data[off + 1]; |
||||
off += 2; |
||||
d.addrs = decodeAddresses(data + off, len - off, addrCount); |
||||
return d; |
||||
} |
||||
@ -0,0 +1,26 @@
|
||||
#pragma once |
||||
|
||||
#include <QString> |
||||
#include <QByteArray> |
||||
#include <QList> |
||||
#include <cstdint> |
||||
#include "../db/db_manager.h" |
||||
|
||||
#define INVITE_LINK_VERSION 0x01 |
||||
|
||||
struct InviteAddr { |
||||
int family; // 4=IPv4, 6=IPv6
|
||||
QByteArray address; |
||||
quint16 port; |
||||
}; |
||||
|
||||
struct InviteData { |
||||
QString channelId; |
||||
QByteArray pubkey; |
||||
QList<InviteAddr> addrs; |
||||
}; |
||||
|
||||
QString encodeInviteLink(const QString& channelId, const QByteArray& pubkey, |
||||
const QList<NodeAddr>& addrs); |
||||
|
||||
InviteData decodeInviteLink(const QString& link); |
||||
@ -0,0 +1,47 @@
|
||||
#include "invitedialog.h" |
||||
#include "qrcode_utils.h" |
||||
#include <QVBoxLayout> |
||||
#include <QHBoxLayout> |
||||
#include <QLineEdit> |
||||
#include <QPushButton> |
||||
#include <QLabel> |
||||
#include <QClipboard> |
||||
#include <QApplication> |
||||
|
||||
InviteDialog::InviteDialog(const QString& link, QWidget* parent) |
||||
: QDialog(parent), m_link(link) |
||||
{ |
||||
setWindowTitle("Пригласительная ссылка"); |
||||
setMinimumWidth(420); |
||||
setModal(true); |
||||
|
||||
auto *layout = new QVBoxLayout(this); |
||||
layout->setSpacing(12); |
||||
|
||||
auto *linkEdit = new QLineEdit(m_link, this); |
||||
linkEdit->setReadOnly(true); |
||||
linkEdit->setStyleSheet("padding: 6px; font-family: monospace;"); |
||||
layout->addWidget(linkEdit); |
||||
|
||||
auto *btnLayout = new QHBoxLayout(); |
||||
auto *copyBtn = new QPushButton("Скопировать", this); |
||||
connect(copyBtn, &QPushButton::clicked, this, [this, linkEdit]() { |
||||
QApplication::clipboard()->setText(m_link); |
||||
linkEdit->setFocus(); |
||||
}); |
||||
btnLayout->addWidget(copyBtn); |
||||
btnLayout->addStretch(); |
||||
layout->addLayout(btnLayout); |
||||
|
||||
auto *qrLabel = new QLabel(this); |
||||
QImage qrImage = generateQRCode(m_link); |
||||
if (!qrImage.isNull()) |
||||
qrLabel->setPixmap(QPixmap::fromImage(qrImage).scaled( |
||||
180, 180, Qt::KeepAspectRatio, Qt::SmoothTransformation)); |
||||
qrLabel->setAlignment(Qt::AlignCenter); |
||||
layout->addWidget(qrLabel, 0, Qt::AlignCenter); |
||||
|
||||
auto *closeBtn = new QPushButton("OK", this); |
||||
connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept); |
||||
layout->addWidget(closeBtn, 0, Qt::AlignRight); |
||||
} |
||||
@ -0,0 +1,13 @@
|
||||
#pragma once |
||||
|
||||
#include <QDialog> |
||||
#include <QString> |
||||
|
||||
class InviteDialog : public QDialog { |
||||
Q_OBJECT |
||||
public: |
||||
explicit InviteDialog(const QString& link, QWidget* parent = nullptr); |
||||
|
||||
private: |
||||
QString m_link; |
||||
}; |
||||
@ -0,0 +1,26 @@
|
||||
#include "qrcode_utils.h" |
||||
#include "ZXingQt.h" |
||||
|
||||
QImage generateQRCode(const QString& text, int moduleSize) |
||||
{ |
||||
using namespace ZXingQt; |
||||
auto barcode = Barcode::fromText(text, BarcodeFormat::QRCode); |
||||
if (!barcode.isValid()) |
||||
return {}; |
||||
WriterOptions opts; |
||||
if (moduleSize > 0) |
||||
opts.scale(moduleSize); |
||||
return barcode.toImage(opts); |
||||
} |
||||
|
||||
QStringList readQRCodes(const QImage& image) |
||||
{ |
||||
using namespace ZXingQt; |
||||
auto results = ReadBarcodes(image); |
||||
QStringList texts; |
||||
for (const auto& b : results) { |
||||
if (b.isValid()) |
||||
texts.append(b.text()); |
||||
} |
||||
return texts; |
||||
} |
||||
@ -0,0 +1,9 @@
|
||||
#pragma once |
||||
|
||||
#include <QImage> |
||||
#include <QString> |
||||
#include <QStringList> |
||||
|
||||
QImage generateQRCode(const QString& text, int moduleSize = 8); |
||||
|
||||
QStringList readQRCodes(const QImage& image); |
||||
@ -0,0 +1,247 @@
|
||||
/*
|
||||
* test_crypto.cpp — тесты криптографических функций (Crypto::) |
||||
*/ |
||||
|
||||
#include <QTest> |
||||
#include <QTemporaryFile> |
||||
#include <QByteArray> |
||||
#include <QElapsedTimer> |
||||
#include <QAtomicInt> |
||||
#include <QDebug> |
||||
#include <cstring> |
||||
#include "../transport/crypto.h" |
||||
|
||||
class TestCrypto : public QObject { |
||||
Q_OBJECT |
||||
|
||||
private slots: |
||||
void initTestCase(); |
||||
void test_derive_key_sizes(); |
||||
void test_derive_priv_not_empty(); |
||||
void test_derive_pub_not_empty(); |
||||
void test_priv_ne_pub(); |
||||
void test_sign_verify_roundtrip(); |
||||
void test_sign_verify_empty(); |
||||
void test_verify_wrong_sig(); |
||||
void test_verify_wrong_key(); |
||||
void test_sign_empty_key(); |
||||
void test_sign_invalid_key_size(); |
||||
void test_signFile_verifyFile(); |
||||
void test_signFile_nonexistent(); |
||||
void test_sign_verify_100mb_parallel(); |
||||
|
||||
private: |
||||
void writeTestFile(const QString& path, unsigned char pattern, size_t fileSize); |
||||
|
||||
private: |
||||
QByteArray m_x25519Priv; // 32 random bytes
|
||||
QByteArray m_edPriv; // derived Ed25519 private key
|
||||
QByteArray m_edPub; // derived Ed25519 public key
|
||||
QByteArray m_message; |
||||
}; |
||||
|
||||
void TestCrypto::initTestCase() { |
||||
m_x25519Priv = QByteArray(Crypto::kKeySize, 0); |
||||
for (int i = 0; i < static_cast<int>(Crypto::kKeySize); i++) |
||||
m_x25519Priv[i] = static_cast<char>(0x42 + i); /* детерминированное значение */ |
||||
|
||||
m_edPriv = Crypto::deriveEd25519Privkey(m_x25519Priv); |
||||
m_edPub = Crypto::deriveEd25519Pubkey(m_x25519Priv); |
||||
m_message = QByteArray("hello world"); |
||||
} |
||||
|
||||
void TestCrypto::test_derive_key_sizes() { |
||||
QCOMPARE(m_edPriv.size(), static_cast<int>(Crypto::kKeySize)); |
||||
QCOMPARE(m_edPub.size(), static_cast<int>(Crypto::kKeySize)); |
||||
} |
||||
|
||||
void TestCrypto::test_derive_priv_not_empty() { |
||||
QVERIFY(!m_edPriv.isEmpty()); |
||||
} |
||||
|
||||
void TestCrypto::test_derive_pub_not_empty() { |
||||
QVERIFY(!m_edPub.isEmpty()); |
||||
} |
||||
|
||||
void TestCrypto::test_priv_ne_pub() { |
||||
QVERIFY(m_edPriv != m_edPub); |
||||
} |
||||
|
||||
void TestCrypto::test_sign_verify_roundtrip() { |
||||
QByteArray sig = Crypto::sign(m_edPriv, m_message); |
||||
QCOMPARE(sig.size(), static_cast<int>(Crypto::kSignSize)); |
||||
QVERIFY(Crypto::verify(m_edPub, m_message, sig)); |
||||
} |
||||
|
||||
void TestCrypto::test_sign_verify_empty() { |
||||
QByteArray empty; |
||||
QByteArray sig = Crypto::sign(m_edPriv, empty); |
||||
QCOMPARE(sig.size(), static_cast<int>(Crypto::kSignSize)); |
||||
QVERIFY(Crypto::verify(m_edPub, empty, sig)); |
||||
} |
||||
|
||||
void TestCrypto::test_verify_wrong_sig() { |
||||
QByteArray sig = Crypto::sign(m_edPriv, m_message); |
||||
QVERIFY(!sig.isEmpty()); |
||||
sig[8] = static_cast<char>(sig[8] ^ 1); /* портим подпись */ |
||||
QVERIFY(!Crypto::verify(m_edPub, m_message, sig)); |
||||
} |
||||
|
||||
void TestCrypto::test_verify_wrong_key() { |
||||
QByteArray sig = Crypto::sign(m_edPriv, m_message); |
||||
/* другой ключ */ |
||||
QByteArray otherPriv(Crypto::kKeySize, 0); |
||||
for (int i = 0; i < static_cast<int>(Crypto::kKeySize); i++) |
||||
otherPriv[i] = static_cast<char>(0x77 + i); |
||||
QByteArray otherPub = Crypto::deriveEd25519Pubkey(otherPriv); |
||||
QVERIFY(!otherPub.isEmpty()); |
||||
QVERIFY(!Crypto::verify(otherPub, m_message, sig)); |
||||
} |
||||
|
||||
void TestCrypto::test_sign_empty_key() { |
||||
QByteArray sig = Crypto::sign(QByteArray(), m_message); |
||||
QVERIFY(sig.isEmpty()); |
||||
QVERIFY(!Crypto::verify(QByteArray(), m_message, sig)); |
||||
} |
||||
|
||||
void TestCrypto::test_sign_invalid_key_size() { |
||||
QByteArray badKey(16, 0x42); |
||||
QByteArray sig = Crypto::sign(badKey, m_message); |
||||
QVERIFY(sig.isEmpty()); |
||||
} |
||||
|
||||
void TestCrypto::test_signFile_verifyFile() { |
||||
QTemporaryFile tmp; |
||||
QVERIFY(tmp.open()); |
||||
QByteArray content("file content for signing test"); |
||||
tmp.write(content); |
||||
tmp.close(); |
||||
|
||||
QByteArray sig; |
||||
QEventLoop loop; |
||||
Crypto::signFile(tmp.fileName(), m_edPriv, |
||||
[&](const Crypto::SignResult& r) { |
||||
sig = r.signature; |
||||
QVERIFY(r.ok); |
||||
QVERIFY(!sig.isEmpty()); |
||||
loop.quit(); |
||||
}); |
||||
loop.exec(); |
||||
|
||||
QEventLoop loop2; |
||||
Crypto::verifyFile(tmp.fileName(), m_edPub, sig, |
||||
[&](const Crypto::SignResult& r) { |
||||
QVERIFY(r.ok); |
||||
loop2.quit(); |
||||
}); |
||||
loop2.exec(); |
||||
} |
||||
|
||||
void TestCrypto::test_signFile_nonexistent() { |
||||
QEventLoop loop; |
||||
Crypto::signFile("/nonexistent/path/test", m_edPriv, |
||||
[&](const Crypto::SignResult& r) { |
||||
QVERIFY(!r.ok); |
||||
QVERIFY(!r.error.isEmpty()); |
||||
loop.quit(); |
||||
}); |
||||
loop.exec(); |
||||
} |
||||
|
||||
void TestCrypto::writeTestFile(const QString& path, unsigned char pattern, size_t fileSize) { |
||||
QFile f(path); |
||||
QVERIFY(f.open(QIODevice::WriteOnly)); |
||||
constexpr size_t kChunkSize = 65536; |
||||
QByteArray chunk(static_cast<int>(kChunkSize), Qt::Uninitialized); |
||||
memset(chunk.data(), pattern, kChunkSize); |
||||
size_t remaining = fileSize; |
||||
while (remaining >= kChunkSize) { |
||||
QCOMPARE(f.write(chunk), static_cast<qint64>(kChunkSize)); |
||||
remaining -= kChunkSize; |
||||
} |
||||
if (remaining > 0) f.write(chunk.constData(), static_cast<qint64>(remaining)); |
||||
f.close(); |
||||
} |
||||
|
||||
void TestCrypto::test_sign_verify_100mb_parallel() { |
||||
constexpr size_t kFileSize = 100ULL * 1024 * 1024; |
||||
constexpr int kNumFiles = 3; |
||||
|
||||
QString paths[kNumFiles]; |
||||
QTemporaryFile tmp[kNumFiles]; |
||||
for (int i = 0; i < kNumFiles; i++) { |
||||
QVERIFY(tmp[i].open()); |
||||
paths[i] = tmp[i].fileName(); |
||||
tmp[i].close(); |
||||
writeTestFile(paths[i], static_cast<unsigned char>(0xAA + i), kFileSize); |
||||
} |
||||
|
||||
/* ------- Sign (параллельно) ------- */ |
||||
QByteArray sigs[kNumFiles]; |
||||
qint64 signTimesMs[kNumFiles] = {}; |
||||
QElapsedTimer totalSignTimer; |
||||
QAtomicInt signDone{0}; |
||||
QEventLoop signLoop; |
||||
|
||||
totalSignTimer.start(); |
||||
for (int i = 0; i < kNumFiles; i++) { |
||||
QElapsedTimer t; |
||||
t.start(); |
||||
Crypto::signFile(paths[i], m_edPriv, |
||||
[&, i, t](const Crypto::SignResult& r) mutable { |
||||
QVERIFY(r.ok); |
||||
QVERIFY(!r.signature.isEmpty()); |
||||
sigs[i] = r.signature; |
||||
signTimesMs[i] = t.elapsed(); |
||||
if (signDone.fetchAndAddRelaxed(1) == kNumFiles - 1) |
||||
signLoop.quit(); |
||||
}); |
||||
} |
||||
signLoop.exec(); |
||||
qint64 totalSignMs = totalSignTimer.elapsed(); |
||||
|
||||
/* ------- Verify (параллельно) ------- */ |
||||
qint64 verifyTimesMs[kNumFiles] = {}; |
||||
QElapsedTimer totalVerifyTimer; |
||||
QAtomicInt verifyDone{0}; |
||||
QEventLoop verifyLoop; |
||||
|
||||
totalVerifyTimer.start(); |
||||
for (int i = 0; i < kNumFiles; i++) { |
||||
QElapsedTimer t; |
||||
t.start(); |
||||
Crypto::verifyFile(paths[i], m_edPub, sigs[i], |
||||
[&, i, t](const Crypto::SignResult& r) mutable { |
||||
QVERIFY(r.ok); |
||||
verifyTimesMs[i] = t.elapsed(); |
||||
if (verifyDone.fetchAndAddRelaxed(1) == kNumFiles - 1) |
||||
verifyLoop.quit(); |
||||
}); |
||||
} |
||||
verifyLoop.exec(); |
||||
qint64 totalVerifyMs = totalVerifyTimer.elapsed(); |
||||
|
||||
/* ------- Профилирование ------- */ |
||||
qint64 signSum = 0, verifySum = 0; |
||||
for (int i = 0; i < kNumFiles; i++) { |
||||
signSum += signTimesMs[i]; |
||||
verifySum += verifyTimesMs[i]; |
||||
} |
||||
|
||||
qDebug() << "\n======= test_sign_verify_100mb_parallel ======="; |
||||
for (int i = 0; i < kNumFiles; i++) |
||||
qDebug().nospace() << " File " << i << ": sign=" << signTimesMs[i] |
||||
<< "ms | verify=" << verifyTimesMs[i] << "ms"; |
||||
qDebug().nospace() << "Phase sign: " << totalSignMs << "ms total" |
||||
<< " (sum=" << signSum << "ms, speedup=" |
||||
<< (double)signSum / totalSignMs << "x)"; |
||||
qDebug().nospace() << "Phase verify: " << totalVerifyMs << "ms total" |
||||
<< " (sum=" << verifySum << "ms, speedup=" |
||||
<< (double)verifySum / totalVerifyMs << "x)"; |
||||
|
||||
/* Вычищаем временные файлы */ |
||||
for (int i = 0; i < kNumFiles; i++) QFile::remove(paths[i]); |
||||
} |
||||
|
||||
QTEST_MAIN(TestCrypto) |
||||
#include "test_crypto.moc" |
||||
@ -0,0 +1,376 @@
|
||||
#include "chat_propagator.h" |
||||
#include "chat_proto.h" |
||||
#include "msg_client.h" |
||||
#include "crypto.h" |
||||
#include "db_manager.h" |
||||
|
||||
#include <QDateTime> |
||||
#include <algorithm> |
||||
#include <random> |
||||
|
||||
static uint64_t read64(const uint8_t* p) { |
||||
uint64_t v; memcpy(&v, p, 8); return v; |
||||
} |
||||
static void write64(uint8_t* p, uint64_t v) { memcpy(p, &v, 8); } |
||||
static uint32_t read32(const uint8_t* p) { |
||||
uint32_t v; memcpy(&v, p, 4); return v; |
||||
} |
||||
static void write32(uint8_t* p, uint32_t v) { memcpy(p, &v, 4); } |
||||
static uint16_t read16(const uint8_t* p) { |
||||
uint16_t v; memcpy(&v, p, 2); return v; |
||||
} |
||||
static void write16(uint8_t* p, uint16_t v) { memcpy(p, &v, 2); } |
||||
|
||||
ChatPropagator::ChatPropagator(MsgClient* client, DbManager* db, QObject* parent) |
||||
: QObject(parent) |
||||
, m_client(client) |
||||
, m_db(db) |
||||
, m_ackTimer(new QTimer(this)) |
||||
{ |
||||
connect(m_client, &MsgClient::received, |
||||
this, &ChatPropagator::onReceived); |
||||
connect(m_ackTimer, &QTimer::timeout, |
||||
this, &ChatPropagator::onAckTimeout); |
||||
} |
||||
|
||||
void ChatPropagator::setKeys(const QByteArray& edPriv, const QByteArray& edPub, quint64 nodeId) { |
||||
m_edPriv = edPriv; |
||||
m_edPub = edPub; |
||||
m_myNodeId = nodeId; |
||||
} |
||||
|
||||
void ChatPropagator::start() { |
||||
if (!m_edPriv.isEmpty() && !m_ackTimer->isActive()) |
||||
m_ackTimer->start(250); |
||||
startCatchUp(); |
||||
} |
||||
|
||||
/* ======================================================================== */ |
||||
|
||||
void ChatPropagator::propagateNewMessage(const QString& channelId, |
||||
const QByteArray& content, |
||||
qint64 timestamp, |
||||
const QByteArray& signature) { |
||||
if (m_edPriv.isEmpty() || !m_myNodeId) return; |
||||
|
||||
QByteArray packet = buildNewMsgPacket(channelId, content, timestamp, signature); |
||||
QList<quint64> exclude; |
||||
exclude.append(m_myNodeId); |
||||
fanoutGossip(channelId, packet, signature, ChatProp::kDefaultFanout, exclude); |
||||
} |
||||
|
||||
QByteArray ChatPropagator::buildNewMsgPacket(const QString& channelId, |
||||
const QByteArray& content, |
||||
qint64 timestamp, |
||||
const QByteArray& signature) { |
||||
QByteArray cid = channelId.toUtf8(); |
||||
uint8_t cidLen = static_cast<uint8_t>(cid.size()); |
||||
size_t total = 1 + 1 + cidLen + 8 + 2 + content.size() + ChatProp::kSignSize; |
||||
|
||||
QByteArray pkt(static_cast<int>(total), Qt::Uninitialized); |
||||
uint8_t* p = reinterpret_cast<uint8_t*>(pkt.data()); |
||||
*p++ = ChatProp::kNewMsg; |
||||
*p++ = cidLen; |
||||
memcpy(p, cid.constData(), cidLen); p += cidLen; |
||||
write64(p, static_cast<uint64_t>(timestamp)); p += 8; |
||||
write16(p, static_cast<uint16_t>(content.size())); p += 2; |
||||
memcpy(p, content.constData(), content.size()); p += content.size(); |
||||
memcpy(p, signature.constData(), ChatProp::kSignSize); |
||||
return pkt; |
||||
} |
||||
|
||||
QByteArray ChatPropagator::buildAckPacket(const QByteArray& sig) { |
||||
QByteArray pkt(static_cast<int>(1 + ChatProp::kSignSize), Qt::Uninitialized); |
||||
pkt[0] = static_cast<char>(ChatProp::kNewAck); |
||||
memcpy(pkt.data() + 1, sig.constData(), ChatProp::kSignSize); |
||||
return pkt; |
||||
} |
||||
|
||||
/* ======================================================================== */ |
||||
|
||||
void ChatPropagator::fanoutGossip(const QString& channelId, const QByteArray& packet, |
||||
const QByteArray& sig, int fanout, |
||||
const QList<quint64>& exclude) { |
||||
auto peers = pickRandomPeers(fanout, exclude); |
||||
if (peers.isEmpty()) return; |
||||
|
||||
GossipEntry e; |
||||
e.channelId = channelId; |
||||
e.packet = packet; |
||||
e.sig = sig; |
||||
e.peers = peers; |
||||
e.sentAt = QDateTime::currentMSecsSinceEpoch(); |
||||
m_pending[sig] = e; |
||||
|
||||
for (auto peer : peers) |
||||
sendGossip(peer, packet); |
||||
} |
||||
|
||||
void ChatPropagator::sendGossip(quint64 peer, const QByteArray& packet) { |
||||
m_client->sendTo(peer, packet); |
||||
} |
||||
|
||||
/* ======================================================================== */ |
||||
|
||||
void ChatPropagator::onReceived(quint64 peerId, const QByteArray& data) { |
||||
if (data.isEmpty()) return; |
||||
uint8_t type = static_cast<uint8_t>(data[0]); |
||||
switch (type) { |
||||
case ChatProp::kNewMsg: handleNewMsg(peerId, data); break; |
||||
case ChatProp::kNewAck: handleNewAck(data); break; |
||||
case ChatProp::kBucketsRsp: handleBucketsRsp(data); break; |
||||
case ChatProp::kHashesRsp: handleHashesRsp(data); break; |
||||
case ChatProp::kMsgsRsp: handleMsgsRsp(data); break; |
||||
} |
||||
} |
||||
|
||||
void ChatPropagator::handleNewMsg(quint64 fromPeer, const QByteArray& payload) { |
||||
QString channelId; |
||||
QByteArray content; |
||||
qint64 timestamp; |
||||
QByteArray sig; |
||||
if (!parseNewMsg(payload, channelId, content, timestamp, sig)) |
||||
return; |
||||
|
||||
/* verify sender's signature */ |
||||
QByteArray peerEdPub = m_db->getNodeEdPub(fromPeer); |
||||
if (!peerEdPub.isEmpty() && !Crypto::verify(peerEdPub, payload.left(payload.size() - 64), sig)) |
||||
return; |
||||
|
||||
/* insert into DB */ |
||||
m_db->insertMessage(channelId, fromPeer, content, timestamp, 0); |
||||
|
||||
emit messageReceived(channelId, fromPeer, content, timestamp); |
||||
|
||||
/* re-gossip */ |
||||
QList<quint64> exclude; |
||||
exclude.append(m_myNodeId); |
||||
exclude.append(fromPeer); |
||||
fanoutGossip(channelId, payload, sig, ChatProp::kDefaultFanout, exclude); |
||||
|
||||
/* ack to sender */ |
||||
m_client->sendTo(fromPeer, buildAckPacket(sig)); |
||||
} |
||||
|
||||
void ChatPropagator::handleNewAck(const QByteArray& payload) { |
||||
if (payload.size() < static_cast<int>(1 + ChatProp::kSignSize)) return; |
||||
QByteArray sig = payload.mid(1, static_cast<int>(ChatProp::kSignSize)); |
||||
|
||||
auto it = m_pending.find(sig); |
||||
if (it == m_pending.end()) return; |
||||
|
||||
/* mark all peers as acked (simple version: ACK means message delivered) */ |
||||
m_pending.erase(it); |
||||
} |
||||
|
||||
void ChatPropagator::onAckTimeout() { |
||||
qint64 now = QDateTime::currentMSecsSinceEpoch(); |
||||
QList<QByteArray> toRemove; |
||||
|
||||
for (auto it = m_pending.begin(); it != m_pending.end(); ++it) { |
||||
auto& e = it.value(); |
||||
if (now - e.sentAt < ChatProp::kDefaultAckTimeoutMs) continue; |
||||
|
||||
if (e.retries >= 3) { |
||||
toRemove.append(it.key()); |
||||
continue; |
||||
} |
||||
|
||||
/* retry: pick new peers for each missing ACK */ |
||||
QList<quint64> unacked; |
||||
for (auto p : e.peers) |
||||
if (!e.acked.contains(p)) unacked.append(p); |
||||
|
||||
if (!unacked.isEmpty()) { |
||||
auto newPeers = pickRandomPeers(1, e.peers + e.acked + |
||||
QList<quint64>{m_myNodeId}); |
||||
if (!newPeers.isEmpty()) { |
||||
sendGossip(newPeers.first(), e.packet); |
||||
e.peers.append(newPeers.first()); |
||||
} |
||||
} |
||||
|
||||
e.sentAt = now; |
||||
e.retries++; |
||||
} |
||||
|
||||
for (auto& k : toRemove) m_pending.remove(k); |
||||
} |
||||
|
||||
/* ========================================================================
|
||||
* Catch-up |
||||
* ======================================================================== */ |
||||
|
||||
void ChatPropagator::startCatchUp() { |
||||
if (!m_db) return; |
||||
auto channels = m_db->getChannels(); |
||||
if (channels.isEmpty()) return; |
||||
|
||||
auto peers = pickRandomPeers(ChatProp::kDefaultFanout, {m_myNodeId}); |
||||
if (peers.isEmpty()) return; |
||||
|
||||
for (auto& ch : channels) { |
||||
QByteArray cid = ch.channelId.toUtf8(); |
||||
QByteArray pkt(static_cast<int>(2 + cid.size()), Qt::Uninitialized); |
||||
pkt[0] = static_cast<char>(ChatProp::kBucketsReq); |
||||
pkt[1] = static_cast<char>(cid.size()); |
||||
memcpy(pkt.data() + 2, cid.constData(), cid.size()); |
||||
|
||||
for (auto peer : peers) |
||||
sendGossip(peer, pkt); |
||||
} |
||||
} |
||||
|
||||
void ChatPropagator::handleBucketsRsp(const QByteArray& payload) { |
||||
if (payload.size() < 3) return; |
||||
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData()); |
||||
if (*p != ChatProp::kBucketsRsp) return; p++; |
||||
uint8_t cidLen = *p++; |
||||
if (p + cidLen + 2 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) return; |
||||
QString channelId = QString::fromUtf8(reinterpret_cast<const char*>(p), cidLen); p += cidLen; |
||||
uint16_t bCount = read16(p); p += 2; |
||||
|
||||
for (int i = 0; i < bCount; i++) { |
||||
if (p + 4 + 1 + 4 + 32 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) break; |
||||
qint64 day = static_cast<qint64>(read32(p)); p += 4; |
||||
int hour = *p++; |
||||
int rCount = static_cast<int>(read32(p)); p += 4; |
||||
QByteArray rHash(reinterpret_cast<const char*>(p), 32); p += 32; |
||||
|
||||
auto local = m_db->getBucketMeta(channelId); |
||||
bool found = false; |
||||
for (auto& b : local) { |
||||
if (b.day == day && b.hour == hour && b.count == rCount && b.hash == rHash) { |
||||
found = true; break; |
||||
} |
||||
} |
||||
if (found) continue; |
||||
|
||||
/* mismatch — request signatures */ |
||||
QByteArray cid = channelId.toUtf8(); |
||||
QByteArray req(static_cast<int>(7 + cid.size()), Qt::Uninitialized); |
||||
uint8_t* rp = reinterpret_cast<uint8_t*>(req.data()); |
||||
*rp++ = ChatProp::kHashesReq; |
||||
*rp++ = static_cast<uint8_t>(cid.size()); |
||||
memcpy(rp, cid.constData(), cid.size()); rp += cid.size(); |
||||
write32(rp, static_cast<uint32_t>(day)); rp += 4; |
||||
*rp = static_cast<uint8_t>(hour); |
||||
|
||||
auto peers = pickRandomPeers(1, {m_myNodeId}); |
||||
if (!peers.isEmpty()) |
||||
sendGossip(peers.first(), req); |
||||
} |
||||
} |
||||
|
||||
void ChatPropagator::handleHashesRsp(const QByteArray& payload) { |
||||
if (payload.size() < 3) return; |
||||
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData()); |
||||
if (*p != ChatProp::kHashesRsp) return; p++; |
||||
uint16_t sigCount = read16(p); p += 2; |
||||
|
||||
QList<QByteArray> remoteSigs; |
||||
for (int i = 0; i < sigCount; i++) { |
||||
if (p + 64 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) break; |
||||
remoteSigs.append(QByteArray(reinterpret_cast<const char*>(p), 64)); |
||||
p += 64; |
||||
} |
||||
if (remoteSigs.isEmpty()) return; |
||||
|
||||
/* find missing */ |
||||
QList<QByteArray> missing; |
||||
QList<QByteArray> remote = remoteSigs; |
||||
for (auto& s : remote) { |
||||
/* TODO: efficient lookup — query DB for this sig */ |
||||
auto msgs = m_db->getMessagesBySigs({s}); |
||||
if (msgs.isEmpty()) missing.append(s); |
||||
} |
||||
if (missing.isEmpty()) return; |
||||
|
||||
/* request missing messages */ |
||||
int mCount = missing.size(); |
||||
QByteArray req(static_cast<int>(3 + mCount * 64), Qt::Uninitialized); |
||||
uint8_t* rp = reinterpret_cast<uint8_t*>(req.data()); |
||||
*rp++ = ChatProp::kMsgsReq; |
||||
write16(rp, static_cast<uint16_t>(mCount)); rp += 2; |
||||
for (auto& s : missing) { memcpy(rp, s.constData(), 64); rp += 64; } |
||||
|
||||
auto peers = pickRandomPeers(1, {m_myNodeId}); |
||||
if (!peers.isEmpty()) |
||||
sendGossip(peers.first(), req); |
||||
} |
||||
|
||||
void ChatPropagator::handleMsgsRsp(const QByteArray& payload) { |
||||
if (payload.size() < 4) return; |
||||
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData()); |
||||
const uint8_t* end = p + payload.size(); |
||||
if (*p != ChatProp::kMsgsRsp) return; p++; |
||||
uint8_t cidLen = *p++; |
||||
if (p + cidLen + 2 > end) return; |
||||
QString channelId = QString::fromUtf8(reinterpret_cast<const char*>(p), cidLen); p += cidLen; |
||||
uint16_t msgCount = read16(p); p += 2; |
||||
|
||||
for (int i = 0; i < msgCount; i++) { |
||||
if (p + 64 + 8 + 8 + 2 > end) break; |
||||
QByteArray sig(reinterpret_cast<const char*>(p), 64); p += 64; |
||||
quint64 author = read64(p); p += 8; |
||||
qint64 ts = static_cast<qint64>(read64(p)); p += 8; |
||||
uint16_t cLen = read16(p); p += 2; |
||||
if (p + cLen > end) break; |
||||
QByteArray content(reinterpret_cast<const char*>(p), cLen); p += cLen; |
||||
|
||||
auto existing = m_db->getMessagesBySigs({sig}); |
||||
if (!existing.isEmpty()) continue; |
||||
|
||||
m_db->insertMessage(channelId, author, content, ts, 0); |
||||
emit messageReceived(channelId, author, content, ts); |
||||
} |
||||
} |
||||
|
||||
/* ======================================================================== */ |
||||
|
||||
bool ChatPropagator::parseNewMsg(const QByteArray& payload, QString& channelId, |
||||
QByteArray& content, qint64& timestamp, |
||||
QByteArray& sig) { |
||||
if (payload.size() < 14) return false; /* minimum: 1+1+1+8+2+1 */ |
||||
|
||||
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData()); |
||||
if (*p != ChatProp::kNewMsg) return false; |
||||
p++; |
||||
|
||||
uint8_t cidLen = *p++; |
||||
if (p + cidLen + 8 + 2 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) |
||||
return false; |
||||
channelId = QString::fromUtf8(reinterpret_cast<const char*>(p), cidLen); p += cidLen; |
||||
|
||||
timestamp = static_cast<qint64>(read64(p)); p += 8; |
||||
|
||||
uint16_t contentLen = read16(p); p += 2; |
||||
if (p + contentLen + ChatProp::kSignSize > |
||||
reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) |
||||
return false; |
||||
|
||||
content = QByteArray(reinterpret_cast<const char*>(p), contentLen); p += contentLen; |
||||
sig = QByteArray(reinterpret_cast<const char*>(p), static_cast<int>(ChatProp::kSignSize)); |
||||
return true; |
||||
} |
||||
|
||||
QList<quint64> ChatPropagator::pickRandomPeers(int count, const QList<quint64>& exclude) { |
||||
QList<quint64> result; |
||||
if (!m_db) return result; |
||||
|
||||
auto accs = m_db->getAccounts(true); |
||||
QList<quint64> pool; |
||||
for (auto& a : accs) { |
||||
if (!exclude.contains(a.nodeId)) |
||||
pool.append(a.nodeId); |
||||
} |
||||
if (pool.isEmpty()) return result; |
||||
|
||||
std::random_device rd; |
||||
std::mt19937 g(rd()); |
||||
std::shuffle(pool.begin(), pool.end(), g); |
||||
|
||||
for (int i = 0; i < count && i < pool.size(); i++) |
||||
result.append(pool[i]); |
||||
return result; |
||||
} |
||||
@ -0,0 +1,70 @@
|
||||
#pragma once |
||||
|
||||
#include <QObject> |
||||
#include <QByteArray> |
||||
#include <QHash> |
||||
#include <QList> |
||||
#include <QTimer> |
||||
#include <cstdint> |
||||
|
||||
class MsgClient; |
||||
class DbManager; |
||||
struct MessageRow; |
||||
|
||||
class ChatPropagator : public QObject { |
||||
Q_OBJECT |
||||
public: |
||||
ChatPropagator(MsgClient* client, DbManager* db, QObject* parent = nullptr); |
||||
|
||||
void setKeys(const QByteArray& edPriv, const QByteArray& edPub, quint64 nodeId); |
||||
void start(); |
||||
void propagateNewMessage(const QString& channelId, const QByteArray& content, |
||||
qint64 timestamp, const QByteArray& signature); |
||||
|
||||
signals: |
||||
void messageReceived(const QString& channelId, quint64 authorNodeId, |
||||
const QByteArray& content, qint64 timestamp); |
||||
|
||||
private: |
||||
MsgClient* m_client; |
||||
DbManager* m_db; |
||||
QTimer* m_ackTimer; |
||||
|
||||
QByteArray m_edPriv; |
||||
QByteArray m_edPub; |
||||
quint64 m_myNodeId = 0; |
||||
|
||||
struct GossipEntry { |
||||
QString channelId; |
||||
QByteArray packet; /* full NEW_MSG packet */ |
||||
QByteArray sig; /* message signature (key) */ |
||||
QList<quint64> peers; /* target peers */ |
||||
QList<quint64> acked; /* peers that acknowledged */ |
||||
qint64 sentAt; /* when sent (ms) */ |
||||
int retries = 0; |
||||
}; |
||||
QHash<QByteArray, GossipEntry> m_pending; /* sig → entry */ |
||||
QByteArray m_mySig; /* sig we're waiting for ACK on */ |
||||
|
||||
void onReceived(quint64 peerId, const QByteArray& data); |
||||
void handleNewMsg(quint64 fromPeer, const QByteArray& payload); |
||||
void handleNewAck(const QByteArray& payload); |
||||
void onAckTimeout(); |
||||
|
||||
void startCatchUp(); |
||||
void handleBucketsRsp(const QByteArray& payload); |
||||
void handleHashesRsp(const QByteArray& payload); |
||||
void handleMsgsRsp(const QByteArray& payload); |
||||
|
||||
void fanoutGossip(const QString& channelId, const QByteArray& packet, |
||||
const QByteArray& sig, int fanout, const QList<quint64>& exclude); |
||||
void sendGossip(quint64 peer, const QByteArray& packet); |
||||
|
||||
QList<quint64> pickRandomPeers(int count, const QList<quint64>& exclude); |
||||
|
||||
QByteArray buildNewMsgPacket(const QString& channelId, const QByteArray& content, |
||||
qint64 timestamp, const QByteArray& signature); |
||||
QByteArray buildAckPacket(const QByteArray& sig); |
||||
bool parseNewMsg(const QByteArray& payload, QString& channelId, QByteArray& content, |
||||
qint64& timestamp, QByteArray& sig); |
||||
}; |
||||
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* chat_proto.h — протокол распространения сообщений чата (gossip + catch-up) |
||||
* |
||||
* Все пакеты идут через MsgClient::sendTo() / received() поверх msg_transport. |
||||
* Подписи — внутри пейлоада. utun — чистый транспорт. |
||||
*/ |
||||
|
||||
#ifndef CHAT_PROPAGATE_PROTO_H |
||||
#define CHAT_PROPAGATE_PROTO_H |
||||
|
||||
#include <cstdint> |
||||
|
||||
namespace ChatProp { |
||||
|
||||
static constexpr uint8_t kNewMsg = 0x01; /* новое сообщение */ |
||||
static constexpr uint8_t kNewAck = 0x02; /* подтверждение получения */ |
||||
static constexpr uint8_t kBucketsReq = 0x03; /* запрос bucket-ов канала */ |
||||
static constexpr uint8_t kBucketsRsp = 0x04; /* bucket-ы: день+час → count+hash */ |
||||
static constexpr uint8_t kHashesReq = 0x05; /* запрос подписей в bucket */ |
||||
static constexpr uint8_t kHashesRsp = 0x06; /* список подписей (64 байта каждая) */ |
||||
static constexpr uint8_t kMsgsReq = 0x07; /* запрос сообщений по подписям */ |
||||
static constexpr uint8_t kMsgsRsp = 0x08; /* сообщения + подписи */ |
||||
|
||||
static constexpr size_t kSignSize = 64; |
||||
static constexpr size_t kHashSize = 32; /* SHA-256 */ |
||||
static constexpr int kDefaultFanout = 4; |
||||
static constexpr int kDefaultAckTimeoutMs = 1000; |
||||
|
||||
/* Форматы пакетов (byte stream) ================================================
|
||||
|
||||
NEW_MSG: [0x01][channel_id_len:1][channel_id][timestamp:8][content_len:2][content][sig:64] |
||||
NEW_ACK: [0x02][sig_of_confirmed_msg:64] |
||||
|
||||
BUCKETS_REQ: [0x03][channel_id_len:1][channel_id] |
||||
BUCKETS_RSP: [0x04][channel_id_len:1][channel_id][bucket_count:2][buckets...] |
||||
bucket = [day_unix:4][hour:1][msg_count:4][sig_hash:32] |
||||
|
||||
HASHES_REQ: [0x05][channel_id_len:1][channel_id][day_unix:4][hour:1] |
||||
HASHES_RSP: [0x06][sig_count:2][sig:64, sig:64, ...] |
||||
|
||||
MSGS_REQ: [0x07][sig_count:2][sig:64, sig:64, ...] |
||||
MSGS_RSP: [0x08][channel_id_len:1][channel_id][msg_count:2][(sig:64, author:8, timestamp:8, content_len:2, content)...] |
||||
|
||||
=============================================================================== */ |
||||
|
||||
} // namespace ChatProp
|
||||
|
||||
#endif |
||||
@ -0,0 +1,221 @@
|
||||
/*
|
||||
* crypto.cpp — Криптографические утилиты (Ed25519 через OpenSSL) |
||||
*/ |
||||
|
||||
#include "crypto.h" |
||||
|
||||
#include <openssl/sha.h> |
||||
#include <openssl/evp.h> |
||||
#include <QFile> |
||||
#include <QCoreApplication> |
||||
#include <thread> |
||||
#include <cstring> |
||||
|
||||
namespace Crypto { |
||||
|
||||
/* ============================================================================
|
||||
* Помощники |
||||
* ============================================================================ */ |
||||
|
||||
namespace { |
||||
|
||||
void postToMain(std::function<void()> fn) { |
||||
auto* app = QCoreApplication::instance(); |
||||
if (app) { |
||||
QMetaObject::invokeMethod(app, std::move(fn), Qt::QueuedConnection); |
||||
} else { |
||||
fn(); /* нет event loop — синхронно */ |
||||
} |
||||
} |
||||
|
||||
QByteArray sha512File(const QString& path) { |
||||
QFile file(path); |
||||
if (!file.open(QIODevice::ReadOnly)) return {}; |
||||
|
||||
EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); |
||||
if (!md_ctx || EVP_DigestInit_ex(md_ctx, EVP_sha512(), NULL) != 1) { |
||||
if (md_ctx) EVP_MD_CTX_free(md_ctx); |
||||
return {}; |
||||
} |
||||
|
||||
QByteArray buf(65536, 0); |
||||
while (true) { |
||||
qint64 n = file.read(buf.data(), buf.size()); |
||||
if (n <= 0) break; |
||||
EVP_DigestUpdate(md_ctx, reinterpret_cast<const uint8_t*>(buf.constData()), |
||||
static_cast<size_t>(n)); |
||||
} |
||||
file.close(); |
||||
|
||||
QByteArray hash; |
||||
hash.resize(static_cast<int>(kSha512Size)); |
||||
unsigned int hash_len = static_cast<unsigned int>(kSha512Size); |
||||
if (EVP_DigestFinal_ex(md_ctx, reinterpret_cast<uint8_t*>(hash.data()), &hash_len) != 1) |
||||
hash.clear(); |
||||
EVP_MD_CTX_free(md_ctx); |
||||
return hash; |
||||
} |
||||
|
||||
} // anonymous
|
||||
|
||||
/* ============================================================================
|
||||
* Синхронные: derive ключей |
||||
* ============================================================================ */ |
||||
|
||||
QByteArray deriveEd25519Privkey(const QByteArray& x25519Privkey) { |
||||
if (x25519Privkey.size() != static_cast<int>(kKeySize)) return QByteArray(); |
||||
|
||||
uint8_t hash[kSha512Size]; |
||||
SHA512(reinterpret_cast<const uint8_t*>(x25519Privkey.constData()), kKeySize, hash); |
||||
|
||||
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, hash, kKeySize); |
||||
if (!pkey) return QByteArray(); |
||||
|
||||
QByteArray result; |
||||
result.resize(static_cast<int>(kKeySize)); |
||||
size_t priv_len = kKeySize; |
||||
if (EVP_PKEY_get_raw_private_key(pkey, reinterpret_cast<uint8_t*>(result.data()), &priv_len) <= 0 |
||||
|| priv_len != kKeySize) { |
||||
EVP_PKEY_free(pkey); |
||||
return QByteArray(); |
||||
} |
||||
EVP_PKEY_free(pkey); |
||||
return result; |
||||
} |
||||
|
||||
QByteArray deriveEd25519Pubkey(const QByteArray& x25519Privkey) { |
||||
if (x25519Privkey.size() != static_cast<int>(kKeySize)) return QByteArray(); |
||||
|
||||
uint8_t hash[kSha512Size]; |
||||
SHA512(reinterpret_cast<const uint8_t*>(x25519Privkey.constData()), kKeySize, hash); |
||||
|
||||
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, hash, kKeySize); |
||||
if (!pkey) return QByteArray(); |
||||
|
||||
QByteArray result; |
||||
result.resize(static_cast<int>(kKeySize)); |
||||
size_t pub_len = kKeySize; |
||||
if (EVP_PKEY_get_raw_public_key(pkey, reinterpret_cast<uint8_t*>(result.data()), &pub_len) <= 0 |
||||
|| pub_len != kKeySize) { |
||||
EVP_PKEY_free(pkey); |
||||
return QByteArray(); |
||||
} |
||||
EVP_PKEY_free(pkey); |
||||
return result; |
||||
} |
||||
|
||||
/* ============================================================================
|
||||
* Синхронные: подпись / проверка (малые блоки) |
||||
* ============================================================================ */ |
||||
|
||||
QByteArray sign(const QByteArray& ed25519Privkey, const QByteArray& data) { |
||||
if (ed25519Privkey.size() != static_cast<int>(kKeySize)) return QByteArray(); |
||||
|
||||
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, |
||||
reinterpret_cast<const uint8_t*>(ed25519Privkey.constData()), kKeySize); |
||||
if (!pkey) return QByteArray(); |
||||
|
||||
EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); |
||||
if (!md_ctx || EVP_DigestSignInit(md_ctx, NULL, NULL, NULL, pkey) != 1) { |
||||
if (md_ctx) EVP_MD_CTX_free(md_ctx); |
||||
EVP_PKEY_free(pkey); |
||||
return QByteArray(); |
||||
} |
||||
|
||||
size_t sig_len = kSignSize; |
||||
QByteArray result; |
||||
result.resize(static_cast<int>(kSignSize)); |
||||
if (EVP_DigestSign(md_ctx, reinterpret_cast<uint8_t*>(result.data()), &sig_len, |
||||
reinterpret_cast<const uint8_t*>(data.constData()), data.size()) != 1) { |
||||
EVP_MD_CTX_free(md_ctx); |
||||
EVP_PKEY_free(pkey); |
||||
return QByteArray(); |
||||
} |
||||
|
||||
EVP_MD_CTX_free(md_ctx); |
||||
EVP_PKEY_free(pkey); |
||||
result.resize(static_cast<int>(sig_len)); |
||||
return result; |
||||
} |
||||
|
||||
bool verify(const QByteArray& ed25519Pubkey, const QByteArray& data, const QByteArray& sig) { |
||||
if (ed25519Pubkey.size() != static_cast<int>(kKeySize) |
||||
|| sig.size() > static_cast<int>(kSignSize)) return false; |
||||
|
||||
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, |
||||
reinterpret_cast<const uint8_t*>(ed25519Pubkey.constData()), kKeySize); |
||||
if (!pkey) return false; |
||||
|
||||
EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); |
||||
if (!md_ctx || EVP_DigestVerifyInit(md_ctx, NULL, NULL, NULL, pkey) != 1) { |
||||
if (md_ctx) EVP_MD_CTX_free(md_ctx); |
||||
EVP_PKEY_free(pkey); |
||||
return false; |
||||
} |
||||
|
||||
int ret = EVP_DigestVerify(md_ctx, reinterpret_cast<const uint8_t*>(sig.constData()), sig.size(), |
||||
reinterpret_cast<const uint8_t*>(data.constData()), data.size()); |
||||
EVP_MD_CTX_free(md_ctx); |
||||
EVP_PKEY_free(pkey); |
||||
return ret == 1; |
||||
} |
||||
|
||||
/* ============================================================================
|
||||
* Асинхронные: файлы |
||||
* ============================================================================ */ |
||||
|
||||
void signFile(const QString& path, const QByteArray& ed25519Privkey, SignCallback cb) { |
||||
std::thread([=]() { |
||||
SignResult r; |
||||
QByteArray hash = sha512File(path); |
||||
if (hash.isEmpty()) { |
||||
r.ok = false; r.error = QStringLiteral("failed to read/hash file"); |
||||
} else { |
||||
r.signature = sign(ed25519Privkey, hash); |
||||
r.ok = !r.signature.isEmpty(); |
||||
if (!r.ok) r.error = QStringLiteral("sign failed"); |
||||
} |
||||
postToMain([cb, r]() { cb(r); }); |
||||
}).detach(); |
||||
} |
||||
|
||||
void verifyFile(const QString& path, const QByteArray& ed25519Pubkey, |
||||
const QByteArray& signature, SignCallback cb) { |
||||
std::thread([=]() { |
||||
SignResult r; |
||||
QByteArray hash = sha512File(path); |
||||
if (hash.isEmpty()) { |
||||
r.ok = false; r.error = QStringLiteral("failed to read/hash file"); |
||||
} else { |
||||
r.ok = verify(ed25519Pubkey, hash, signature); |
||||
if (!r.ok) r.error = QStringLiteral("signature mismatch"); |
||||
} |
||||
postToMain([cb, r]() { cb(r); }); |
||||
}).detach(); |
||||
} |
||||
|
||||
/* ============================================================================
|
||||
* Асинхронные: in-memory блоки |
||||
* ============================================================================ */ |
||||
|
||||
void signAsync(const QByteArray& ed25519Privkey, const QByteArray& data, SignCallback cb) { |
||||
std::thread([=]() { |
||||
SignResult r; |
||||
r.signature = sign(ed25519Privkey, data); |
||||
r.ok = !r.signature.isEmpty(); |
||||
if (!r.ok) r.error = QStringLiteral("sign failed"); |
||||
postToMain([cb, r]() { cb(r); }); |
||||
}).detach(); |
||||
} |
||||
|
||||
void verifyAsync(const QByteArray& ed25519Pubkey, const QByteArray& data, |
||||
const QByteArray& signature, SignCallback cb) { |
||||
std::thread([=]() { |
||||
SignResult r; |
||||
r.ok = verify(ed25519Pubkey, data, signature); |
||||
if (!r.ok) r.error = QStringLiteral("signature mismatch"); |
||||
postToMain([cb, r]() { cb(r); }); |
||||
}).detach(); |
||||
} |
||||
|
||||
} // namespace Crypto
|
||||
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* crypto.h — Криптографические утилиты для chat GUI |
||||
* |
||||
* Ключи: X25519 → SHA-512 → Ed25519 (аналог sc_derive_ed25519_key / pubkey в utun) |
||||
* Подпись: Ed25519 (EVP_DigestSign / EVP_DigestVerify) |
||||
*/ |
||||
|
||||
#ifndef MSG_CRYPTO_H |
||||
#define MSG_CRYPTO_H |
||||
|
||||
#include <QByteArray> |
||||
#include <QString> |
||||
#include <functional> |
||||
|
||||
namespace Crypto { |
||||
|
||||
static constexpr size_t kKeySize = 32; // X25519 / Ed25519 key size
|
||||
static constexpr size_t kSignSize = 64; // Ed25519 signature size
|
||||
static constexpr size_t kSha512Size = 64; // SHA-512 digest size
|
||||
|
||||
// ---------------- синхронные (малые блоки) ----------------
|
||||
|
||||
QByteArray deriveEd25519Privkey(const QByteArray& x25519Privkey); |
||||
QByteArray deriveEd25519Pubkey(const QByteArray& x25519Privkey); |
||||
|
||||
QByteArray sign(const QByteArray& ed25519Privkey, const QByteArray& data); |
||||
bool verify(const QByteArray& ed25519Pubkey, const QByteArray& data, const QByteArray& sig); |
||||
|
||||
// ---------------- асинхронные (файлы / большие блоки) ----------------
|
||||
|
||||
struct SignResult { |
||||
bool ok = false; |
||||
QByteArray signature; // 64 bytes
|
||||
QString error; |
||||
}; |
||||
|
||||
using SignCallback = std::function<void(const SignResult&)>; |
||||
|
||||
void signFile(const QString& path, const QByteArray& ed25519Privkey, SignCallback cb); |
||||
void verifyFile(const QString& path, const QByteArray& ed25519Pubkey, |
||||
const QByteArray& signature, SignCallback cb); |
||||
void signAsync(const QByteArray& ed25519Privkey, const QByteArray& data, SignCallback cb); |
||||
void verifyAsync(const QByteArray& ed25519Pubkey, const QByteArray& data, |
||||
const QByteArray& signature, SignCallback cb); |
||||
|
||||
} // namespace Crypto
|
||||
|
||||
#endif // MSG_CRYPTO_H
|
||||
@ -0,0 +1,43 @@
|
||||
--- |
||||
Language: Cpp |
||||
Standard: c++17 |
||||
BasedOnStyle: LLVM |
||||
|
||||
IndentWidth: 4 |
||||
TabWidth: 4 |
||||
UseTab: ForContinuationAndIndentation # ForIndentation |
||||
|
||||
AccessModifierOffset: -4 |
||||
ColumnLimit: 135 |
||||
|
||||
#AlignConsecutiveAssignments: true |
||||
AlignConsecutiveBitFields: true |
||||
AlignEscapedNewlines: DontAlign |
||||
AlignTrailingComments: true |
||||
|
||||
AllowShortCaseLabelsOnASingleLine: true |
||||
AllowShortFunctionsOnASingleLine: Inline |
||||
#AllowShortLambdasOnASingleLine: Inline |
||||
AllowShortEnumsOnASingleLine: true |
||||
AllowAllArgumentsOnNextLine: true |
||||
AllowAllParametersOfDeclarationOnNextLine: true |
||||
|
||||
AlwaysBreakAfterDefinitionReturnType: None |
||||
AlwaysBreakTemplateDeclarations: Yes |
||||
BreakBeforeBraces: Custom |
||||
BraceWrapping: |
||||
AfterClass: true |
||||
AfterEnum: true |
||||
AfterStruct: true |
||||
AfterUnion: true |
||||
AfterFunction: true |
||||
SplitEmptyFunction: false |
||||
BreakBeforeBinaryOperators: NonAssignment |
||||
BreakBeforeTernaryOperators: true |
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true |
||||
FixNamespaceComments: true |
||||
IncludeBlocks: Regroup |
||||
KeepEmptyLinesAtTheStartOfBlocks: false |
||||
PointerAlignment: Left |
||||
ReflowComments: true |
||||
SortIncludes: true |
||||
@ -0,0 +1,58 @@
|
||||
--- |
||||
Checks: > |
||||
-*, |
||||
bugprone-*, |
||||
-bugprone-easily-swappable-parameters, |
||||
-bugprone-exception-escape, |
||||
-bugprone-implicit-widening-of-multiplication-result, |
||||
-bugprone-integer-division, |
||||
-bugprone-macro-parentheses, |
||||
-bugprone-narrowing-conversions, |
||||
-bugprone-signed-char-misuse, |
||||
-bugprone-use-after-move, |
||||
-bugprone-assignment-in-if-condition, |
||||
performance-*, |
||||
-performance-enum-size, |
||||
-performance-move-const-arg, |
||||
portability-*, |
||||
-portability-avoid-pragma-once, |
||||
readability-avoid-const-params-in-decls, |
||||
readability-const-return-type, |
||||
readability-delete-null-pointer, |
||||
readability-deleted-default, |
||||
readability-inconsistent-declaration-parameter-name, |
||||
readability-misleading-indentation, |
||||
readability-misplaced-array-index, |
||||
readability-non-const-parameter, |
||||
readability-redundant-*, |
||||
readability-simplify-*, |
||||
-readability-simplify-boolean-expr, |
||||
readability-static-*, |
||||
-readability-static-definition-in-anonymous-namespace, |
||||
readability-string-compare, |
||||
readability-suspicious-call-argument, |
||||
readability-uniqueptr-delete-release, |
||||
readability-use-anyofallof, |
||||
-readability-redundant-inline-specifier, |
||||
-readability-redundant-member-init, |
||||
modernize-deprecated-*, |
||||
modernize-loop-convert, |
||||
modernize-make-*, |
||||
modernize-pass-by-value, |
||||
modernize-raw-string-literal, |
||||
modernize-redundant-void-arg, |
||||
modernize-replace-*, |
||||
modernize-return-braced-init-list, |
||||
modernize-shrink-to-fit, |
||||
modernize-unary-static-assert, |
||||
modernize-use-*, |
||||
-modernize-use-constraints, |
||||
-modernize-use-designated-initializers, |
||||
-modernize-use-nodiscard, |
||||
-modernize-use-ranges, |
||||
-modernize-use-trailing-return-type, |
||||
-modernize-use-integer-sign-comparison, |
||||
|
||||
WarningsAsErrors: '' |
||||
HeaderFilterRegex: '^(?!.*zint\.h).*$' |
||||
FormatStyle: file |
||||
@ -0,0 +1,19 @@
|
||||
root = true |
||||
|
||||
[*] |
||||
insert_final_newline = true |
||||
trim_trailing_whitespace = true |
||||
charset = utf-8 |
||||
|
||||
[*.{cpp,c,h,html,py,kt,cs,qlm}] |
||||
indent_style = tab |
||||
indent_size = 4 |
||||
max_line_length = 135 |
||||
|
||||
[CMakeLists.txt] |
||||
indent_style = space |
||||
indent_size = 4 |
||||
|
||||
[*.md] |
||||
trim_trailing_whitespace = false |
||||
|
||||
@ -0,0 +1,3 @@
|
||||
test/samples export-ignore |
||||
zint export-ignore |
||||
.git* export-ignore |
||||
@ -0,0 +1,2 @@
|
||||
github: axxel |
||||
custom: 'www.paypal.com/donate/?hosted_button_id=8LDF4NNQF8Z3L' |
||||
@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3 |
||||
|
||||
import subprocess |
||||
import tempfile |
||||
import shutil |
||||
import sys |
||||
import re |
||||
from pathlib import Path |
||||
|
||||
|
||||
def run_command(cmd, cwd=None): |
||||
try: |
||||
return subprocess.run(cmd, check=True, capture_output=True, text=True, cwd=cwd) |
||||
except subprocess.CalledProcessError as e: |
||||
print(f"✗ Error {cmd}: {e.stderr}") |
||||
sys.exit(1) |
||||
|
||||
|
||||
def main(repo_root=None, output_dir=None): |
||||
repo_root = Path.cwd() if repo_root is None else Path(repo_root).resolve() |
||||
output_dir = repo_root if output_dir is None else Path(output_dir).resolve() |
||||
output_dir.mkdir(parents=True, exist_ok=True) |
||||
print(f"Working in repository: {repo_root}") |
||||
|
||||
# Extract version and optional suffix from core/CMakeLists.txt |
||||
cmake_txt = (repo_root / "core" / "CMakeLists.txt").read_text() |
||||
version = re.search(r'ZXing VERSION "?([0-9]+\.[0-9]+\.[0-9]+)"?', cmake_txt).group(1) |
||||
suffix = re.search(r'ZXING_VERSION_SUFFIX "?([^\)\s\"]*)"?', cmake_txt).group(1) |
||||
ver_tag = f"{version}{suffix}" |
||||
print(f"Project version from CMake: {ver_tag}") |
||||
|
||||
# Create output filenames with git tag |
||||
output_stem = f"zxing-cpp-{ver_tag}" |
||||
output_tgz = output_dir / f"{output_stem}.tar.gz" |
||||
output_zip = output_dir / f"{output_stem}.zip" |
||||
|
||||
# Step 1: Create initial tar ball via git archive |
||||
print("\n[1/4] Creating git archive...") |
||||
run_command(["git", "archive", f"--prefix={output_stem}/", f"--output={output_tgz}", "HEAD", repo_root]) |
||||
print(f"✓ Created git archive: {output_tgz}") |
||||
|
||||
# Step 2: Extract tar ball into temporary folder |
||||
print("\n[2/4] Extracting archive to temporary folder...") |
||||
with tempfile.TemporaryDirectory() as temp_dir: |
||||
print(f"✓ Created temporary directory: {temp_dir}") |
||||
|
||||
run_command(["tar", "-xf", str(output_tgz), "-C", temp_dir]) |
||||
print(f"✓ Extracted archive to temporary folder") |
||||
|
||||
# Step 3: Replace files in core/src/libzint/ with files from zint/backend/ |
||||
print("\n[3/4] Replacing files in core/src/libzint/ with zint/backend/ files...") |
||||
dst_dir = Path(temp_dir) / output_stem / "core" / "src" / "libzint" |
||||
src_dir = repo_root / "zint" / "backend" # Use actual repo directory |
||||
|
||||
# Find all files/symlinks currently in core/src/libzint/ (including subdirectories) |
||||
libzint_files = list(dst_dir.rglob("*")) |
||||
# Include both regular files and symlinks |
||||
libzint_files = [f for f in libzint_files if f.is_file() or f.is_symlink()] |
||||
|
||||
replaced_count = 0 |
||||
not_found_count = 0 |
||||
|
||||
for libzint_file in libzint_files: |
||||
# Get relative path from core/src/libzint/ |
||||
rel_path = libzint_file.relative_to(dst_dir) |
||||
|
||||
# Source path in zint/backend/ |
||||
source_file = src_dir / rel_path |
||||
|
||||
# Only replace if the file exists in zint/backend/ |
||||
if source_file.exists() and source_file.is_file(): |
||||
# Remove symlink if it exists |
||||
if libzint_file.is_symlink(): |
||||
libzint_file.unlink() |
||||
# Copy the actual file |
||||
shutil.copy2(source_file, libzint_file) |
||||
replaced_count += 1 |
||||
else: |
||||
not_found_count += 1 |
||||
print(f" - Skipped (not in zint/backend): {rel_path}") |
||||
|
||||
print(f"✓ Replaced {replaced_count} files, skipped {not_found_count} files") |
||||
|
||||
# Step 4: Create tar.gz and zip archives |
||||
print("\n[4/4] Creating archives (tar.gz and zip)...") |
||||
run_command(["tar", "-czf", str(output_tgz), "-C", temp_dir, output_stem]) |
||||
|
||||
if (output_zip.exists()): |
||||
output_zip.unlink() |
||||
|
||||
run_command(["zip", "-q", "-r", str(output_zip), output_stem], cwd=temp_dir) |
||||
|
||||
# Cleanup initial tar file |
||||
print(f"\n✅ Done! Source archives created:") |
||||
print(f" - {output_tgz}") |
||||
print(f" - {output_zip}") |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
repo_root = sys.argv[1] if len(sys.argv) > 1 else None |
||||
output_dir = sys.argv[2] if len(sys.argv) > 2 else None |
||||
main(repo_root=repo_root, output_dir=output_dir) |
||||
@ -0,0 +1,435 @@
|
||||
name: CI |
||||
|
||||
# Controls when the action will run. Triggers the workflow on push or pull request |
||||
# events but only for the master branch |
||||
on: |
||||
push: |
||||
branches: [ master ] |
||||
pull_request: |
||||
branches: [ master ] |
||||
|
||||
env: |
||||
# Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) |
||||
BUILD_TYPE: Release |
||||
|
||||
jobs: |
||||
build: |
||||
runs-on: ${{ matrix.os }} |
||||
strategy: |
||||
matrix: |
||||
os: [windows-latest, ubuntu-latest, macos-latest] |
||||
# don't cancel all jobs just because one of them failed |
||||
fail-fast: false |
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Setup Python 3 |
||||
uses: actions/setup-python@v6 |
||||
with: |
||||
python-version: '3.x' |
||||
|
||||
- name: Configure |
||||
run: > |
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_SHARED_LIBS=ON |
||||
-DZXING_BLACKBOX_TESTS=ON -DZXING_TEST_DOTNET=ON -DZXING_TEST_GO=ON -DZXING_TEST_INSTALL=ON -DZXING_UNIT_TESTS=ON |
||||
-DZXING_PYTHON_MODULE=ON |
||||
|
||||
- name: Build |
||||
run: cmake --build build --parallel --config ${{env.BUILD_TYPE}} |
||||
|
||||
- name: Set PATH for Tests |
||||
shell: bash # to make the $GITHUB_PATH update work |
||||
if: runner.os == 'Windows' |
||||
run: | |
||||
echo "${GITHUB_WORKSPACE}/build/core/${BUILD_TYPE}" >> $GITHUB_PATH |
||||
echo "${GITHUB_WORKSPACE}/build/bin/${BUILD_TYPE}" >> $GITHUB_PATH |
||||
|
||||
- name: Test |
||||
run: ctest --test-dir build -V -C ${{env.BUILD_TYPE}} |
||||
|
||||
- name: Install |
||||
run: | |
||||
cmake -E make_directory install |
||||
cmake --install build --config ${{env.BUILD_TYPE}} --prefix ${{github.workspace}}/install |
||||
|
||||
- uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: ${{matrix.os}}-artifacts |
||||
path: install |
||||
|
||||
build-exp: |
||||
runs-on: ${{ matrix.os }} |
||||
strategy: |
||||
matrix: |
||||
os: [windows-latest, ubuntu-latest, macos-latest, ubuntu-24.04-arm, macos-15-intel, windows-11-arm] |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Setup Python 3 |
||||
id: setup-python |
||||
if: matrix.os == 'windows-11-arm' # the default python on the arm runner is an x64 build |
||||
uses: actions/setup-python@v6 |
||||
with: |
||||
python-version: '3.13' |
||||
architecture: arm64 |
||||
|
||||
- name: Configure |
||||
run: > |
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_SHARED_LIBS=ON |
||||
-DZXING_READERS=ON -DZXING_WRITERS=BOTH -DZXING_EXPERIMENTAL_API=ON |
||||
-DZXING_BLACKBOX_TESTS=ON -DZXING_TEST_INSTALL=ON -DZXING_UNIT_TESTS=OFF -DZXING_PYTHON_MODULE=ON |
||||
${{ steps.setup-python.outputs.python-path != '' && format('-DPython_EXECUTABLE={0}', steps.setup-python.outputs.python-path) || '' }} |
||||
|
||||
- name: Build |
||||
run: cmake --build build --parallel --config ${{env.BUILD_TYPE}} |
||||
|
||||
- name: Set PATH for Tests |
||||
shell: bash # to make the $GITHUB_PATH update work |
||||
if: runner.os == 'Windows' |
||||
run: | |
||||
echo "${GITHUB_WORKSPACE}/build/core/${BUILD_TYPE}" >> $GITHUB_PATH |
||||
echo "${GITHUB_WORKSPACE}/build/bin/${BUILD_TYPE}" >> $GITHUB_PATH |
||||
|
||||
- name: Test |
||||
run: ctest --test-dir build -V -C ${{env.BUILD_TYPE}} |
||||
|
||||
- name: Install |
||||
run: | |
||||
cmake -E make_directory install |
||||
cmake --install build --config ${{env.BUILD_TYPE}} --prefix ${{github.workspace}}/install |
||||
|
||||
- uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: ${{matrix.os}}-exp-artifacts |
||||
path: install |
||||
|
||||
build-ubuntu-gcc-12: # keep homebrew happy |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Configure |
||||
run: > |
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo |
||||
-DZXING_WRITERS=BOTH |
||||
-DCMAKE_CXX_COMPILER=g++-12 -DCMAKE_C_COMPILER=gcc-12 |
||||
|
||||
- name: Build |
||||
run: cmake --build build --parallel |
||||
|
||||
- name: Test |
||||
run: ctest -V --test-dir build |
||||
|
||||
build-ubuntu-asan-ubsan: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Configure |
||||
run: > |
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DZXING_WRITERS=BOTH |
||||
-DZXING_BLACKBOX_TESTS=ON -DZXING_UNIT_TESTS=ON -DZXING_PYTHON_MODULE=OFF -DZXING_C_API=OFF |
||||
-DCMAKE_CXX_COMPILER=clang++ |
||||
-DCMAKE_CXX_FLAGS="-march=native -fsanitize=address,undefined -fno-optimize-sibling-calls -fsanitize-address-use-after-scope -fno-omit-frame-pointer" |
||||
-DCMAKE_C_COMPILER=clang |
||||
-DCMAKE_C_FLAGS="-march=native -fsanitize=address,undefined -fno-optimize-sibling-calls -fsanitize-address-use-after-scope -fno-omit-frame-pointer" |
||||
|
||||
- name: Build |
||||
run: cmake --build build --parallel |
||||
|
||||
- name: Test |
||||
run: ctest -V --test-dir build |
||||
|
||||
build-ubuntu-tsan: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Configure |
||||
run: > |
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DZXING_WRITERS=OFF -DZXING_EXAMPLES=OFF |
||||
-DZXING_BLACKBOX_TESTS=ON -DZXING_UNIT_TESTS=OFF -DZXING_PYTHON_MODULE=OFF -DZXING_C_API=OFF |
||||
-DCMAKE_CXX_COMPILER=clang++ |
||||
-DCMAKE_CXX_FLAGS="-march=native -fsanitize=thread -fno-optimize-sibling-calls -fno-omit-frame-pointer -g" |
||||
-DCMAKE_C_COMPILER=clang |
||||
-DCMAKE_C_FLAGS="-march=native -fsanitize=thread -fno-optimize-sibling-calls -fno-omit-frame-pointer -g" |
||||
|
||||
- name: Build |
||||
run: cmake --build build --parallel |
||||
|
||||
- name: Test |
||||
run: ctest -V --test-dir build |
||||
|
||||
build-old-writers: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Configure |
||||
run: > |
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo |
||||
-DZXING_READERS=ON -DZXING_WRITERS=OLD -DZXING_PYTHON_MODULE=ON -DZXING_C_API=ON |
||||
-DZXING_BLACKBOX_TESTS=ON -DZXING_TEST_INSTALL=ON -DZXING_UNIT_TESTS=ON -DZXING_PYTHON_MODULE=ON |
||||
|
||||
- name: Build |
||||
run: cmake --build build --parallel |
||||
|
||||
- name: Test |
||||
run: ctest -V --test-dir build |
||||
|
||||
build-ios: |
||||
runs-on: macos-latest |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Build the swift package |
||||
run: swift build |
||||
|
||||
- name: Build the demo app |
||||
working-directory: wrappers/ios/demo |
||||
run: xcodebuild build -scheme demo -sdk "iphonesimulator" |
||||
|
||||
# - name: Validate the Pod |
||||
# run: pod lib lint --allow-warnings |
||||
|
||||
build-android: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- uses: actions/setup-java@v5 |
||||
with: |
||||
distribution: 'temurin' |
||||
java-version: '17' |
||||
|
||||
- name: Build the lib/app |
||||
working-directory: wrappers/android |
||||
run: ./gradlew assembleRelease |
||||
|
||||
- uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: android-artifacts |
||||
path: "wrappers/android/zxingcpp/build/outputs/aar/zxingcpp-release.aar" |
||||
|
||||
- name: Publish Library Snapshot |
||||
if: github.repository == 'zxing-cpp/zxing-cpp' && github.event_name != 'pull_request' |
||||
working-directory: wrappers/android |
||||
env: |
||||
ORG_GRADLE_PROJECT_publishSnapshot: true |
||||
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }} |
||||
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} |
||||
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }} |
||||
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }} |
||||
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_KEY_PASSWORD }} |
||||
run: ./gradlew publishToMavenCentral --no-configuration-cache |
||||
|
||||
build-kn: |
||||
runs-on: ubuntu-latest |
||||
defaults: |
||||
run: |
||||
working-directory: wrappers/kn |
||||
|
||||
steps: |
||||
- name: Checkout repository |
||||
uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Prepare Zig toolchain |
||||
uses: mlugg/setup-zig@v2 |
||||
with: |
||||
version: 0.13.0 |
||||
|
||||
- name: Checkout toolchain initializer repository |
||||
uses: actions/checkout@v6 |
||||
with: |
||||
repository: ISNing/kn-toolchain-initializer |
||||
path: wrappers/kn/.kn-toolchain-initializer |
||||
|
||||
- name: Set up JDK |
||||
uses: actions/setup-java@v5 |
||||
with: |
||||
java-version: 17 |
||||
distribution: temurin |
||||
|
||||
- name: Setup Gradle |
||||
uses: gradle/actions/setup-gradle@v5 |
||||
|
||||
- name: Validate Gradle Wrapper |
||||
uses: gradle/actions/wrapper-validation@v5 |
||||
|
||||
- name: Initialize Kotlin/Native toolchain |
||||
working-directory: wrappers/kn/.kn-toolchain-initializer |
||||
run: ./gradlew build -DkotlinVersion=2.2.0 |
||||
|
||||
- name: Run test for linuxX64 target |
||||
run: | |
||||
echo -e "konan.dir=$HOME/.konan/kotlin-native-prebuilt-linux-x86_64-2.2.0" >> local.properties |
||||
./gradlew linuxX64Test |
||||
|
||||
build-python: |
||||
runs-on: ${{ matrix.os }} |
||||
strategy: |
||||
matrix: |
||||
python-version: ['3.12'] |
||||
os: [ubuntu-latest, macos-latest, windows-latest] |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Set up Python ${{ matrix.python-version }} |
||||
uses: actions/setup-python@v6 |
||||
with: |
||||
python-version: ${{ matrix.python-version }} |
||||
|
||||
- name: Install dependencies |
||||
run: | |
||||
python -m pip install --upgrade pip |
||||
python -m pip install numpy pillow build |
||||
|
||||
- name: Build module |
||||
working-directory: wrappers/python |
||||
run: python -m pip -v install -e . |
||||
|
||||
- name: Test module |
||||
working-directory: wrappers/python |
||||
run: python -m unittest -v |
||||
|
||||
- name: Build wheel |
||||
working-directory: wrappers/python |
||||
run: python -m build --wheel |
||||
|
||||
- uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: ${{matrix.os}}-python-artifacts |
||||
path: wrappers/python/dist/* |
||||
|
||||
build-rust: |
||||
runs-on: ubuntu-latest |
||||
defaults: |
||||
run: |
||||
working-directory: wrappers/rust |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Lint |
||||
run: | |
||||
cargo fmt --check |
||||
cargo clippy -- -Dwarnings |
||||
|
||||
- name: Build |
||||
run: cargo build --release --verbose --all-features --examples |
||||
|
||||
- name: Test |
||||
run: cargo test --release --all-features |
||||
|
||||
- name: Package |
||||
# --allow-dirty is required on the windows build (but not the ubuntu build?!) |
||||
run: | |
||||
cargo package --all-features |
||||
cargo publish --dry-run |
||||
|
||||
- uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: rust-artifacts |
||||
path: wrappers/rust/target/package/*.crate |
||||
|
||||
build-wasm: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- uses: emscripten-core/setup-emsdk@v15 |
||||
|
||||
- name: Configure |
||||
run: emcmake cmake -Swrappers/wasm -Bbuild |
||||
|
||||
- name: Build |
||||
run: cmake --build build --parallel |
||||
|
||||
# - name: Test |
||||
# run: node build/EmGlueTests.js |
||||
|
||||
- uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: wasm-artifacts |
||||
path: | |
||||
build/zxing* |
||||
build/demo* |
||||
|
||||
# disable WinRT build for now, as it does not compile with C++20 (see wrappers/winrt/CMakeLists.txt) |
||||
# build-winrt: |
||||
# runs-on: windows-latest |
||||
|
||||
# steps: |
||||
# - uses: actions/checkout@v6 |
||||
# with: |
||||
# submodules: true |
||||
|
||||
# - name: Configure |
||||
# shell: cmd # powershell messes up the arguments containing a '.' ?!? |
||||
# run: > |
||||
# cmake -S wrappers/winrt -B build -A ARM64 |
||||
# -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0 -DCMAKE_BUILD_TYPE=Release |
||||
# -DBUILD_WINRT_LIB=ON -DZXING_EXAMPLES=OFF -DZXING_BLACKBOX_TESTS=OFF -DZXING_C_API=OFF |
||||
# -DEXTENSION_SDK_OUTPUT=dist/UAP/v0.8.0.0/ExtensionSDKs/ZXingWinRT/1.0.0.0 |
||||
|
||||
# - name: Build |
||||
# run: cmake --build build -j8 --config Release |
||||
|
||||
# - uses: actions/upload-artifact@v6 |
||||
# with: |
||||
# name: winrt-ARM64-artifacts |
||||
# path: build/dist |
||||
|
||||
build-tarball: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Build |
||||
run: python .github/create_release_archive.py |
||||
|
||||
- name: Find generated archives |
||||
id: find |
||||
run: | |
||||
tgz=$(ls zxing-cpp-*.tar.gz 2>/dev/null | head -n1 || true) |
||||
zip=$(ls zxing-cpp-*.zip 2>/dev/null | head -n1 || true) |
||||
echo "tgz=$tgz" >> $GITHUB_OUTPUT |
||||
echo "zip=$zip" >> $GITHUB_OUTPUT |
||||
|
||||
- name: Upload source_artifacts |
||||
uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: source_artifacts |
||||
path: | |
||||
${{ steps.find.outputs.tgz }} |
||||
${{ steps.find.outputs.zip }} |
||||
@ -0,0 +1,59 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need |
||||
# to commit it to your repository. |
||||
# |
||||
# You may wish to alter this file to override the set of languages analyzed, |
||||
# or to provide custom queries or build logic. |
||||
# |
||||
# ******** NOTE ******** |
||||
# We have attempted to detect the languages in your repository. Please check |
||||
# the `language` matrix defined below to confirm you have the correct set of |
||||
# supported CodeQL languages. |
||||
# |
||||
name: CodeQL |
||||
|
||||
on: |
||||
workflow_dispatch: |
||||
# push: |
||||
# branches: [ master ] |
||||
# pull_request: |
||||
# # The branches below must be a subset of the branches above |
||||
# branches: [ master ] |
||||
# schedule: |
||||
# - cron: '18 21 * * 6' |
||||
|
||||
jobs: |
||||
analyze: |
||||
name: Analyze |
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} |
||||
|
||||
strategy: |
||||
fail-fast: false |
||||
matrix: |
||||
language: [ 'cpp' ] # , 'java-kotlin', 'swift' currently fail with autobuild, 'python' is not worth the extra build time |
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] |
||||
# Learn more: |
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed |
||||
|
||||
steps: |
||||
- name: Checkout repository |
||||
uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Initialize CodeQL |
||||
uses: github/codeql-action/init@v4 |
||||
with: |
||||
languages: ${{ matrix.language }} |
||||
build-mode: manual |
||||
# If you wish to specify custom queries, you can do so here or in a config file. |
||||
# By default, queries listed here will override any specified in a config file. |
||||
# Prefix the list here with "+" to use these queries and those in the config file. |
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main |
||||
|
||||
- name: Build |
||||
run: | |
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DZXING_EXPERIMENTAL_API=ON |
||||
cmake --build build -j |
||||
|
||||
- name: Perform CodeQL Analysis |
||||
uses: github/codeql-action/analyze@v4 |
||||
@ -0,0 +1,71 @@
|
||||
# This workflow uses actions that are not certified by GitHub. |
||||
# They are provided by a third-party and are governed by |
||||
# separate terms of service, privacy policy, and support |
||||
# documentation. |
||||
# |
||||
# Find more information at: |
||||
# https://github.com/microsoft/msvc-code-analysis-action |
||||
|
||||
name: Microsoft C++ Code Analysis |
||||
|
||||
on: workflow_dispatch |
||||
|
||||
# on: |
||||
# push: |
||||
# branches: [ "master" ] |
||||
# pull_request: |
||||
# branches: [ "master" ] |
||||
# schedule: |
||||
# - cron: '34 6 * * 1' |
||||
|
||||
env: |
||||
# Path to the CMake build directory. |
||||
build: '${{ github.workspace }}/build' |
||||
|
||||
permissions: |
||||
contents: read |
||||
|
||||
jobs: |
||||
analyze: |
||||
permissions: |
||||
contents: read # for actions/checkout to fetch code |
||||
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results |
||||
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status |
||||
name: Analyze |
||||
runs-on: windows-latest |
||||
|
||||
steps: |
||||
- name: Checkout repository |
||||
uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Configure CMake |
||||
run: cmake -B ${{ env.build }} -DZXING_EXPERIMENTAL_API=ON -DZXING_DISABLE_PCH=ON |
||||
|
||||
# Build is not required unless generated source files are used |
||||
# - name: Build CMake |
||||
# run: cmake --build ${{ env.build }} |
||||
|
||||
- name: Initialize MSVC Code Analysis |
||||
uses: microsoft/msvc-code-analysis-action@v0.1.1 |
||||
# Provide a unique ID to access the sarif output path |
||||
id: run-analysis |
||||
with: |
||||
cmakeBuildDirectory: ${{ env.build }} |
||||
# Ruleset file that will determine what checks will be run |
||||
ruleset: NativeRecommendedRules.ruleset |
||||
|
||||
# Upload SARIF file to GitHub Code Scanning Alerts |
||||
# this currently fails with the Error: The CodeQL Action does not support uploading multiple SARIF runs with the same category. |
||||
# - name: Upload SARIF to GitHub |
||||
# uses: github/codeql-action/upload-sarif@v4 |
||||
# with: |
||||
# sarif_file: ${{ steps.run-analysis.outputs.sarif }} |
||||
|
||||
# Upload SARIF file as an Artifact to download and view |
||||
- name: Upload SARIF as an Artifact |
||||
uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: sarif-file |
||||
path: ${{ steps.run-analysis.outputs.sarif }} |
||||
@ -0,0 +1,41 @@
|
||||
name: publish-android |
||||
|
||||
on: |
||||
# release: |
||||
# types: [published] |
||||
|
||||
workflow_dispatch: |
||||
inputs: |
||||
publish: |
||||
description: 'Publish package (y/n)' |
||||
default: 'n' |
||||
|
||||
jobs: |
||||
publish: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
- uses: actions/setup-java@v5 |
||||
with: |
||||
distribution: 'temurin' |
||||
java-version: '17' |
||||
# working-directory: wrappers/android |
||||
# run: ./gradlew versionDisplay |
||||
|
||||
- name: Build Library |
||||
working-directory: wrappers/android |
||||
run: ./gradlew assembleRelease |
||||
|
||||
- name: Publish Library |
||||
if: ${{ github.event.inputs.publish == 'y' }} |
||||
working-directory: wrappers/android |
||||
env: |
||||
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }} |
||||
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} |
||||
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }} |
||||
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }} |
||||
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_KEY_PASSWORD }} |
||||
run: ./gradlew publishToMavenCentral --no-configuration-cache |
||||
|
||||
|
||||
# To actually publish the the blob, login to https://central.sonatype.com/publishing/deployments. |
||||
@ -0,0 +1,130 @@
|
||||
name: publish-dotnet |
||||
|
||||
on: |
||||
# release: |
||||
# types: [published] |
||||
|
||||
workflow_dispatch: |
||||
inputs: |
||||
publish: |
||||
description: 'Publish package (y/n)' |
||||
default: 'n' |
||||
|
||||
jobs: |
||||
build-native: |
||||
name: build-native ${{ matrix.os }} |
||||
runs-on: ${{ matrix.os }} |
||||
strategy: |
||||
fail-fast: false |
||||
matrix: |
||||
include: |
||||
- os: windows-latest |
||||
lib_name: ZXing.dll |
||||
rid: win-x64 |
||||
lib_path: build/core/Release/ZXing.dll |
||||
- os: windows-11-arm |
||||
lib_name: ZXing.dll |
||||
rid: win-arm64 |
||||
lib_path: build/core/Release/ZXing.dll |
||||
- os: ubuntu-latest |
||||
lib_name: libZXing.so |
||||
rid: linux-x64 |
||||
lib_path: build/core/libZXing.so |
||||
- os: ubuntu-24.04-arm |
||||
lib_name: libZXing.so |
||||
rid: linux-arm64 |
||||
lib_path: build/core/libZXing.so |
||||
- os: macos-15-intel |
||||
lib_name: libZXing.dylib |
||||
rid: osx-x64 |
||||
lib_path: build/core/libZXing.dylib |
||||
- os: macos-latest |
||||
lib_name: libZXing.dylib |
||||
rid: osx-arm64 |
||||
lib_path: build/core/libZXing.dylib |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Configure CMake |
||||
run: > |
||||
cmake -S . -B build |
||||
-DCMAKE_BUILD_TYPE=Release |
||||
-DBUILD_SHARED_LIBS=ON |
||||
-DZXING_EXAMPLES=OFF |
||||
-DZXING_TEST_DOTNET=ON |
||||
|
||||
- name: Build |
||||
run: cmake --build build --config Release --parallel |
||||
|
||||
- name: Test |
||||
run: ctest --test-dir build -V -C Release -R DotNetTest |
||||
|
||||
- name: Stage native library |
||||
run: | |
||||
cmake -E make_directory artifact/runtimes/${{ matrix.rid }}/native |
||||
cmake -E copy_if_different ${{ matrix.lib_path }} artifact/runtimes/${{ matrix.rid }}/native/${{ matrix.lib_name }} |
||||
|
||||
- name: Upload native library |
||||
uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: native-${{ matrix.rid }} |
||||
path: artifact |
||||
|
||||
publish-nuget: |
||||
needs: build-native |
||||
runs-on: ubuntu-latest |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Download native libraries |
||||
uses: actions/download-artifact@v8 |
||||
with: |
||||
pattern: native-* |
||||
path: wrappers/dotnet |
||||
merge-multiple: true |
||||
|
||||
- name: Validate runtime files |
||||
shell: bash |
||||
run: | |
||||
test -f wrappers/dotnet/runtimes/win-x64/native/ZXing.dll |
||||
test -f wrappers/dotnet/runtimes/win-arm64/native/ZXing.dll |
||||
test -f wrappers/dotnet/runtimes/linux-x64/native/libZXing.so |
||||
test -f wrappers/dotnet/runtimes/linux-arm64/native/libZXing.so |
||||
test -f wrappers/dotnet/runtimes/osx-x64/native/libZXing.dylib |
||||
test -f wrappers/dotnet/runtimes/osx-arm64/native/libZXing.dylib |
||||
|
||||
- name: Pack NuGet package |
||||
run: dotnet pack wrappers/dotnet/ZXingCpp/ZXingCpp.csproj --configuration Release --output wrappers/dotnet/artifacts |
||||
|
||||
- name: Smoke test local NuGet package |
||||
shell: bash |
||||
run: | |
||||
set -euo pipefail |
||||
pkg_file="$(ls wrappers/dotnet/artifacts/ZXingCpp.*.nupkg | head -n 1)" |
||||
pkg_version="$(basename "$pkg_file" | sed -E 's/^ZXingCpp\.//; s/\.nupkg$//')" |
||||
|
||||
tmp_dir="$(mktemp -d)" |
||||
dotnet new console -n ZxingPkgTest -o "$tmp_dir/ZxingPkgTest" |
||||
echo "Console.WriteLine(global::ZXingCpp.BarcodeFormat.QRCode.ToString());" > "$tmp_dir/ZxingPkgTest/Program.cs" |
||||
|
||||
dotnet add "$tmp_dir/ZxingPkgTest/ZxingPkgTest.csproj" package ZXingCpp --version "$pkg_version" --no-restore |
||||
dotnet restore "$tmp_dir/ZxingPkgTest/ZxingPkgTest.csproj" \ |
||||
--source "${{ github.workspace }}/wrappers/dotnet/artifacts" \ |
||||
--source https://api.nuget.org/v3/index.json |
||||
dotnet run --project "$tmp_dir/ZxingPkgTest/ZxingPkgTest.csproj" --no-restore |
||||
|
||||
- name: Upload NuGet package artifact |
||||
uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: dotnet-nuget-package |
||||
path: wrappers/dotnet/artifacts/*.nupkg |
||||
|
||||
- name: Push NuGet package |
||||
if: ${{ github.event_name == 'release' || github.event.inputs.publish == 'y' }} |
||||
run: dotnet nuget push wrappers/dotnet/artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate |
||||
@ -0,0 +1,131 @@
|
||||
name: publish-gh-pages |
||||
|
||||
on: |
||||
# Runs on pushes targeting the default branch |
||||
# push: |
||||
# branches: ["master"] |
||||
|
||||
# Runs on new releases |
||||
release: |
||||
types: [published] |
||||
|
||||
# Allows you to run this workflow manually from the Actions tab |
||||
workflow_dispatch: |
||||
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. |
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. |
||||
concurrency: |
||||
group: "pages" |
||||
cancel-in-progress: false |
||||
|
||||
jobs: |
||||
build-wasm: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- name: Checkout |
||||
uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Setup emsdk |
||||
uses: emscripten-core/setup-emsdk@v15 |
||||
# with: |
||||
# # Make sure to set a version number! |
||||
# version: 1.38.40 |
||||
# # This is the name of the cache folder. |
||||
# # The cache folder will be placed in the build directory, |
||||
# # so make sure it doesn't conflict with anything! |
||||
# actions-cache-folder: 'emsdk-cache' |
||||
|
||||
- name: Configure |
||||
run: emcmake cmake -Swrappers/wasm -Bbuild |
||||
|
||||
- name: Build |
||||
run: cmake --build build --parallel |
||||
|
||||
- name: Prepare Archive |
||||
shell: sh |
||||
run: | |
||||
mkdir -p public/demo |
||||
mv build/zxing_* build/*.html public/demo/ |
||||
|
||||
- name: Upload artifact |
||||
uses: actions/upload-artifact@v7 |
||||
with: |
||||
path: 'public' |
||||
name: 'artifact-wasm' |
||||
|
||||
build-docs: |
||||
runs-on: macos-latest |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Install dependencies |
||||
run: brew install doxygen |
||||
|
||||
- name: Configure |
||||
run: cmake -S . -B build |
||||
|
||||
- name: Build docs |
||||
run: cmake --build build --target docs |
||||
|
||||
- name: Prepare deploy folder |
||||
run: | |
||||
mkdir -p public/docs |
||||
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then |
||||
VERSION=${GITHUB_REF#refs/tags/} |
||||
mkdir -p public/docs/$VERSION |
||||
cp -r build/docs/html/* public/docs/$VERSION/ |
||||
else |
||||
mkdir -p public/docs/latest |
||||
cp -r build/docs/html/* public/docs/latest/ |
||||
fi |
||||
|
||||
- uses: actions/upload-artifact@v7 |
||||
with: |
||||
path: public |
||||
name: 'artifact-docs' |
||||
|
||||
deploy: |
||||
needs: [build-wasm, build-docs] |
||||
environment: |
||||
name: github-pages |
||||
url: ${{ steps.deployment.outputs.page_url }} |
||||
runs-on: ubuntu-latest |
||||
|
||||
permissions: |
||||
contents: read |
||||
pages: write |
||||
id-token: write |
||||
|
||||
steps: |
||||
- name: Download WASM artifact |
||||
uses: actions/download-artifact@v8 |
||||
with: |
||||
name: artifact-wasm |
||||
path: wasm |
||||
|
||||
- name: Download Docs artifact |
||||
uses: actions/download-artifact@v8 |
||||
with: |
||||
name: artifact-docs |
||||
path: docs |
||||
|
||||
- name: Merge artifacts |
||||
run: | |
||||
mkdir public |
||||
cp -r wasm/* public/ |
||||
cp -r docs/* public/ |
||||
|
||||
- name: Upload merged artifact |
||||
uses: actions/upload-pages-artifact@v4 |
||||
with: |
||||
path: public |
||||
|
||||
- name: Deploy to GitHub Pages |
||||
id: deployment |
||||
uses: actions/deploy-pages@v5 |
||||
@ -0,0 +1,97 @@
|
||||
name: publish-kn |
||||
|
||||
on: |
||||
workflow_dispatch: |
||||
inputs: |
||||
publish: |
||||
description: 'Publish package (y/n)' |
||||
default: 'n' |
||||
|
||||
jobs: |
||||
publish: |
||||
name: Library Publish |
||||
|
||||
runs-on: macos-latest # at least macos-13 is required to enable c++20 support |
||||
|
||||
defaults: |
||||
run: |
||||
working-directory: ./wrappers/kn |
||||
|
||||
steps: |
||||
- name: Checkout repository |
||||
uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Prepare Zig toolchain |
||||
uses: mlugg/setup-zig@v2 |
||||
with: |
||||
version: 0.13.0 |
||||
|
||||
- name: Checkout toolchain initializer repository |
||||
uses: actions/checkout@v6 |
||||
with: |
||||
repository: ISNing/kn-toolchain-initializer |
||||
path: wrappers/kn/.kn-toolchain-initializer |
||||
|
||||
- name: Set up JDK |
||||
uses: actions/setup-java@v5 |
||||
with: |
||||
java-version: 17 |
||||
distribution: temurin |
||||
|
||||
- name: Setup Gradle |
||||
uses: gradle/actions/setup-gradle@v5 |
||||
|
||||
- name: Validate Gradle Wrapper |
||||
uses: gradle/actions/wrapper-validation@v5 |
||||
|
||||
- name: Initialize Kotlin/Native toolchain |
||||
working-directory: wrappers/kn/.kn-toolchain-initializer |
||||
run: ./gradlew build -DkotlinVersion=2.2.0 |
||||
|
||||
- name: Export Toolchain properties |
||||
run: | |
||||
echo -e "konan.dir=$HOME/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.0" >> local.properties |
||||
|
||||
- name: Build Library |
||||
run: | |
||||
export PATH="$PATH:$(brew --prefix llvm@15)/bin" |
||||
./gradlew assemble |
||||
|
||||
- name: Run All Library Tests |
||||
run: | |
||||
export PATH="$PATH:$(brew --prefix llvm@15)/bin" |
||||
./gradlew allTests |
||||
|
||||
- name: Upload Library Test Reports |
||||
uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: allTests-reports |
||||
path: wrappers/kn/build/reports/tests/allTests |
||||
|
||||
- name: Upload Built LinuxArm64 shared library |
||||
uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: linuxArm64-shared |
||||
path: wrappers/kn/build/cmake/libZXing/linuxArm64 |
||||
|
||||
- name: Upload Built LinuxX64 shared library |
||||
uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: linuxX64-shared |
||||
path: wrappers/kn/build/cmake/libZXing/linuxX64 |
||||
|
||||
- name: Publish Library |
||||
if: ${{ github.event.inputs.publish == 'y' }} |
||||
env: |
||||
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }} |
||||
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} |
||||
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }} |
||||
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }} |
||||
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_KEY_PASSWORD }} |
||||
run: | |
||||
export PATH="$PATH:$(brew --prefix llvm@15)/bin" |
||||
./gradlew publishToMavenCentral |
||||
|
||||
# To actually publish the blob, login to https://central.sonatype.com/publishing/deployments. |
||||
@ -0,0 +1,100 @@
|
||||
name: publish-python |
||||
|
||||
on: |
||||
# release: |
||||
# types: [published] |
||||
|
||||
workflow_dispatch: |
||||
inputs: |
||||
publish: |
||||
description: 'Publish package (y/n)' |
||||
default: 'n' |
||||
# push: |
||||
# branches: [master] |
||||
# tags: ["v*.*.*"] |
||||
# pull_request: |
||||
# branches: [master] |
||||
|
||||
jobs: |
||||
build-wheels: |
||||
name: Build wheels on ${{ matrix.os }} |
||||
runs-on: ${{ matrix.os }} |
||||
strategy: |
||||
fail-fast: false |
||||
matrix: |
||||
os: [ubuntu-latest, macos-latest, windows-latest, macos-15-intel, ubuntu-24.04-arm, windows-11-arm] |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Set up Python |
||||
uses: actions/setup-python@v6 |
||||
with: |
||||
python-version: '3.12' |
||||
|
||||
- name: Install cibuildwheel |
||||
run: python3 -m pip install cibuildwheel==3.3.0 |
||||
|
||||
- name: Build wheels |
||||
run: python3 -m cibuildwheel --output-dir wheelhouse wrappers/python |
||||
|
||||
- name: Upload wheels |
||||
uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: cibw-wheels-${{ matrix.os }} |
||||
path: ./wheelhouse/*.whl |
||||
|
||||
build-sdist: |
||||
name: Build source distribution |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Set up Python |
||||
uses: actions/setup-python@v6 |
||||
with: |
||||
python-version: '3.14' |
||||
|
||||
- name: Install dependencies |
||||
working-directory: wrappers/python |
||||
run: python -m pip install --upgrade build |
||||
|
||||
- name: Build sdist |
||||
working-directory: wrappers/python |
||||
run: python3 -m build --sdist |
||||
|
||||
- name: Upload sdist |
||||
uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: cibw-sdist |
||||
path: wrappers/python/dist/*.tar.gz |
||||
|
||||
upload-pypi: |
||||
name: Upload to PyPI |
||||
needs: [build-wheels, build-sdist] |
||||
runs-on: ubuntu-latest |
||||
environment: |
||||
name: pypi |
||||
url: https://pypi.org/p/zxing-cpp |
||||
permissions: |
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing |
||||
# only run if the commit is tagged... |
||||
# if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') |
||||
if: github.event_name == 'release' || github.event.inputs.publish == 'y' |
||||
steps: |
||||
- uses: actions/download-artifact@v8 |
||||
with: |
||||
pattern: cibw-* |
||||
path: dist |
||||
merge-multiple: true |
||||
|
||||
- name: List wheels |
||||
run: ls dist |
||||
|
||||
- uses: pypa/gh-action-pypi-publish@release/v1 |
||||
# with: |
||||
# repository-url: https://test.pypi.org/legacy/ |
||||
@ -0,0 +1,39 @@
|
||||
name: publish-source |
||||
|
||||
on: |
||||
release: |
||||
types: [published] |
||||
workflow_dispatch: |
||||
|
||||
jobs: |
||||
publish-source: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
with: |
||||
submodules: true |
||||
|
||||
- name: Create source archives |
||||
run: python .github/create_release_archive.py |
||||
|
||||
- name: Create test samples tarball |
||||
run: tar -czf test_samples.tar.gz test/samples |
||||
|
||||
- name: Upload workflow artifact |
||||
if: github.event_name != 'release' |
||||
uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: source-archives |
||||
path: | |
||||
*.zip |
||||
*.gz |
||||
if-no-files-found: error |
||||
|
||||
- name: Upload to release |
||||
if: github.event_name == 'release' |
||||
run: | |
||||
tag='${{ github.event.release.tag_name }}' |
||||
version="${tag#v}" |
||||
gh release upload --clobber "$tag" "$GITHUB_WORKSPACE/zxing-cpp-$version.tar.gz" "$GITHUB_WORKSPACE/zxing-cpp-$version.zip" "$GITHUB_WORKSPACE/test_samples.tar.gz" |
||||
env: |
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
||||
@ -0,0 +1,82 @@
|
||||
name: publish-winrt |
||||
on: |
||||
# release: |
||||
# types: [published] |
||||
|
||||
workflow_dispatch: |
||||
inputs: |
||||
publish: |
||||
description: 'Publish package (y/n)' |
||||
default: 'n' |
||||
package_version: |
||||
description: 'Package version (major.minor.patch)' |
||||
default: '1.0.0' |
||||
|
||||
jobs: |
||||
build: |
||||
runs-on: windows-latest |
||||
strategy: |
||||
matrix: |
||||
target: [Win32, x64, ARM64] |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v6 |
||||
|
||||
- name: Create build environment |
||||
shell: cmd |
||||
run: | |
||||
cmake -E make_directory ${{runner.workspace}}/build |
||||
cmake -E copy_directory ${{github.workspace}}/wrappers/winrt/UAP ${{runner.workspace}}/build/dist/UAP |
||||
cmake -E copy_directory ${{github.workspace}}/wrappers/winrt/nuget ${{runner.workspace}}/build/dist/nuget |
||||
|
||||
- name: Configure CMake |
||||
shell: cmd |
||||
working-directory: ${{runner.workspace}}/build |
||||
run: cmake ${{github.workspace}}/wrappers/winrt -A ${{matrix.target}} -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0 -DCMAKE_BUILD_TYPE=Release -DBUILD_WINRT_LIB=ON -DZXING_EXAMPLES=OFF -DZXING_BLACKBOX_TESTS=OFF -DEXTENSION_SDK_OUTPUT=dist/UAP/v0.8.0.0/ExtensionSDKs/ZXingWinRT/1.0.0.0 |
||||
|
||||
- name: Build |
||||
shell: cmd |
||||
working-directory: ${{runner.workspace}}/build |
||||
run: cmake --build . -j8 --config Release |
||||
|
||||
- uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: winrt-${{matrix.target}}-artifacts |
||||
path: ${{runner.workspace}}/build/dist |
||||
|
||||
publish-package: |
||||
needs: build |
||||
runs-on: windows-latest |
||||
if: ${{ github.event_name == 'release' || github.event.inputs.publish == 'y' }} |
||||
steps: |
||||
- uses: actions/download-artifact@v8 |
||||
with: |
||||
name: winrt-Win32-artifacts |
||||
- uses: actions/download-artifact@v8 |
||||
with: |
||||
name: winrt-x64-artifacts |
||||
- uses: actions/download-artifact@v8 |
||||
with: |
||||
name: winrt-ARM64-artifacts |
||||
|
||||
- name: Create NuGet package |
||||
if: ${{ github.event_name != 'release' }} |
||||
shell: cmd |
||||
run: nuget pack -Version ${{ github.event.inputs.package_version }} -OutputFileNamesWithoutVersion nuget/ZXingWinRT.nuspec |
||||
|
||||
- name: Create NuGet package |
||||
if: ${{ github.event_name == 'release' }} |
||||
shell: bash |
||||
env: |
||||
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} |
||||
run: nuget pack -Version ${RELEASE_TAG_NAME:1} -OutputFileNamesWithoutVersion nuget/ZXingWinRT.nuspec |
||||
|
||||
- name: Publish NuGet package |
||||
shell: cmd |
||||
run: nuget push huycn.zxingcpp.winrt.nupkg -ApiKey ${{ secrets.NUGET_API_KEY }} -Source https://api.nuget.org/v3/index.json |
||||
|
||||
- uses: actions/upload-artifact@v6 |
||||
with: |
||||
name: nuget-package |
||||
path: huycn.zxingcpp.winrt.nupkg |
||||
|
||||
@ -0,0 +1,27 @@
|
||||
*.o |
||||
*.so |
||||
*.lib |
||||
*.d |
||||
*.a |
||||
compile_commands.json |
||||
/build |
||||
|
||||
# QtCreator |
||||
CMakeLists.txt.user |
||||
|
||||
# Vim |
||||
*.swp |
||||
|
||||
# XCode/Swift |
||||
.swiftpm |
||||
.build |
||||
|
||||
# JetBrain (AndroidStudio, clion) |
||||
.idea |
||||
|
||||
# Visual Studio |
||||
/.vs |
||||
/CMakeUserPresets.json |
||||
|
||||
# Visual Studio Code |
||||
/.vscode |
||||
@ -0,0 +1,4 @@
|
||||
[submodule "zint"] |
||||
path = zint |
||||
url = https://github.com/zint/zint.git |
||||
shallow = true |
||||
@ -0,0 +1,121 @@
|
||||
cmake_minimum_required(VERSION 3.16) |
||||
|
||||
project(ZXing) |
||||
|
||||
option (ZXING_READERS "Build with reader support (decoders)" ON) |
||||
set (ZXING_WRITERS "ON" CACHE STRING "Build with old and/or new writer (encoder) backend (OFF/ON/OLD/NEW/BOTH)") |
||||
option (ZXING_C_API "Build the C-API" ON) |
||||
option (ZXING_EXPERIMENTAL_API "Build with experimental API" OFF) |
||||
option (ZXING_EXAMPLES "Build the example barcode reader/writer applications" ON) |
||||
option (ZXING_EXAMPLES_QT "Build the Qt based example barcode reader/writer applications" OFF) |
||||
option (ZXING_BLACKBOX_TESTS "Build the black box reader/writer tests" OFF) |
||||
option (ZXING_UNIT_TESTS "Build the unit tests (don't enable for production builds)" OFF) |
||||
option (ZXING_TEST_DOTNET "Test if the .NET wrapper works" OFF) |
||||
option (ZXING_TEST_GO "Test if the Go wrapper works" OFF) |
||||
option (ZXING_TEST_INSTALL "Test if the installed/public API works" OFF) |
||||
option (ZXING_PYTHON_MODULE "Build the python module" OFF) |
||||
set (ZXING_DEPENDENCIES "AUTO" CACHE STRING "Fetch from github or use locally installed (AUTO/GITHUB/LOCAL)") |
||||
option (ZXING_CLANG_TIDY "Enable clang-tidy analysis" OFF) |
||||
|
||||
if (WIN32) |
||||
option (BUILD_SHARED_LIBS "Build and link as shared library" OFF) |
||||
else() |
||||
option (BUILD_SHARED_LIBS "Build and link as shared library" ON) |
||||
endif() |
||||
|
||||
if (MSVC) |
||||
add_definitions (-DUNICODE -D_UNICODE -D_CRT_SECURE_NO_WARNINGS) |
||||
option (ZXING_LINK_CPP_STATICALLY "MSVC only, link standard library statically (/MT and /MTd)" OFF) |
||||
if (LINK_CPP_STATICALLY OR ZXING_LINK_CPP_STATICALLY) |
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>") |
||||
endif() |
||||
endif() |
||||
|
||||
if (NOT CMAKE_BUILD_TYPE) |
||||
set (DEFAULT_BUILD_TYPE "Release") |
||||
message (STATUS "Setting build type to '${DEFAULT_BUILD_TYPE}' as none was specified.") |
||||
set (CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}" CACHE STRING "Choose the type of build." FORCE) |
||||
set_property (CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") |
||||
endif() |
||||
|
||||
if (NOT (ZXING_READERS OR ZXING_WRITERS)) |
||||
message(FATAL_ERROR "At least one of ZXING_READERS/ZXING_WRITERS must be enabled.") |
||||
endif() |
||||
|
||||
set(ZXING_WRITERS_LIST OFF ON OLD NEW BOTH) |
||||
set_property(CACHE ZXING_WRITERS PROPERTY STRINGS ${ZXING_WRITERS_LIST}) |
||||
if(NOT ZXING_WRITERS IN_LIST ZXING_WRITERS_LIST) |
||||
message(FATAL_ERROR "ZXING_WRITERS must be one of ${ZXING_WRITERS_LIST}") |
||||
endif() |
||||
|
||||
set(ZXING_DEPENDENCIES_LIST AUTO GITHUB LOCAL) |
||||
set_property(CACHE ZXING_DEPENDENCIES PROPERTY STRINGS ${ZXING_DEPENDENCIES_LIST}) |
||||
if(NOT ZXING_DEPENDENCIES IN_LIST ZXING_DEPENDENCIES_LIST) |
||||
message(FATAL_ERROR "ZXING_DEPENDENCIES must be one of ${ZXING_DEPENDENCIES_LIST}") |
||||
endif() |
||||
|
||||
if (NOT DEFINED CMAKE_CXX_STANDARD) |
||||
set (CMAKE_CXX_STANDARD 20) |
||||
set (CMAKE_CXX_STANDARD_REQUIRED ON) |
||||
set (CMAKE_CXX_EXTENSIONS OFF) |
||||
endif() |
||||
|
||||
set(CMAKE_COLOR_DIAGNOSTICS ON) |
||||
|
||||
if (ZXING_CLANG_TIDY) |
||||
find_program(CMAKE_CXX_CLANG_TIDY NAMES "clang-tidy" DOC "Path to clang-tidy executable") |
||||
if (NOT CMAKE_CXX_CLANG_TIDY) |
||||
message(WARNING "clang-tidy not found.") |
||||
else() |
||||
message(STATUS "clang-tidy found: ${CMAKE_CXX_CLANG_TIDY}") |
||||
set(ZXING_DISABLE_PCH ON) |
||||
endif() |
||||
endif() |
||||
|
||||
# make installed binaries relocatable on macOS and Linux |
||||
set(CMAKE_BUILD_RPATH_USE_ORIGIN ON) # improve build reproducibility |
||||
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) |
||||
set(CMAKE_INSTALL_RPATH "$<$<PLATFORM_ID:Darwin>:@loader_path/../lib>$<$<PLATFORM_ID:Linux>:$ORIGIN/../lib>") |
||||
|
||||
add_subdirectory (core) |
||||
add_subdirectory (docs) |
||||
|
||||
enable_testing() |
||||
|
||||
include(zxing.cmake) |
||||
|
||||
if (ZXING_EXAMPLES) |
||||
add_subdirectory (example) |
||||
endif() |
||||
if (ZXING_EXAMPLES_QT) |
||||
add_subdirectory (wrappers/qt) |
||||
endif() |
||||
if (ZXING_BLACKBOX_TESTS) |
||||
add_subdirectory (test/blackbox) |
||||
endif() |
||||
if (ZXING_UNIT_TESTS) |
||||
add_subdirectory (test/unit) |
||||
endif() |
||||
if (ZXING_TEST_DOTNET) |
||||
add_subdirectory (wrappers/dotnet) |
||||
endif() |
||||
if (ZXING_TEST_GO) |
||||
add_subdirectory (wrappers/go) |
||||
endif() |
||||
if (ZXING_TEST_INSTALL) |
||||
add_test( |
||||
NAME InstallTest |
||||
COMMAND |
||||
${CMAKE_COMMAND} |
||||
-DPROJECT_BINARY_DIR=${CMAKE_BINARY_DIR} |
||||
-DSOURCE_DIR=${CMAKE_SOURCE_DIR}/test/install |
||||
-DZXING_WRITERS=${ZXING_WRITERS} |
||||
-P ${CMAKE_SOURCE_DIR}/test/install/run.cmake |
||||
) |
||||
endif() |
||||
if (ZXING_PYTHON_MODULE) |
||||
add_subdirectory (wrappers/python) |
||||
endif() |
||||
if (ZXING_C_API) |
||||
add_subdirectory (wrappers/c) |
||||
endif() |
||||
@ -0,0 +1,202 @@
|
||||
Apache License |
||||
Version 2.0, January 2004 |
||||
http://www.apache.org/licenses/ |
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||
|
||||
1. Definitions. |
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, |
||||
and distribution as defined by Sections 1 through 9 of this document. |
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by |
||||
the copyright owner that is granting the License. |
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all |
||||
other entities that control, are controlled by, or are under common |
||||
control with that entity. For the purposes of this definition, |
||||
"control" means (i) the power, direct or indirect, to cause the |
||||
direction or management of such entity, whether by contract or |
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||
outstanding shares, or (iii) beneficial ownership of such entity. |
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity |
||||
exercising permissions granted by this License. |
||||
|
||||
"Source" form shall mean the preferred form for making modifications, |
||||
including but not limited to software source code, documentation |
||||
source, and configuration files. |
||||
|
||||
"Object" form shall mean any form resulting from mechanical |
||||
transformation or translation of a Source form, including but |
||||
not limited to compiled object code, generated documentation, |
||||
and conversions to other media types. |
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or |
||||
Object form, made available under the License, as indicated by a |
||||
copyright notice that is included in or attached to the work |
||||
(an example is provided in the Appendix below). |
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object |
||||
form, that is based on (or derived from) the Work and for which the |
||||
editorial revisions, annotations, elaborations, or other modifications |
||||
represent, as a whole, an original work of authorship. For the purposes |
||||
of this License, Derivative Works shall not include works that remain |
||||
separable from, or merely link (or bind by name) to the interfaces of, |
||||
the Work and Derivative Works thereof. |
||||
|
||||
"Contribution" shall mean any work of authorship, including |
||||
the original version of the Work and any modifications or additions |
||||
to that Work or Derivative Works thereof, that is intentionally |
||||
submitted to Licensor for inclusion in the Work by the copyright owner |
||||
or by an individual or Legal Entity authorized to submit on behalf of |
||||
the copyright owner. For the purposes of this definition, "submitted" |
||||
means any form of electronic, verbal, or written communication sent |
||||
to the Licensor or its representatives, including but not limited to |
||||
communication on electronic mailing lists, source code control systems, |
||||
and issue tracking systems that are managed by, or on behalf of, the |
||||
Licensor for the purpose of discussing and improving the Work, but |
||||
excluding communication that is conspicuously marked or otherwise |
||||
designated in writing by the copyright owner as "Not a Contribution." |
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||
on behalf of whom a Contribution has been received by Licensor and |
||||
subsequently incorporated within the Work. |
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
copyright license to reproduce, prepare Derivative Works of, |
||||
publicly display, publicly perform, sublicense, and distribute the |
||||
Work and such Derivative Works in Source or Object form. |
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
(except as stated in this section) patent license to make, have made, |
||||
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||
where such license applies only to those patent claims licensable |
||||
by such Contributor that are necessarily infringed by their |
||||
Contribution(s) alone or by combination of their Contribution(s) |
||||
with the Work to which such Contribution(s) was submitted. If You |
||||
institute patent litigation against any entity (including a |
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||
or a Contribution incorporated within the Work constitutes direct |
||||
or contributory patent infringement, then any patent licenses |
||||
granted to You under this License for that Work shall terminate |
||||
as of the date such litigation is filed. |
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the |
||||
Work or Derivative Works thereof in any medium, with or without |
||||
modifications, and in Source or Object form, provided that You |
||||
meet the following conditions: |
||||
|
||||
(a) You must give any other recipients of the Work or |
||||
Derivative Works a copy of this License; and |
||||
|
||||
(b) You must cause any modified files to carry prominent notices |
||||
stating that You changed the files; and |
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works |
||||
that You distribute, all copyright, patent, trademark, and |
||||
attribution notices from the Source form of the Work, |
||||
excluding those notices that do not pertain to any part of |
||||
the Derivative Works; and |
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its |
||||
distribution, then any Derivative Works that You distribute must |
||||
include a readable copy of the attribution notices contained |
||||
within such NOTICE file, excluding those notices that do not |
||||
pertain to any part of the Derivative Works, in at least one |
||||
of the following places: within a NOTICE text file distributed |
||||
as part of the Derivative Works; within the Source form or |
||||
documentation, if provided along with the Derivative Works; or, |
||||
within a display generated by the Derivative Works, if and |
||||
wherever such third-party notices normally appear. The contents |
||||
of the NOTICE file are for informational purposes only and |
||||
do not modify the License. You may add Your own attribution |
||||
notices within Derivative Works that You distribute, alongside |
||||
or as an addendum to the NOTICE text from the Work, provided |
||||
that such additional attribution notices cannot be construed |
||||
as modifying the License. |
||||
|
||||
You may add Your own copyright statement to Your modifications and |
||||
may provide additional or different license terms and conditions |
||||
for use, reproduction, or distribution of Your modifications, or |
||||
for any such Derivative Works as a whole, provided Your use, |
||||
reproduction, and distribution of the Work otherwise complies with |
||||
the conditions stated in this License. |
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||
any Contribution intentionally submitted for inclusion in the Work |
||||
by You to the Licensor shall be under the terms and conditions of |
||||
this License, without any additional terms or conditions. |
||||
Notwithstanding the above, nothing herein shall supersede or modify |
||||
the terms of any separate license agreement you may have executed |
||||
with Licensor regarding such Contributions. |
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade |
||||
names, trademarks, service marks, or product names of the Licensor, |
||||
except as required for reasonable and customary use in describing the |
||||
origin of the Work and reproducing the content of the NOTICE file. |
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or |
||||
agreed to in writing, Licensor provides the Work (and each |
||||
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
implied, including, without limitation, any warranties or conditions |
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||
appropriateness of using or redistributing the Work and assume any |
||||
risks associated with Your exercise of permissions under this License. |
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, |
||||
whether in tort (including negligence), contract, or otherwise, |
||||
unless required by applicable law (such as deliberate and grossly |
||||
negligent acts) or agreed to in writing, shall any Contributor be |
||||
liable to You for damages, including any direct, indirect, special, |
||||
incidental, or consequential damages of any character arising as a |
||||
result of this License or out of the use or inability to use the |
||||
Work (including but not limited to damages for loss of goodwill, |
||||
work stoppage, computer failure or malfunction, or any and all |
||||
other commercial damages or losses), even if such Contributor |
||||
has been advised of the possibility of such damages. |
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing |
||||
the Work or Derivative Works thereof, You may choose to offer, |
||||
and charge a fee for, acceptance of support, warranty, indemnity, |
||||
or other liability obligations and/or rights consistent with this |
||||
License. However, in accepting such obligations, You may act only |
||||
on Your own behalf and on Your sole responsibility, not on behalf |
||||
of any other Contributor, and only if You agree to indemnify, |
||||
defend, and hold each Contributor harmless for any liability |
||||
incurred by, or claims asserted against, such Contributor by reason |
||||
of your accepting any such warranty or additional liability. |
||||
|
||||
END OF TERMS AND CONDITIONS |
||||
|
||||
APPENDIX: How to apply the Apache License to your work. |
||||
|
||||
To apply the Apache License to your work, attach the following |
||||
boilerplate notice, with the fields enclosed by brackets "{}" |
||||
replaced with your own identifying information. (Don't include |
||||
the brackets!) The text should be enclosed in the appropriate |
||||
comment syntax for the file format. We also recommend that a |
||||
file or class name and description of purpose be included on the |
||||
same "printed page" as the copyright notice for easier |
||||
identification within third-party archives. |
||||
|
||||
Copyright {yyyy} {name of copyright owner} |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
you may not use this file except in compliance with the License. |
||||
You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software |
||||
distributed under the License is distributed on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
See the License for the specific language governing permissions and |
||||
limitations under the License. |
||||
|
||||
@ -0,0 +1,38 @@
|
||||
// swift-tools-version:5.7.1 |
||||
import PackageDescription |
||||
|
||||
let package = Package( |
||||
name: "ZXingCpp", |
||||
platforms: [ |
||||
.macOS(.v13), .iOS(.v12) |
||||
], |
||||
products: [ |
||||
.library( |
||||
name: "ZXingCpp", |
||||
targets: ["ZXingCpp"]) |
||||
], |
||||
targets: [ |
||||
.target( |
||||
name: "ZXingCppCore", |
||||
path: "core/src", |
||||
exclude: ["libzint", "ZXingC.cpp", "ZXingCpp.cpp"], |
||||
publicHeadersPath: ".", |
||||
cxxSettings: [ |
||||
.headerSearchPath("../../wrappers/ios/Sources/Wrapper"), |
||||
.define("ZXING_INTERNAL") |
||||
] |
||||
), |
||||
.target( |
||||
name: "ZXingCpp", |
||||
dependencies: ["ZXingCppCore"], |
||||
path: "wrappers/ios/Sources/Wrapper", |
||||
publicHeadersPath: ".", |
||||
linkerSettings: [ |
||||
.linkedFramework("CoreGraphics"), |
||||
.linkedFramework("CoreImage"), |
||||
.linkedFramework("CoreVideo") |
||||
] |
||||
) |
||||
], |
||||
cxxLanguageStandard: CXXLanguageStandard.cxx20 |
||||
) |
||||
@ -0,0 +1,154 @@
|
||||
[](https://github.com/zxing-cpp/zxing-cpp/actions?query=workflow%3ACI) |
||||
|
||||
# ZXing-C++ |
||||
|
||||
ZXing-C++ ("zebra crossing") is an open-source, multi-format linear/matrix barcode image processing library implemented in C++. |
||||
|
||||
It was originally ported from the Java [ZXing library](https://github.com/zxing/zxing) but has been developed further and now includes many improvements in terms of runtime and detection performance. It can both read and write barcodes in a number of formats. Since version 3.0 the default writing backend is provided by the [zint library](https://sourceforge.net/projects/zint/). |
||||
|
||||
## Features |
||||
|
||||
* Written in pure C++20 (public API is C++17 compatible), no third-party dependencies (for the library itself) |
||||
* Thread safe |
||||
* Wrappers/Bindings for: |
||||
|
||||
| | | | |
||||
|:-|:-|:-| |
||||
| [Android](wrappers/android/README.md) | [C](wrappers/c/README.md) | [Go](wrappers/go/README.md) | |
||||
| [iOS](wrappers/ios/README.md) | [Kotlin/Native](wrappers/kn/README.md) | [.NET](wrappers/dotnet/README.md) | |
||||
| [Python](wrappers/python/README.md) | [Qt](wrappers/qt/README.md) | [Rust](wrappers/rust/README.md) | |
||||
| [WebAssembly](wrappers/wasm/README.md) | [WinRT](wrappers/winrt/README.md) | | |
||||
| External projects: | |
||||
| [Flutter](https://pub.dev/packages/flutter_zxing) | [Julia](https://github.com/rakeshksr/ZXingCPP.jl) | | |
||||
|
||||
|
||||
## Supported Formats |
||||
|
||||
| Symbology | Variants | |
||||
|:----------|:---------| |
||||
| ***Retail:*** | *(Point-of-Sale, Coupons)* |
||||
| EAN/UPC | EAN-13, EAN-8, EAN-5ᵂ, EAN-2ᵂ, UPC-A, UPC-E, ISBN |
||||
| DataBar | Omnidirectional, Stacked, Limited, Expanded, Expanded Stacked |
||||
| ***Industrial:*** | *(Logistics, Tracking, Pharma)* |
||||
| Code39 | Standard, Extended, PZN, Code32, (VIN, LOGMARS) |
||||
| Code93 | |
||||
| Code128 | |
||||
| ITF | ITF-14, (DHL Leitcode, DHL Identcode) |
||||
| Telepen | Telepen Alpha, Telepen Numeric |
||||
| ***Matrix:*** | *(Documents, Tickets, Logistics, IDs)* |
||||
| Aztec Code | Aztec Code, Aztec Rune |
||||
| Data Matrix | ECC200 |
||||
| MaxiCode | (partial read support) |
||||
| PDF417 | PDF417, Compact PDF417, MicroPDF417 |
||||
| QR Code | Model 1ᴿ, Model 2, Micro QR Code, rMQR |
||||
| ***Other:*** | *(Legacy, Niche)* |
||||
| Codabar | |
||||
| DXFilmEdge | |
||||
|
||||
|
||||
[Note:] |
||||
* ᵂ : write support only |
||||
* ᴿ : read support only |
||||
* DataBar used to be called RSS. |
||||
* DataBar, DX Film Edge, MaxiCode, Micro QR Code and rMQR Code are not supported for writing if the library is configured with `ZXING_WRITERS=OLD`. |
||||
|
||||
## Sponsors |
||||
|
||||
You can sponsor this library at [GitHub Sponsors](https://github.com/sponsors/axxel). |
||||
|
||||
| | Named Sponsors: | |
||||
|:-:|:-| |
||||
| [](https://www.alliedvision.com/) | [Allied Vision Konstanz GmbH](https://www.alliedvision.com/) | |
||||
| [](https://github.com/jknlsn) | [Jake Nelson](https://github.com/jknlsn) | |
||||
| [](https://github.com/kurzdigital) | [KURZ Digital Solutions GmbH & Co. KG](https://github.com/kurzdigital) | |
||||
| [](https://github.com/moonshine-ai) | [Moonshine AI](https://github.com/moonshine-ai) | |
||||
| [](https://www.sap.com/germany/about/company/innovation/open-source.html) | [SAP (Open Source Program Office)](https://www.sap.com/germany/about/company/innovation/open-source.html) | |
||||
| [](https://github.com/somcosoftware) | [Somco Software](https://somcosoftware.com/en) | |
||||
| [](https://synedra.com/) | [synedra information technologies GmbH](https://synedra.com/) | |
||||
|
||||
Thanks a lot for your contribution! |
||||
|
||||
## Getting Started |
||||
|
||||
### To read barcodes: |
||||
1. Load your image into memory (3rd-party library required). |
||||
2. Call `ReadBarcodes()` from [`ReadBarcode.h`](core/src/ReadBarcode.h), the simplest API to get a list of `Barcode` objects. |
||||
|
||||
A very simple example looks like this: |
||||
```c++ |
||||
#include "ZXing/ZXingCpp.h" |
||||
#include <iostream> |
||||
|
||||
int main(int argc, char** argv) |
||||
{ |
||||
int width, height; |
||||
unsigned char* data; |
||||
// load your image data from somewhere. ImageFormat::Lum assumes grey scale image data. |
||||
|
||||
auto image = ZXing::ImageView(data, width, height, ZXing::ImageFormat::Lum); |
||||
auto options = ZXing::ReaderOptions().formats(ZXing::BarcodeFormat::QRCode); |
||||
auto barcodes = ZXing::ReadBarcodes(image, options); |
||||
|
||||
for (const auto& b : barcodes) |
||||
std::cout << ZXing::ToString(b.format()) << ": " << b.text() << "\n"; |
||||
|
||||
return 0; |
||||
} |
||||
``` |
||||
To see the full capability of the API, have a look at [`ZXingReader.cpp`](example/ZXingReader.cpp). |
||||
|
||||
### To write barcodes: |
||||
1. Create a `Barcode` object with `CreateBarcodeFrom...()` from [`CreateBarcode.h`](core/src/CreateBarcode.h). |
||||
2. The `Barcode::symbol()` can be used to get access to the bit matrix (1 module == 1 pixel, no quiet zone) |
||||
3. Alternatively the 3 `WriteBarcodeTo...()` functions from [`WriteBarcode.h`](core/src/WriteBarcode.h) can be used to create an `Image`, a SVG string or a UTF-8 string representation. |
||||
|
||||
A very simple example looks like this: |
||||
```c++ |
||||
#include "ZXing/ZXingCpp.h" |
||||
#include <iostream> |
||||
|
||||
int main(int argc, char** argv) |
||||
{ |
||||
auto barcode = ZXing::CreateBarcodeFromText("some text", ZXing::BarcodeFormat::QRCode); |
||||
auto svg = ZXing::WriteBarcodeToSVG(barcode); |
||||
|
||||
// see also ZXing::WriteBarcodeToImage() |
||||
|
||||
std::cout << svg << "\n"; |
||||
|
||||
return 0; |
||||
} |
||||
``` |
||||
|
||||
As an example for how to parameterize the process with `CreatorOptions` and `WriterOptions`, have a look at [`ZXingWriter.cpp`](example/ZXingWriter.cpp). |
||||
|
||||
### Online API documentation |
||||
|
||||
The latest API documentation can be found here: https://zxing-cpp.github.io/zxing-cpp/docs/latest |
||||
|
||||
## Web Demos |
||||
- [Read barcodes](https://zxing-cpp.github.io/zxing-cpp/demo/demo_reader.html) |
||||
- [Write barcodes](https://zxing-cpp.github.io/zxing-cpp/demo/demo_writer.html) |
||||
- [Read barcodes from camera](https://zxing-cpp.github.io/zxing-cpp/demo/demo_cam_reader.html) |
||||
|
||||
[Note: those live demos are not necessarily fully up-to-date at all times.] |
||||
|
||||
## Build Instructions |
||||
These are the generic instructions to build the library on Windows/macOS/Linux. For details on how to build the individual wrappers, follow the links above. |
||||
|
||||
1. Make sure [CMake](https://cmake.org) version 3.16 or newer is installed. The python module requires 3.18 or higher. |
||||
2. Make sure a sufficiently C++20 compliant compiler is installed (minimum VS 2019 16.10? / gcc 11 / clang 12?). |
||||
3. See the cmake `ZXING_...` options to enable the testing code, python wrapper, etc. |
||||
|
||||
``` |
||||
git clone https://github.com/zxing-cpp/zxing-cpp.git --recursive --depth 1 |
||||
cmake -S zxing-cpp -B zxing-cpp/build -DCMAKE_BUILD_TYPE=Release |
||||
cmake --build zxing-cpp/build --parallel --config Release |
||||
``` |
||||
|
||||
[Note: ready to use packages are available for/as |
||||
[vcpkg](https://github.com/Microsoft/vcpkg/tree/master/ports/nu-book-zxing-cpp), |
||||
[conan](https://github.com/conan-io/conan-center-index/tree/master/recipes/zxing-cpp), |
||||
[mingw](https://github.com/msys2/MINGW-packages/tree/master/mingw-w64-zxing-cpp), |
||||
[homebrew](https://formulae.brew.sh/formula/zxing-cpp) and a bunch of |
||||
[linux distributions](https://repology.org/project/zxing-cpp/versions).] |
||||
@ -0,0 +1,29 @@
|
||||
cmake_minimum_required(VERSION 3.16) |
||||
|
||||
project(ZXingDocs VERSION 3.1.0) |
||||
|
||||
find_package(Doxygen) |
||||
|
||||
if(DOXYGEN_FOUND) |
||||
include(FetchContent) |
||||
FetchContent_Declare( |
||||
doxygen-awesome-css |
||||
URL https://github.com/jothepro/doxygen-awesome-css/archive/refs/heads/main.zip |
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE |
||||
) |
||||
FetchContent_MakeAvailable(doxygen-awesome-css) |
||||
|
||||
# Save the location the files were cloned into |
||||
# This allows us to get the path to doxygen-awesome.css |
||||
FetchContent_GetProperties(doxygen-awesome-css SOURCE_DIR AWESOME_CSS_DIR) |
||||
|
||||
# Generate the Doxyfile |
||||
set(DOXYFILE_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in) |
||||
set(DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) |
||||
configure_file(${DOXYFILE_IN} ${DOXYFILE_OUT} @ONLY) |
||||
|
||||
add_custom_target(docs |
||||
COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYFILE_OUT} |
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} |
||||
VERBATIM) |
||||
endif() |
||||
@ -0,0 +1,58 @@
|
||||
zxing_add_package_stb() |
||||
|
||||
option (ZXING_EXAMPLES_USE_WEBP "Add support for WebP image decoding" OFF) |
||||
|
||||
if (ZXING_EXAMPLES_USE_WEBP) |
||||
zxing_add_package(WebP WebP https://github.com/webmproject/libwebp.git v1.6.0) |
||||
endif() |
||||
|
||||
include (GNUInstallDirs) |
||||
|
||||
if (ZXING_WRITERS AND ZXING_ENABLE_QRCODE) |
||||
add_executable (ZXingWriter ZXingWriter.cpp) |
||||
|
||||
target_link_libraries (ZXingWriter ZXing::ZXing stb::stb) |
||||
|
||||
add_test(NAME ZXingWriterTest COMMAND ZXingWriter qrcode "I have the best words." test.png) |
||||
|
||||
install(TARGETS ZXingWriter DESTINATION ${CMAKE_INSTALL_BINDIR}) |
||||
endif() |
||||
|
||||
if (ZXING_READERS) |
||||
add_executable (ZXingReader ZXingReader.cpp) |
||||
|
||||
target_link_libraries (ZXingReader ZXing::ZXing stb::stb) |
||||
if (ZXING_EXAMPLES_USE_WEBP) |
||||
target_link_libraries (ZXingReader ZXing::ZXing stb::stb WebP::webp) |
||||
target_compile_definitions(ZXingReader PRIVATE ZXING_USE_WEBP) |
||||
endif() |
||||
|
||||
add_test(NAME ZXingReaderTest COMMAND ZXingReader -fast -format qrcode test.png) # see above |
||||
set_tests_properties(ZXingReaderTest PROPERTIES DEPENDS ZXingWriterTest) |
||||
|
||||
if (ZXING_EXAMPLES_USE_WEBP AND WIN32) |
||||
add_custom_command(TARGET ZXingReader POST_BUILD |
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different |
||||
$<TARGET_RUNTIME_DLLS:ZXingReader> |
||||
$<TARGET_FILE_DIR:ZXingReader> |
||||
COMMAND_EXPAND_LISTS |
||||
VERBATIM |
||||
) |
||||
endif() |
||||
|
||||
install(TARGETS ZXingReader DESTINATION ${CMAKE_INSTALL_BINDIR}) |
||||
endif() |
||||
|
||||
if (ZXING_READERS) |
||||
find_package(OpenCV QUIET) |
||||
if (OpenCV_FOUND) |
||||
add_executable (ZXingOpenCV ZXingOpenCV.cpp) |
||||
target_include_directories (ZXingOpenCV PRIVATE ${OpenCV_INCLUDE_DIRS}) |
||||
target_link_libraries (ZXingOpenCV ZXing::ZXing ${OpenCV_LIBS}) |
||||
else () |
||||
message("INFO: OpenCV not found, skipping ZXingOpenCV example") |
||||
endif() |
||||
endif() |
||||
|
||||
add_executable (ZXingBarcodeFormat ZXingBarcodeFormat.cpp) |
||||
target_link_libraries (ZXingBarcodeFormat ZXing::ZXing) |
||||
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2026 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "BarcodeFormat.h" |
||||
|
||||
#include <version> |
||||
#ifdef __cpp_lib_format |
||||
|
||||
#include "StdPrint.h" |
||||
|
||||
#include <cctype> |
||||
#include <cstring> |
||||
#include <format> |
||||
#include <functional> |
||||
#include <string> |
||||
|
||||
static void PrintUsage(const char* exePath) |
||||
{ |
||||
std::print("ZXingBarcodeFormat - A command line tool to generate wrapper source code for the BarcodeFormat enum\n\n"); |
||||
std::println("Usage: {} <Android|C#|Go|K/N|Rust|Swift>", exePath); |
||||
} |
||||
|
||||
static void PrintBFs( |
||||
std::format_string<std::string, int> fmt, |
||||
std::function<std::string(const char*)> name = [](const char* name) { return std::string(name); }) |
||||
{ |
||||
std::println(fmt, name("Invalid"), 0xffff); |
||||
#define X(NAME, SYM, VAR, FLAGS, ZINT, ENABLED, HRI) std::println(fmt, name(#NAME), ZX_BCF_ID(SYM, VAR)); |
||||
ZX_BCF_LIST(X) |
||||
#undef X |
||||
} |
||||
|
||||
int main(int argc, char* argv[]) |
||||
{ |
||||
auto is = [&, i=1](const char* str) { return strncmp(argv[i], str, strlen(argv[i])) == 0; }; |
||||
|
||||
if (argc != 2) { |
||||
PrintUsage(argv[0]); |
||||
return -1; |
||||
} else if (is("Go")) { |
||||
std::println("package zxingcpp\n"); |
||||
std::println("const ("); |
||||
PrintBFs(" BarcodeFormat{:14} BarcodeFormat = 0x{:04X}"); |
||||
std::println(")"); |
||||
} else if (is("C#")) { |
||||
PrintBFs(" public static readonly BarcodeFormat {:15} = new BarcodeFormat(0x{:04X});"); |
||||
} else if (is("K/N")) { |
||||
PrintBFs(" {0:15}(ZXing_BarcodeFormat.ZXing_BarcodeFormat_{0}),"); |
||||
} else if (is("Rust")) { |
||||
PrintBFs("pub const ZXing_BarcodeFormat_{}: ZXing_BarcodeFormat = 0x{:04X};"); |
||||
} else if (is("Swift")) { |
||||
auto swiftName = [](const char* name) { |
||||
std::string sv(name); |
||||
std::string ret = (char)std::tolower(sv[0]) + sv.substr(1); |
||||
// Convert to camelCase, but keep uppercase letters if they are followed by a lowercase letter
|
||||
// (e.g. QRCode -> qrCode). This is a bit hacky, but it works for the current set of names and avoids the
|
||||
// need for a manual mapping. This ensures that the generated Swift code follows typical naming patterns.
|
||||
for (int i = 2; i < ret.size() && (std::isupper(ret[i]) || std::isdigit(ret[i])); ++i) |
||||
ret[i - 1] = std::tolower(ret[i - 1]); |
||||
ret.back() = std::tolower(ret.back()); |
||||
return ret == "eanupc" ? "eanUPC" : ret == "upca" ? "upcA" : ret == "upce" ? "upcE" : ret; |
||||
}; |
||||
PrintBFs(" public static let {:15} = BarcodeFormat(rawValue: 0x{:04X})", swiftName); |
||||
} else if (is("Android")) { |
||||
auto androidName = [](const char* name) { |
||||
std::string sv(name); |
||||
std::string ret = std::string(1, (char)std::toupper(sv[0])); |
||||
|
||||
for (size_t i = 1; i < sv.size(); ++i) { |
||||
const int c = sv[i]; |
||||
const int prev = sv[i - 1]; |
||||
const int next = i + 1 < sv.size() ? sv[i + 1] : 0; |
||||
if ((std::isdigit(c) != std::isdigit(prev)) || (std::isupper(c) && std::islower(prev)) |
||||
|| (std::isupper(c) && std::isupper(prev) && next != 0 && std::islower(next))) |
||||
ret.push_back('_'); |
||||
ret.push_back(static_cast<char>(std::toupper(c))); |
||||
} |
||||
|
||||
return ret == "EANUPC" ? "EAN_UPC" |
||||
: ret == "UPCA" ? "UPC_A" |
||||
: ret == "UPCE" ? "UPC_E" |
||||
: ret == "ALL_GS_1" ? "ALL_GS1" |
||||
: ret; |
||||
}; |
||||
PrintBFs("\t\t{:20}(0x{:04X}),", androidName); |
||||
} else { |
||||
PrintUsage(argv[0]); |
||||
return -1; |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
#else |
||||
|
||||
#include <cstdio> |
||||
|
||||
int main() |
||||
{ |
||||
printf("This tool requires C++20 support with std::format. Please use a compatible compiler.\n"); |
||||
return -1; |
||||
} |
||||
|
||||
#endif |
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2021 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "ZXingOpenCV.h" |
||||
|
||||
#include <iostream> |
||||
|
||||
using namespace cv; |
||||
|
||||
int main() |
||||
{ |
||||
namedWindow("Display window"); |
||||
|
||||
Mat image; |
||||
VideoCapture cap(0); |
||||
|
||||
if (!cap.isOpened()) |
||||
std::cout << "cannot open camera"; |
||||
else |
||||
while (waitKey(25) != 27) { |
||||
cap >> image; |
||||
auto barcodes = ReadBarcodes(image); |
||||
for (auto& barcode : barcodes) |
||||
DrawBarcode(image, barcode); |
||||
imshow("Display window", image); |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2020 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#pragma once |
||||
|
||||
#include "ReadBarcode.h" |
||||
|
||||
#include <opencv2/opencv.hpp> |
||||
|
||||
inline ZXing::ImageView ImageViewFromMat(const cv::Mat& image) |
||||
{ |
||||
using ZXing::ImageFormat; |
||||
|
||||
auto fmt = ImageFormat::None; |
||||
switch (image.channels()) { |
||||
case 1: fmt = ImageFormat::Lum; break; |
||||
case 2: fmt = ImageFormat::LumA; break; |
||||
case 3: fmt = ImageFormat::BGR; break; |
||||
case 4: fmt = ImageFormat::BGRA; break; |
||||
} |
||||
|
||||
if (image.depth() != CV_8U || fmt == ImageFormat::None) |
||||
return {nullptr, 0, 0, ImageFormat::None}; |
||||
|
||||
return {image.data, image.cols, image.rows, fmt}; |
||||
} |
||||
|
||||
inline ZXing::Barcodes ReadBarcodes(const cv::Mat& image, const ZXing::ReaderOptions& options = {}) |
||||
{ |
||||
return ZXing::ReadBarcodes(ImageViewFromMat(image), options); |
||||
} |
||||
|
||||
inline void DrawBarcode(cv::Mat& img, ZXing::Barcode barcode) |
||||
{ |
||||
auto pos = barcode.position(); |
||||
auto zx2cv = [](ZXing::PointI p) { return cv::Point(p.x, p.y); }; |
||||
auto contour = std::vector<cv::Point>{zx2cv(pos[0]), zx2cv(pos[1]), zx2cv(pos[2]), zx2cv(pos[3])}; |
||||
const auto* pts = contour.data(); |
||||
int npts = contour.size(); |
||||
|
||||
cv::polylines(img, &pts, &npts, 1, true, CV_RGB(0, 255, 0)); |
||||
cv::putText(img, barcode.text(), zx2cv(pos[3]) + cv::Point(0, 20), cv::FONT_HERSHEY_DUPLEX, 0.5, CV_RGB(0, 255, 0)); |
||||
} |
||||
@ -0,0 +1,392 @@
|
||||
/*
|
||||
* Copyright 2016 Nu-book Inc. |
||||
* Copyright 2019 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "GTIN.h" |
||||
#include "ZXingCpp.h" |
||||
|
||||
#include <chrono> |
||||
#include <cctype> |
||||
#include <cstring> |
||||
#include <iostream> |
||||
#include <fstream> |
||||
#include <iomanip> |
||||
#include <iterator> |
||||
#include <memory> |
||||
#include <string> |
||||
#include <vector> |
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION |
||||
#include <stb_image.h> |
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION |
||||
#include <stb_image_write.h> |
||||
|
||||
#ifdef ZXING_USE_WEBP |
||||
#include <webp/decode.h> |
||||
#endif |
||||
|
||||
using namespace ZXing; |
||||
|
||||
struct CLI |
||||
{ |
||||
std::vector<std::string> filePaths; |
||||
std::string outPath; |
||||
int forceChannels = 0; |
||||
int rotate = 0; |
||||
bool oneLine = false; |
||||
bool bytesOnly = false; |
||||
bool showSymbol = false; |
||||
bool json = false; |
||||
}; |
||||
|
||||
static void PrintUsage(const char* exePath) |
||||
{ |
||||
std::cout << "Usage: " << exePath << " [options] <image file>...\n" |
||||
<< " -fast Skip some lines/pixels during detection (faster)\n" |
||||
<< " -norotate Don't try rotated image during detection (faster)\n" |
||||
<< " -noinvert Don't search for inverted codes during detection (faster)\n" |
||||
<< " -noscale Don't try downscaled images during detection (faster)\n" |
||||
<< " -formats <FORMAT[,...]>\n" |
||||
<< " Only detect given format(s) (faster)\n" |
||||
<< " -single Stop after the first barcode is detected (faster)\n" |
||||
<< " -ispure Assume the image contains only a 'pure'/perfect code (faster)\n" |
||||
<< " -errors Include barcodes with errors (like checksum error)\n" |
||||
<< " -binarizer <local|global|fixed>\n" |
||||
<< " Binarizer to be used for gray to binary conversion\n" |
||||
<< " -mode <plain|eci|hri|escaped>\n" |
||||
<< " Text mode used to render the raw byte content into text\n" |
||||
<< " -1 Print only file name, content/error on one line per file/barcode (implies '-mode Escaped')\n" |
||||
<< " -symbol Print the detected symbol (if available)\n" |
||||
<< " -json Print a complete JSON formatted serialization\n" |
||||
#ifdef ZXING_EXPERIMENTAL_API |
||||
<< " -denoise Use extra denoiseing (closing operation)\n" |
||||
#endif |
||||
<< " -bytes Write (only) the bytes content of the symbol(s) to stdout\n" |
||||
<< " -pngout <file name>\n" |
||||
<< " Write a copy of the input image with barcodes outlined by a green line\n" |
||||
<< " -help Print usage information\n" |
||||
<< " -version Print version information\n" |
||||
<< "\n" |
||||
<< "Supported formats are (Symbology : Variants):"; |
||||
for (auto f : BarcodeFormats::list(BarcodeFormat::AllReadable)) { |
||||
if (Symbology(f) == f) |
||||
std::cout << "\n " << std::setw(13) << ToString(f) << " : "; |
||||
else |
||||
std::cout << ToString(f) << ", "; |
||||
} |
||||
std::cout << "\n\n"; |
||||
|
||||
std::cout << "BarcodeFormats can be lowercase, with or without any of ' -_/', separated by ',' or '|'\n"; |
||||
} |
||||
|
||||
static bool ParseOptions(int argc, char* argv[], ReaderOptions& options, CLI& cli) |
||||
{ |
||||
for (int i = 1; i < argc; ++i) { |
||||
auto is = [&](const char* str) { return strlen(argv[i]) > 1 && strncmp(argv[i], str, strlen(argv[i])) == 0; }; |
||||
if (is("-fast")) { |
||||
options.tryHarder(false); |
||||
} else if (is("-norotate")) { |
||||
options.tryRotate(false); |
||||
} else if (is("-noinvert")) { |
||||
options.tryInvert(false); |
||||
} else if (is("-noscale")) { |
||||
options.tryDownscale(false); |
||||
#ifdef ZXING_EXPERIMENTAL_API |
||||
} else if (is("-denoise")) { |
||||
options.tryDenoise(true); |
||||
#endif |
||||
} else if (is("-single")) { |
||||
options.maxNumberOfSymbols(1); |
||||
} else if (is("-ispure")) { |
||||
options.isPure(true); |
||||
options.binarizer(Binarizer::FixedThreshold); |
||||
} else if (is("-errors")) { |
||||
options.returnErrors(true); |
||||
} else if (is("-formats")) { |
||||
if (++i == argc) |
||||
return false; |
||||
try { |
||||
options.formats(BarcodeFormatsFromString(argv[i])); |
||||
} catch (const std::exception& e) { |
||||
std::cerr << "Error: " << e.what() << "\n\n"; |
||||
return false; |
||||
} |
||||
} else if (is("-binarizer")) { |
||||
if (++i == argc) |
||||
return false; |
||||
else if (is("local")) |
||||
options.binarizer(Binarizer::LocalAverage); |
||||
else if (is("global")) |
||||
options.binarizer(Binarizer::GlobalHistogram); |
||||
else if (is("fixed")) |
||||
options.binarizer(Binarizer::FixedThreshold); |
||||
else |
||||
return false; |
||||
} else if (is("-mode")) { |
||||
if (++i == argc) |
||||
return false; |
||||
else if (is("plain")) |
||||
options.textMode(TextMode::Plain); |
||||
else if (is("eci")) |
||||
options.textMode(TextMode::ECI); |
||||
else if (is("hri")) |
||||
options.textMode(TextMode::HRI); |
||||
else if (is("escaped")) |
||||
options.textMode(TextMode::Escaped); |
||||
else |
||||
return false; |
||||
} else if (is("-1")) { |
||||
cli.oneLine = true; |
||||
} else if (is("-bytes")) { |
||||
cli.bytesOnly = true; |
||||
} else if (is("-symbol")) { |
||||
cli.showSymbol = true; |
||||
} else if (is("-json")) { |
||||
cli.json = true; |
||||
} else if (is("-pngout")) { |
||||
if (++i == argc) |
||||
return false; |
||||
cli.outPath = argv[i]; |
||||
} else if (is("-channels")) { |
||||
if (++i == argc) |
||||
return false; |
||||
cli.forceChannels = std::stoi(argv[i]); |
||||
} else if (is("-rotate")) { |
||||
if (++i == argc) |
||||
return false; |
||||
cli.rotate = std::stoi(argv[i]); |
||||
} else if (is("-help") || is("--help")) { |
||||
PrintUsage(argv[0]); |
||||
exit(0); |
||||
} else if (is("-version") || is("--version")) { |
||||
std::cout << "ZXingReader version " << Version() << "\n"; |
||||
exit(0); |
||||
} else { |
||||
cli.filePaths.push_back(argv[i]); |
||||
} |
||||
} |
||||
|
||||
return !cli.filePaths.empty(); |
||||
} |
||||
|
||||
void drawLine(const ImageView& iv, PointI a, PointI b, bool error) |
||||
{ |
||||
int steps = maxAbsComponent(b - a); |
||||
PointF dir = bresenhamDirection(PointF(b - a)); |
||||
int R = RedIndex(iv.format()), G = GreenIndex(iv.format()), B = BlueIndex(iv.format()); |
||||
for (int i = 0; i < steps; ++i) { |
||||
auto p = PointI(centered(a + i * dir)); |
||||
auto* dst = const_cast<uint8_t*>(iv.data(p.x, p.y)); |
||||
if (dst < iv.data(0, 0) || dst > iv.data(iv.width() - 1, iv.height() - 1)) |
||||
continue; |
||||
dst[R] = error ? 0xff : 0; |
||||
dst[G] = error ? 0 : 0xff; |
||||
dst[B] = 0; |
||||
} |
||||
} |
||||
|
||||
void drawRect(const ImageView& image, const Position& pos, bool error) |
||||
{ |
||||
for (int i = 0; i < 4; ++i) |
||||
drawLine(image, pos[i], pos[(i + 1) % 4], error); |
||||
} |
||||
|
||||
std::unique_ptr<uint8_t, void (*)(void*)> LoadWebP(const std::string& filePath, int* width, int* height, int* channels) |
||||
{ |
||||
#ifdef ZXING_USE_WEBP |
||||
std::ifstream file(filePath, std::ios::binary); |
||||
if (!file) |
||||
throw std::runtime_error("Failed to read image: " + filePath + " (failed to open file)"); |
||||
|
||||
std::vector<uint8_t> compressedData(std::istreambuf_iterator<char>(file), {}); |
||||
if (compressedData.empty()) |
||||
throw std::runtime_error("Failed to read image: " + filePath + " (empty file)"); |
||||
|
||||
*channels = 3; |
||||
uint8_t* img = WebPDecodeRGB(compressedData.data(), compressedData.size(), width, height); |
||||
|
||||
if (img == nullptr) |
||||
throw std::runtime_error("Failed to read image: " + filePath + " (WebP decode failed)"); |
||||
|
||||
return std::unique_ptr<uint8_t, void (*)(void*)>(img, WebPFree); |
||||
#else |
||||
std::cerr << "Failed to read image: " << filePath << " (WebP support is not enabled in this build)" << "\n"; |
||||
return std::unique_ptr<uint8_t, void (*)(void*)>(nullptr, nullptr); |
||||
#endif |
||||
} |
||||
|
||||
std::unique_ptr<uint8_t, void (*)(void*)> LoadStbi(const std::string& filePath, int* width, int* height, int* channels, int forceChannels) |
||||
{ |
||||
auto buffer = std::unique_ptr<uint8_t, void (*)(void*)>( |
||||
filePath == "-" ? stbi_load_from_file(stdin, width, height, channels, forceChannels) |
||||
: stbi_load(filePath.c_str(), width, height, channels, forceChannels), |
||||
stbi_image_free); |
||||
if (buffer == nullptr) |
||||
std::cerr << "Failed to read image: " << filePath << " (" << stbi_failure_reason() << ")" << "\n"; |
||||
return buffer; |
||||
} |
||||
|
||||
bool IEndsWith(std::string_view str, std::string_view suffix) |
||||
{ |
||||
return str.size() > suffix.size() && std::equal(str.end() - suffix.size(), str.end(), suffix.begin(), |
||||
[](char a, char b) { return std::tolower(static_cast<unsigned char>(a)) == b; }); |
||||
} |
||||
|
||||
int main(int argc, char* argv[]) |
||||
{ |
||||
ReaderOptions options; |
||||
CLI cli; |
||||
Barcodes allBarcodes; |
||||
int ret = 0; |
||||
|
||||
options.textMode(TextMode::HRI); |
||||
options.eanAddOnSymbol(EanAddOnSymbol::Read); |
||||
|
||||
if (!ParseOptions(argc, argv, options, cli)) { |
||||
PrintUsage(argv[0]); |
||||
return -1; |
||||
} |
||||
|
||||
std::cout.setf(std::ios::boolalpha); |
||||
|
||||
if (!cli.outPath.empty()) |
||||
cli.forceChannels = 3; // the drawing code only works for RGB data
|
||||
|
||||
for (const auto& filePath : cli.filePaths) { |
||||
int width, height, channels; |
||||
auto buffer = IEndsWith(filePath, ".webp") |
||||
? LoadWebP(filePath, &width, &height, &channels) |
||||
: LoadStbi(filePath, &width, &height, &channels, cli.forceChannels); |
||||
if (buffer == nullptr) |
||||
continue; |
||||
|
||||
channels = cli.forceChannels ? cli.forceChannels : channels; |
||||
|
||||
auto ImageFormatFromChannels = std::array{ImageFormat::None, ImageFormat::Lum, ImageFormat::LumA, ImageFormat::RGB, ImageFormat::RGBA}; |
||||
ImageView image{buffer.get(), width, height, ImageFormatFromChannels.at(channels)}; |
||||
auto barcodes = ReadBarcodes(image.rotated(cli.rotate), options); |
||||
|
||||
// if we did not find anything, insert a dummy to produce some output for each file
|
||||
if (barcodes.empty()) |
||||
barcodes.emplace_back(); |
||||
|
||||
allBarcodes.insert(allBarcodes.end(), barcodes.begin(), barcodes.end()); |
||||
if (filePath == cli.filePaths.back()) { |
||||
auto merged = MergeStructuredAppendSequences(allBarcodes); |
||||
// report all merged sequences as part of the last file to make the logic not overly complicated here
|
||||
barcodes.insert(barcodes.end(), std::make_move_iterator(merged.begin()), std::make_move_iterator(merged.end())); |
||||
} |
||||
|
||||
for (auto&& barcode : barcodes) { |
||||
|
||||
if (!cli.outPath.empty()) |
||||
drawRect(image, barcode.position(), bool(barcode.error())); |
||||
|
||||
ret |= static_cast<int>(barcode.error().type()); |
||||
|
||||
if (cli.bytesOnly) { |
||||
std::cout.write(reinterpret_cast<const char*>(barcode.bytes().data()), barcode.bytes().size()); |
||||
continue; |
||||
} |
||||
|
||||
if (cli.json) { |
||||
if (barcode.format() != BarcodeFormat::None) |
||||
std::cout << "{\"FilePath\":\"" << filePath << "\"," << barcode.extra("ALL").substr(1) << "\n"; |
||||
continue; |
||||
} |
||||
|
||||
if (cli.oneLine) { |
||||
std::cout << filePath << " " << ToString(barcode.format()); |
||||
if (barcode.isValid()) |
||||
std::cout << " \"" << barcode.text(TextMode::Escaped) << "\""; |
||||
else if (barcode.error()) |
||||
std::cout << " " << ToString(barcode.error()); |
||||
std::cout << "\n"; |
||||
continue; |
||||
} |
||||
|
||||
if (cli.filePaths.size() > 1 || barcodes.size() > 1) { |
||||
static bool firstFile = true; |
||||
if (!firstFile) |
||||
std::cout << "\n"; |
||||
if (cli.filePaths.size() > 1) |
||||
std::cout << "File: " << filePath << "\n"; |
||||
firstFile = false; |
||||
} |
||||
|
||||
if (barcode.format() == BarcodeFormat::None) { |
||||
std::cout << "No barcode found\n"; |
||||
continue; |
||||
} |
||||
|
||||
std::cout << "Text: \"" << barcode.text() << "\"\n" |
||||
<< "Bytes: " << barcode.text(options.textMode() == TextMode::ECI ? TextMode::HexECI : TextMode::Hex) << "\n" |
||||
<< "Format: " << ToString(barcode.format()) << "\n" |
||||
<< "Symbology: " << ToString(barcode.symbology()) << "\n" |
||||
<< "Identifier: " << barcode.symbologyIdentifier() << "\n" |
||||
<< "Content: " << ToString(barcode.contentType()) << "\n" |
||||
<< "HasECI: " << barcode.hasECI() << "\n" |
||||
<< "Position: " << ToString(barcode.position()) << "\n" |
||||
<< "Rotation: " << barcode.orientation() << " deg\n" |
||||
<< "IsMirrored: " << barcode.isMirrored() << "\n" |
||||
<< "IsInverted: " << barcode.isInverted() << "\n"; |
||||
|
||||
auto printOptional = [](const char* key, const std::string& v) { |
||||
if (!v.empty()) |
||||
std::cout << key << v << "\n"; |
||||
}; |
||||
|
||||
printOptional("ECLevel: ", barcode.ecLevel()); |
||||
printOptional("Version: ", barcode.version()); |
||||
printOptional("Error: ", ToString(barcode.error())); |
||||
|
||||
if (barcode.lineCount()) |
||||
std::cout << "Lines: " << barcode.lineCount() << "\n"; |
||||
|
||||
if (barcode.symbology() == BarcodeFormat::EANUPC) { |
||||
printOptional("Country: ", GTIN::LookupCountryIdentifier(barcode.text(), barcode.format())); |
||||
printOptional("Add-On: ", GTIN::EanAddOn(barcode)); |
||||
printOptional("Price: ", GTIN::Price(GTIN::EanAddOn(barcode))); |
||||
printOptional("Issue #: ", GTIN::IssueNr(GTIN::EanAddOn(barcode))); |
||||
} else if (barcode.format() == BarcodeFormat::ITF && barcode.bytes().size() == 14) { |
||||
printOptional("Country: ", GTIN::LookupCountryIdentifier(barcode.text(), barcode.format())); |
||||
} |
||||
|
||||
if (barcode.isPartOfSequence()) |
||||
std::cout << "Structured Append: symbol " << barcode.sequenceIndex() + 1 << " of " |
||||
<< barcode.sequenceSize() << " (parity/id: '" << barcode.sequenceId() << "')\n"; |
||||
else if (barcode.sequenceSize() > 0) |
||||
std::cout << "Structured Append: merged result from " << barcode.sequenceSize() << " symbols (parity/id: '" |
||||
<< barcode.sequenceId() << "')\n"; |
||||
|
||||
printOptional("Extra: ", barcode.extra()); |
||||
if (cli.showSymbol && barcode.symbol().data()) |
||||
std::cout << "Symbol:\n" << WriteBarcodeToUtf8(barcode); |
||||
} |
||||
|
||||
if (cli.filePaths.size() == 1 && !cli.outPath.empty()) |
||||
stbi_write_png(cli.outPath.c_str(), image.width(), image.height(), 3, image.data(), image.rowStride()); |
||||
|
||||
#ifdef NDEBUG |
||||
if (getenv("MEASURE_PERF")) { |
||||
auto startTime = std::chrono::high_resolution_clock::now(); |
||||
auto duration = startTime - startTime; |
||||
int N = 0; |
||||
int blockSize = 1; |
||||
do { |
||||
for (int i = 0; i < blockSize; ++i) |
||||
ReadBarcodes(image, options); |
||||
N += blockSize; |
||||
duration = std::chrono::high_resolution_clock::now() - startTime; |
||||
if (blockSize < 1000 && duration < std::chrono::milliseconds(100)) |
||||
blockSize *= 10; |
||||
} while (duration < std::chrono::seconds(1)); |
||||
fprintf(stderr, "time: %5.2f ms per frame\n", double(std::chrono::duration_cast<std::chrono::milliseconds>(duration).count()) / N); |
||||
} |
||||
#endif |
||||
} |
||||
|
||||
return ret; |
||||
} |
||||
@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright 2016 Nu-book Inc. |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// #define USE_OLD_WRITER_API
|
||||
|
||||
#ifndef USE_OLD_WRITER_API |
||||
#include "CreateBarcode.h" |
||||
#include "WriteBarcode.h" |
||||
#else |
||||
#include "BitMatrix.h" |
||||
#include "BitMatrixIO.h" |
||||
#include "CharacterSet.h" |
||||
#include "MultiFormatWriter.h" |
||||
#endif |
||||
#include "Version.h" |
||||
|
||||
#include <algorithm> |
||||
#include <cctype> |
||||
#include <cstring> |
||||
#include <fstream> |
||||
#include <iostream> |
||||
#include <iomanip> |
||||
#include <string> |
||||
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION |
||||
#include <stb_image_write.h> |
||||
|
||||
using namespace ZXing; |
||||
|
||||
static void PrintUsage(const char* exePath) |
||||
{ |
||||
std::cout << "Usage: " << exePath |
||||
<< " [-options <creator-options>] [-scale <factor>] [-binary] [-noqz] [-hrt] [-invert] <format> <text> <output>\n" |
||||
<< " -options Comma separated list of format specific options and flags\n" |
||||
<< " -scale module size of generated image / negative numbers mean 'target size in pixels'\n" |
||||
<< " -rotate Rotate image by given angle (90, 180 or 270)\n" |
||||
// << " -encoding Encoding used to encode input text\n"
|
||||
<< " -binary Interpret <text> as a file name containing binary data\n" |
||||
<< " -noqz Print barcode without quiet zone\n" |
||||
<< " -hrt Print human readable text below the barcode (if supported)\n" |
||||
<< " -invert Invert colors (switch black and white)\n" |
||||
<< " -help Print usage information\n" |
||||
<< " -version Print version information\n" |
||||
<< "\n" |
||||
<< "Supported formats are (Symbology : Variants):"; |
||||
for (auto f : BarcodeFormats::list(BarcodeFormat::AllCreatable)) { |
||||
if (Symbology(f) == f || f == BarcodeFormat::DXFilmEdge) |
||||
std::cout << "\n " << std::setw(13) << ToString(f) << " : "; |
||||
else |
||||
std::cout << ToString(f) << ", "; |
||||
} |
||||
std::cout << "\n\n"; |
||||
|
||||
std::cout << "Format can be lowercase letters, with or without any of ' -_/'.\n" |
||||
<< "Output format is determined by file name, supported are png, jpg and svg.\n"; |
||||
} |
||||
|
||||
struct CLI |
||||
{ |
||||
BarcodeFormat format = BarcodeFormat::None; |
||||
int scale = 0; |
||||
int rotate = 0; |
||||
std::string input; |
||||
std::string outPath; |
||||
std::string options; |
||||
bool inputIsFile = false; |
||||
bool invert = false; |
||||
bool addHRT = false; |
||||
bool addQZs = true; |
||||
bool verbose = false; |
||||
// CharacterSet encoding = CharacterSet::Unknown;
|
||||
}; |
||||
|
||||
static bool ParseOptions(int argc, char* argv[], CLI& cli) |
||||
{ |
||||
int nonOptArgCount = 0; |
||||
for (int i = 1; i < argc; ++i) { |
||||
auto is = [&](const char* str) { return strncmp(argv[i], str, strlen(argv[i])) == 0; }; |
||||
if (is("-scale")) { |
||||
if (++i == argc) |
||||
return false; |
||||
cli.scale = std::stoi(argv[i]); |
||||
} else if (is("-rotate")) { |
||||
if (++i == argc) |
||||
return false; |
||||
cli.rotate = std::stoi(argv[i]); |
||||
// } else if (is("-encoding")) {
|
||||
// if (++i == argc)
|
||||
// return false;
|
||||
// cli.encoding = CharacterSetFromString(argv[i]);
|
||||
} else if (is("-binary")) { |
||||
cli.inputIsFile = true; |
||||
} else if (is("-hrt")) { |
||||
cli.addHRT = true; |
||||
} else if (is("-noqz")) { |
||||
cli.addQZs = false; |
||||
} else if (is("-invert")) { |
||||
cli.invert = true; |
||||
} else if (is("-options")) { |
||||
if (++i == argc) |
||||
return false; |
||||
cli.options = argv[i]; |
||||
} else if (is("-verbose")) { |
||||
cli.verbose = true; |
||||
} else if (is("-help") || is("--help")) { |
||||
PrintUsage(argv[0]); |
||||
exit(0); |
||||
} else if (is("-version") || is("--version")) { |
||||
std::cout << "ZXingWriter version " << ZXING_VERSION_STR << " (with " |
||||
#ifdef ZXING_USE_ZINT |
||||
<< "zint" |
||||
#else |
||||
<< "zxing-cpp" |
||||
#endif |
||||
<< " backend) \n"; |
||||
exit(0); |
||||
} else if (nonOptArgCount == 0) { |
||||
try { |
||||
cli.format = BarcodeFormatFromString(argv[i]); |
||||
} catch (const std::exception& e) { |
||||
std::cerr << "Error: " << e.what() << "\n\n"; |
||||
return false; |
||||
} |
||||
++nonOptArgCount; |
||||
} else if (nonOptArgCount == 1) { |
||||
cli.input = argv[i]; |
||||
++nonOptArgCount; |
||||
} else if (nonOptArgCount == 2) { |
||||
cli.outPath = argv[i]; |
||||
++nonOptArgCount; |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
return nonOptArgCount == 3; |
||||
} |
||||
|
||||
static std::string GetExtension(const std::string& path) |
||||
{ |
||||
auto fileNameStart = path.find_last_of("/\\"); |
||||
auto fileName = fileNameStart == std::string::npos ? path : path.substr(fileNameStart + 1); |
||||
auto extStart = fileName.find_last_of('.'); |
||||
auto ext = extStart == std::string::npos ? "" : fileName.substr(extStart + 1); |
||||
std::transform(ext.begin(), ext.end(), ext.begin(), [](char c) { return std::tolower(c); }); |
||||
return ext; |
||||
} |
||||
|
||||
template <typename T = char> |
||||
std::vector<T> ReadFile(const std::string& fn) |
||||
{ |
||||
std::basic_ifstream<T> ifs(fn, std::ios::binary); |
||||
if (!ifs.good()) |
||||
throw std::runtime_error("failed to open/read file " + fn); |
||||
return ifs ? std::vector(std::istreambuf_iterator<T>(ifs), std::istreambuf_iterator<T>()) : std::vector<T>(); |
||||
}; |
||||
|
||||
int main(int argc, char* argv[]) |
||||
{ |
||||
CLI cli; |
||||
|
||||
if (!ParseOptions(argc, argv, cli)) { |
||||
PrintUsage(argv[0]); |
||||
return -1; |
||||
} |
||||
|
||||
try { |
||||
#if 1 |
||||
auto cOpts = CreatorOptions(cli.format, cli.options); |
||||
auto barcode = cli.inputIsFile ? CreateBarcodeFromBytes(ReadFile(cli.input), cOpts) : CreateBarcodeFromText(cli.input, cOpts); |
||||
|
||||
auto wOpts = WriterOptions().scale(cli.scale).addQuietZones(cli.addQZs).addHRT(cli.addHRT).invert(cli.invert).rotate(cli.rotate); |
||||
auto bitmap = WriteBarcodeToImage(barcode, wOpts); |
||||
|
||||
if (cli.verbose) { |
||||
std::cout.setf(std::ios::boolalpha); |
||||
std::cout << "Text: \"" << barcode.text() << "\"\n" |
||||
<< "Bytes: " << barcode.text(TextMode::Hex) << "\n" |
||||
<< "Format: " << ToString(barcode.format()) << "\n" |
||||
<< "Symbology: " << ToString(barcode.symbology()) << "\n" |
||||
<< "Identifier: " << barcode.symbologyIdentifier() << "\n" |
||||
<< "Content: " << ToString(barcode.contentType()) << "\n" |
||||
<< "HasECI: " << barcode.hasECI() << "\n" |
||||
<< "Position: " << ToString(barcode.position()) << "\n" |
||||
<< "Rotation: " << barcode.orientation() << " deg\n" |
||||
<< "IsMirrored: " << barcode.isMirrored() << "\n" |
||||
<< "IsInverted: " << barcode.isInverted() << "\n" |
||||
<< "ECLevel: " << barcode.ecLevel() << "\n"; |
||||
std::cout << WriteBarcodeToUtf8(barcode, wOpts); |
||||
} |
||||
#else // 'old' writer API (non zint based)
|
||||
auto writer = MultiFormatWriter(cli.format).setMargin(cli.addQZs ? 10 : 0); |
||||
|
||||
BitMatrix matrix; |
||||
if (cli.inputIsFile) { |
||||
auto file = ReadFile(cli.input); |
||||
std::wstring bytes; |
||||
for (uint8_t c : file) |
||||
bytes.push_back(c); |
||||
writer.setEncoding(CharacterSet::BINARY); |
||||
matrix = writer.encode(bytes, cli.scale, std::clamp(cli.scale / 2, 50, 300)); |
||||
} else { |
||||
writer.setEncoding(CharacterSet::UTF8); |
||||
matrix = writer.encode(cli.input, cli.scale, std::clamp(cli.scale / 2, 50, 300)); |
||||
} |
||||
auto bitmap = ToMatrix<uint8_t>(matrix); |
||||
#endif |
||||
|
||||
auto ext = GetExtension(cli.outPath); |
||||
int success = 0; |
||||
if (ext == "" || ext == "png") { |
||||
success = stbi_write_png(cli.outPath.c_str(), bitmap.width(), bitmap.height(), 1, bitmap.data(), 0); |
||||
} else if (ext == "jpg" || ext == "jpeg") { |
||||
success = stbi_write_jpg(cli.outPath.c_str(), bitmap.width(), bitmap.height(), 1, bitmap.data(), 0); |
||||
} else if (ext == "svg") { |
||||
#ifndef USE_OLD_WRITER_API |
||||
success = (std::ofstream(cli.outPath) << WriteBarcodeToSVG(barcode, wOpts)).good(); |
||||
#else |
||||
success = (std::ofstream(cli.outPath) << ToSVG(matrix)).good(); |
||||
#endif |
||||
} |
||||
|
||||
if (!success) { |
||||
std::cerr << "Failed to write image: " << cli.outPath << "\n"; |
||||
return -1; |
||||
} |
||||
} catch (const std::exception& e) { |
||||
std::cerr << e.what() << "\n"; |
||||
return -1; |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
@ -0,0 +1,752 @@
|
||||
/*
|
||||
* Copyright 2016 Nu-book Inc. |
||||
* Copyright 2019 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "BlackboxTestRunner.h" |
||||
|
||||
#include "ByteArray.h" |
||||
#include "ImageLoader.h" |
||||
#include "ReadBarcode.h" |
||||
#include "Utf.h" |
||||
#include "ZXAlgorithms.h" |
||||
#include "StdPrint.h" |
||||
#include "Version.h" |
||||
|
||||
#include <chrono> |
||||
#include <exception> |
||||
#include <format> |
||||
#include <fstream> |
||||
#include <map> |
||||
#include <optional> |
||||
#include <set> |
||||
#include <sstream> |
||||
#include <string> |
||||
#include <string_view> |
||||
#include <vector> |
||||
|
||||
#ifdef __GNUC__ |
||||
// turns out that libstdc++ is depending on TBB without even linking against it, so this fails to link :-/
|
||||
#undef __cpp_lib_execution |
||||
#endif |
||||
|
||||
#ifdef __cpp_lib_execution |
||||
#include <execution> |
||||
#else |
||||
#include <future> |
||||
#endif |
||||
|
||||
namespace ZXing::Test { |
||||
|
||||
namespace { |
||||
|
||||
struct PureTag {} pure; |
||||
|
||||
struct TestCase |
||||
{ |
||||
struct TC |
||||
{ |
||||
std::string name = {}; |
||||
int minPassCount = 0; // The number of images which must decode for the test to pass.
|
||||
int maxMisreads = 0; // Maximum number of successfully read images with the wrong contents.
|
||||
std::set<fs::path> notDetectedFiles = {}; |
||||
std::map<fs::path, std::string> misReadFiles = {}; |
||||
}; |
||||
|
||||
TC tc[2] = {}; |
||||
int rotation = 0; // The rotation in degrees clockwise to use for this test.
|
||||
|
||||
TestCase(int mntf, int mnts, int mmf, int mms, int r) : tc{{"fast", mntf, mmf}, {"slow", mnts, mms}}, rotation(r) {} |
||||
TestCase(int mntf, int mnts, int r) : TestCase(mntf, mnts, 0, 0, r) {} |
||||
TestCase(int mntp, int mmp, PureTag) : tc{{"pure", mntp, mmp}} {} |
||||
}; |
||||
|
||||
struct FalsePositiveTestCase |
||||
{ |
||||
int maxAllowed; // Maximum number of images which can fail due to successfully reading the wrong contents
|
||||
int rotation; // The rotation in degrees clockwise to use for this test.
|
||||
}; |
||||
} |
||||
|
||||
// Helper for `compareResult()` - map `key` to Barcode property, converting value to std::string
|
||||
static std::string getBarcodeValue(const Barcode& barcode, const std::string& key) |
||||
{ |
||||
if (key == "format") |
||||
return ToString(barcode.format()); |
||||
if (key == "contentType") |
||||
return ToString(barcode.contentType()); |
||||
if (key == "ecLevel") |
||||
return barcode.ecLevel(); |
||||
if (key == "orientation") |
||||
return std::to_string(barcode.orientation()); |
||||
if (key == "symbologyIdentifier") |
||||
return barcode.symbologyIdentifier(); |
||||
if (key == "sequenceSize") |
||||
return std::to_string(barcode.sequenceSize()); |
||||
if (key == "sequenceIndex") |
||||
return std::to_string(barcode.sequenceIndex()); |
||||
if (key == "sequenceId") |
||||
return barcode.sequenceId(); |
||||
if (key == "isLastInSequence") |
||||
return barcode.isLastInSequence() ? "true" : "false"; |
||||
if (key == "isPartOfSequence") |
||||
return barcode.isPartOfSequence() ? "true" : "false"; |
||||
if (key == "isMirrored") |
||||
return barcode.isMirrored() ? "true" : "false"; |
||||
if (key == "isInverted") |
||||
return barcode.isInverted() ? "true" : "false"; |
||||
if (key == "readerInit") |
||||
return barcode.readerInit() ? "true" : "false"; |
||||
|
||||
return std::format("***Unknown key '{}'***", key); |
||||
} |
||||
|
||||
// Read ".result.txt" file contents `expected` with lines "key=value" and compare to `actual`
|
||||
static bool compareResult(const Barcode& barcode, const std::string& expected, std::string& actual) |
||||
{ |
||||
bool ret = true; |
||||
|
||||
actual.clear(); |
||||
actual.reserve(expected.size()); |
||||
|
||||
std::stringstream expectedLines(expected); |
||||
std::string expectedLine; |
||||
while (std::getline(expectedLines, expectedLine)) { |
||||
if (expectedLine.empty() || expectedLine[0] == '#') |
||||
continue; |
||||
auto equals = expectedLine.find('='); |
||||
if (equals == std::string::npos) { |
||||
actual += "***Bad format, missing equals***\n"; |
||||
return false; |
||||
} |
||||
std::string key = expectedLine.substr(0, equals); |
||||
std::string expectedValue = expectedLine.substr(equals + 1); |
||||
std::string actualValue = getBarcodeValue(barcode, key); |
||||
if (actualValue != expectedValue) { |
||||
ret = false; |
||||
actualValue += " ***Mismatch***"; |
||||
} |
||||
actual += StrCat(key, '=', actualValue, '\n'); |
||||
} |
||||
return ret; |
||||
} |
||||
|
||||
static std::string checkResult(const fs::path& imgPath, BarcodeFormat expectedFormat, const Barcode& barcode) |
||||
{ |
||||
if (expectedFormat != barcode.format() && expectedFormat != barcode.symbology()) // allow matching either format or symbology
|
||||
return std::format("Format mismatch: expected '{}' but got '{}'", ToString(expectedFormat), ToString(barcode.format())); |
||||
|
||||
auto readFile = [imgPath](const char* ending) { |
||||
std::ifstream ifs(fs::path(imgPath).replace_extension(ending), std::ios::binary); |
||||
return ifs ? std::optional(std::string(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>())) : std::nullopt; |
||||
}; |
||||
|
||||
if (auto expected = readFile(".result.txt")) { |
||||
std::string actual; |
||||
if (!compareResult(barcode, *expected, actual)) |
||||
return std::format("Result mismatch: expected\n{} but got\n{}", *expected, actual); |
||||
} |
||||
|
||||
if (auto expected = readFile(".txt")) { |
||||
expected = EscapeNonGraphical(*expected); |
||||
auto utf8Result = barcode.text(TextMode::Escaped); |
||||
return utf8Result != *expected ? std::format("Content mismatch: expected '{}' but got '{}'", *expected, utf8Result) : ""; |
||||
} |
||||
|
||||
if (auto expected = readFile(".bin")) { |
||||
ByteArray binaryExpected(*expected); |
||||
return barcode.bytes() != binaryExpected |
||||
? std::format("Content mismatch: expected '{}' but got '{}'", ToHex(binaryExpected), ToHex(barcode.bytes())) |
||||
: ""; |
||||
} |
||||
|
||||
return "Error reading file"; |
||||
} |
||||
|
||||
static int failed = 0; |
||||
static int extra = 0; |
||||
static int totalImageLoadTime = 0; |
||||
|
||||
int timeSince(std::chrono::steady_clock::time_point startTime) |
||||
{ |
||||
auto duration = std::chrono::steady_clock::now() - startTime; |
||||
return narrow_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(duration).count()); |
||||
} |
||||
|
||||
// pre-load images into cache, so the disc io time does not end up in the timing measurement
|
||||
void preloadImageCache(const std::vector<fs::path>& imgPaths) |
||||
{ |
||||
auto startTime = std::chrono::steady_clock::now(); |
||||
ImageLoader::clearCache(); |
||||
for (const auto& imgPath : imgPaths) |
||||
ImageLoader::load(imgPath); |
||||
totalImageLoadTime += timeSince(startTime); |
||||
} |
||||
|
||||
static std::string printPositiveTestStats(int imageCount, const TestCase::TC& tc) |
||||
{ |
||||
int passCount = imageCount - Size(tc.misReadFiles) - Size(tc.notDetectedFiles); |
||||
|
||||
std::print(" | {}: {:3} of {:3}, misread {} of {}", tc.name, passCount, tc.minPassCount, Size(tc.misReadFiles), tc.maxMisreads); |
||||
|
||||
std::string failures; |
||||
if (passCount < tc.minPassCount && !tc.notDetectedFiles.empty()) { |
||||
failures += std::format(" Not detected ({}):", tc.name); |
||||
for (const auto& f : tc.notDetectedFiles) |
||||
failures += std::format(" {}", f.filename().string()); |
||||
failures += "\n"; |
||||
failed += tc.minPassCount - passCount; |
||||
} |
||||
|
||||
extra += std::max(0, passCount - tc.minPassCount); |
||||
if (passCount > tc.minPassCount) |
||||
failures += std::format(" Unexpected detections ({}): {}\n", tc.name, passCount - tc.minPassCount); |
||||
|
||||
if (Size(tc.misReadFiles) > tc.maxMisreads) { |
||||
failures += std::format(" Read error ({}):", tc.name); |
||||
for (const auto& [path, error] : tc.misReadFiles) |
||||
failures += std::format(" {}: {}\n", path.filename().string(), error); |
||||
failed += Size(tc.misReadFiles) - tc.maxMisreads; |
||||
} |
||||
return failures; |
||||
} |
||||
|
||||
static std::vector<fs::path> getImagesInDirectory(const fs::path& directory) |
||||
{ |
||||
std::vector<fs::path> result; |
||||
for (const auto& entry : fs::directory_iterator(directory)) |
||||
if (fs::is_regular_file(entry.status()) && Contains({".webp", ".png", ".jpg", ".pgm", ".gif"}, entry.path().extension())) |
||||
result.push_back(entry.path()); |
||||
|
||||
preloadImageCache(result); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
static void doRunTests(const fs::path& directory, BarcodeFormat format, int totalTests, const std::vector<TestCase>& tests, |
||||
ReaderOptions opts) |
||||
{ |
||||
auto imgPaths = getImagesInDirectory(directory); |
||||
auto folderName = directory.stem(); |
||||
|
||||
if (Size(imgPaths) != totalTests) |
||||
std::println("TEST {} => Expected number of tests: {}, got: {} => FAILED", folderName.string(), totalTests, imgPaths.size()); |
||||
|
||||
for (auto& test : tests) { |
||||
std::print("{:20} @ {:3}, {:3}", folderName.string(), test.rotation, Size(imgPaths)); |
||||
std::vector<int> times; |
||||
std::string failures; |
||||
for (auto tc : test.tc) { |
||||
if (tc.name.empty()) |
||||
break; |
||||
auto startTime = std::chrono::steady_clock::now(); |
||||
opts.tryDownscale(tc.name == "slow_"); |
||||
opts.downscaleFactor(2); |
||||
opts.downscaleThreshold(180); |
||||
opts.tryHarder(tc.name == "slow"); |
||||
opts.tryRotate(tc.name == "slow"); |
||||
opts.tryInvert(tc.name == "slow"); |
||||
opts.isPure(tc.name == "pure"); |
||||
if (opts.isPure()) |
||||
opts.binarizer(Binarizer::FixedThreshold); |
||||
#ifndef PRINT_DEBUG |
||||
#ifdef __cpp_lib_execution |
||||
std::vector<Barcode> barcodes(imgPaths.size()); |
||||
std::transform(std::execution::par, imgPaths.begin(), imgPaths.end(), barcodes.begin(), [&](const fs::path& imgPath) { |
||||
return ReadBarcode(ImageLoader::load(imgPath).rotated(test.rotation), opts); |
||||
}); |
||||
for (size_t i = 0; i < imgPaths.size(); ++i) { |
||||
const auto& imgPath = imgPaths[i]; |
||||
const auto& barcode = barcodes[i]; |
||||
#else |
||||
auto futures = std::vector<std::pair<fs::path, std::future<Barcode>>>{}; |
||||
for (const auto& imgPath : imgPaths) { |
||||
futures.push_back(std::make_pair(imgPath, std::async(std::launch::async, [&](const fs::path& path) { |
||||
return ReadBarcode(ImageLoader::load(path).rotated(test.rotation), opts); |
||||
}, imgPath))); |
||||
} |
||||
for (auto& [imgPath, fut] : futures) { |
||||
auto barcode = fut.get(); |
||||
#endif // __cpp_lib_execution
|
||||
#else |
||||
for (const auto& imgPath : imgPaths) { |
||||
auto barcode = ReadBarcode(ImageLoader::load(imgPath).rotated(test.rotation), opts); |
||||
#endif |
||||
if (barcode.isValid()) { |
||||
auto error = checkResult(imgPath, format, barcode); |
||||
if (!error.empty()) |
||||
tc.misReadFiles[imgPath] = error; |
||||
} else { |
||||
tc.notDetectedFiles.insert(imgPath); |
||||
} |
||||
} |
||||
|
||||
times.push_back(timeSince(startTime)); |
||||
failures += printPositiveTestStats(Size(imgPaths), tc); |
||||
} |
||||
std::println(" | time: {:3} vs {:3} ms", times.front(), times.back()); |
||||
if (!failures.empty()) |
||||
std::println("\n{}", failures); |
||||
} |
||||
} |
||||
|
||||
static Barcode readMultiple(const std::vector<fs::path>& imgPaths, BarcodeFormat format) |
||||
{ |
||||
Barcodes allBarcodes; |
||||
for (const auto& imgPath : imgPaths) { |
||||
auto barcodes = ReadBarcodes(ImageLoader::load(imgPath), |
||||
ReaderOptions().formats(format).tryDownscale(false)); |
||||
allBarcodes.insert(allBarcodes.end(), barcodes.begin(), barcodes.end()); |
||||
} |
||||
|
||||
return MergeStructuredAppendSequence(allBarcodes); |
||||
} |
||||
|
||||
static void doRunStructuredAppendTest(const fs::path& directory, BarcodeFormat format, int totalTests, |
||||
const std::vector<TestCase>& tests) |
||||
{ |
||||
auto imgPaths = getImagesInDirectory(directory); |
||||
auto folderName = directory.stem(); |
||||
|
||||
std::map<fs::path, std::vector<fs::path>> imageGroups; |
||||
for (const auto& imgPath : imgPaths) { |
||||
std::string fn = imgPath.filename().string(); |
||||
auto p = fn.find_last_of('-'); |
||||
imageGroups[imgPath.parent_path() / fn.substr(0, p)].push_back(imgPath); |
||||
} |
||||
|
||||
if (Size(imageGroups) != totalTests) |
||||
std::println("TEST {} => Expected number of tests: {}, got: {} => FAILED", folderName.string(), totalTests, |
||||
imageGroups.size()); |
||||
|
||||
for (auto& test : tests) { |
||||
std::print("{:20} @ {:3}, {:3}", folderName.string(), test.rotation, Size(imgPaths)); |
||||
auto tc = test.tc[0]; |
||||
auto startTime = std::chrono::steady_clock::now(); |
||||
|
||||
for (const auto& [testPath, testImgPaths] : imageGroups) { |
||||
auto barcode = readMultiple(testImgPaths, format); |
||||
if (barcode.isValid()) { |
||||
auto error = checkResult(testPath, format, barcode); |
||||
if (!error.empty()) |
||||
tc.misReadFiles[testPath] = error; |
||||
} else { |
||||
tc.notDetectedFiles.insert(testPath); |
||||
} |
||||
} |
||||
|
||||
auto failures = printPositiveTestStats(Size(imageGroups), tc); |
||||
std::println(" | time: {:3} ms", timeSince(startTime)); |
||||
if (!failures.empty()) |
||||
std::println("\n{}", failures); |
||||
} |
||||
} |
||||
|
||||
int runBlackBoxTests(const fs::path& testPathPrefix, const std::set<std::string>& includedTests) |
||||
{ |
||||
auto hasTest = [&includedTests](const fs::path& dir) { |
||||
auto stem = dir.stem().string(); |
||||
return includedTests.empty() || Contains(includedTests, stem) || |
||||
Contains(includedTests, stem.substr(0, stem.size() - 2)); |
||||
}; |
||||
|
||||
auto runTests = [&](std::string_view directory, BarcodeFormat format, int total, |
||||
const std::vector<TestCase>& tests, const ReaderOptions& opts = ReaderOptions()) { |
||||
if (hasTest(directory) && format & (BarcodeFormat::AllReadable | BarcodeFormat::None)) |
||||
doRunTests(testPathPrefix / directory, format, total, tests, opts); |
||||
}; |
||||
|
||||
auto runStructuredAppendTest = [&](std::string_view directory, BarcodeFormat format, int total, |
||||
const std::vector<TestCase>& tests) { |
||||
if (hasTest(directory) && format & (BarcodeFormat::AllReadable | BarcodeFormat::None)) |
||||
doRunStructuredAppendTest(testPathPrefix / directory, format, total, tests); |
||||
}; |
||||
|
||||
try |
||||
{ |
||||
auto startTime = std::chrono::steady_clock::now(); |
||||
|
||||
using enum BarcodeFormat; |
||||
// clang-format off
|
||||
|
||||
// Expected failures:
|
||||
// abc-inverted.png (fast) - fast does not try inverted
|
||||
// az-thick.png (pure)
|
||||
runTests("aztec-1", Aztec, 32, { |
||||
{ 31, 32, 0 }, |
||||
{ 31, 32, 90 }, |
||||
{ 31, 32, 180 }, |
||||
{ 31, 32, 270 }, |
||||
{ 30, 0, pure }, |
||||
}); |
||||
|
||||
runTests("aztec-2", Aztec, 16, { |
||||
{ 16, 16, 0 }, |
||||
{ 16, 16, 90 }, |
||||
{ 16, 16, 180 }, |
||||
{ 16, 16, 270 }, |
||||
}); |
||||
|
||||
runTests("datamatrix-1", DataMatrix, 30, { |
||||
{ 30, 30, 0 }, |
||||
{ 0, 28, 90 }, |
||||
{ 0, 28, 180 }, |
||||
{ 0, 28, 270 }, |
||||
{ 28, 0, pure }, |
||||
}); |
||||
|
||||
runTests("datamatrix-2", DataMatrix, 13, { |
||||
{ 13, 13, 0 }, |
||||
{ 0, 13, 90 }, |
||||
{ 0, 13, 180 }, |
||||
{ 0, 13, 270 }, |
||||
}); |
||||
|
||||
runTests("datamatrix-3", DataMatrix, 28, { |
||||
{ 26, 27, 0 }, |
||||
{ 0, 27, 90 }, |
||||
{ 0, 27, 180 }, |
||||
{ 0, 27, 270 }, |
||||
}); |
||||
|
||||
runTests("datamatrix-4", DataMatrix, 21, { |
||||
{ 21, 21, 0 }, |
||||
{ 0, 21, 90 }, |
||||
{ 0, 21, 180 }, |
||||
{ 0, 21, 270 }, |
||||
{ 19, 0, pure }, |
||||
}); |
||||
|
||||
runTests("dxfilmedge-1", DXFilmEdge, 3, { |
||||
{ 1, 3, 0 }, |
||||
{ 0, 3, 180 }, |
||||
}); |
||||
|
||||
runTests("codabar-1", Codabar, 11, { |
||||
{ 11, 11, 0 }, |
||||
{ 11, 11, 180 }, |
||||
}); |
||||
|
||||
runTests("codabar-2", Codabar, 4, { |
||||
{ 3, 4, 0 }, |
||||
{ 3, 4, 180 }, |
||||
}); |
||||
|
||||
runTests("code39-1", Code39, 6, { |
||||
{ 6, 6, 0 }, |
||||
{ 6, 6, 180 }, |
||||
}); |
||||
|
||||
runTests("code39-2", Code39Ext, 3, { |
||||
{ 3, 3, 0 }, |
||||
{ 3, 3, 180 }, |
||||
}); |
||||
|
||||
runTests("code39-3", Code39, 5, { |
||||
{ 5, 5, 0 }, |
||||
{ 5, 5, 180 }, |
||||
}); |
||||
|
||||
runTests("code93-1", Code93, 3, { |
||||
{ 3, 3, 0 }, |
||||
{ 3, 3, 180 }, |
||||
}); |
||||
|
||||
runTests("code128-1", Code128, 6, { |
||||
{ 6, 6, 0 }, |
||||
{ 6, 6, 180 }, |
||||
}); |
||||
|
||||
runTests("code128-2", Code128, 14, { |
||||
{ 13, 14, 0 }, |
||||
{ 13, 14, 180 }, |
||||
}); |
||||
|
||||
runTests("code128-3", Code128, 2, { |
||||
{ 2, 2, 0 }, |
||||
{ 2, 2, 180 }, |
||||
}); |
||||
|
||||
runTests("ean8-1", EAN8, 9, { |
||||
{ 9, 9, 0 }, |
||||
{ 9, 9, 180 }, |
||||
{ 8, 0, pure }, |
||||
}); |
||||
|
||||
runTests("ean13-1", EAN13, 16, { |
||||
{ 10, 14, 0 }, |
||||
{ 11, 14, 180 }, |
||||
}); |
||||
|
||||
runTests("ean13-2", EAN13, 16, { |
||||
{ 1, 6, 0 }, |
||||
{ 1, 6, 180 }, |
||||
}); |
||||
|
||||
runTests("ean13-3", EAN13, 8, { |
||||
{ 8, 8, 0 }, |
||||
{ 8, 8, 180 }, |
||||
}); |
||||
|
||||
runTests("ean13-4", EAN13, 20, { |
||||
{ 8, 12, 0 }, |
||||
{ 7, 12, 180 }, |
||||
}); |
||||
|
||||
runTests("ean13-extension-1", EAN13, 5, { |
||||
{ 4, 5, 0 }, |
||||
{ 4, 5, 180 }, |
||||
}, ReaderOptions().eanAddOnSymbol(EanAddOnSymbol::Require)); |
||||
|
||||
runTests("itf-1", ITF, 14, { |
||||
{ 13, 14, 0 }, |
||||
{ 13, 14, 180 }, |
||||
}); |
||||
|
||||
runTests("itf-2", ITF, 6, { |
||||
{ 6, 6, 0 }, |
||||
{ 6, 6, 180 }, |
||||
}); |
||||
|
||||
runTests("telepen-1", Telepen, 4, { |
||||
{ 4, 4, 0 }, |
||||
{ 4, 4, 180 }, |
||||
}); |
||||
|
||||
runTests("upca-1", UPCA, 5, { |
||||
{ 4, 5, 0 }, |
||||
{ 4, 5, 180 }, |
||||
}, ReaderOptions().formats(UPCA)); |
||||
|
||||
runTests("upca-2", UPCA, 26, { |
||||
{ 10, 16, 0 }, |
||||
{ 12, 16, 180 }, |
||||
}, ReaderOptions().formats(UPCA)); |
||||
|
||||
runTests("upca-3", UPCA, 6, { |
||||
{ 3, 6, 0 }, |
||||
{ 3, 6, 180 }, |
||||
}, ReaderOptions().formats(UPCA)); |
||||
|
||||
runTests("upca-4", UPCA, 13, { |
||||
{ 5, 9, 0 }, |
||||
{ 6, 9, 180 }, |
||||
}, ReaderOptions().formats(UPCA)); |
||||
|
||||
runTests("upca-5", UPCA, 20, { |
||||
{ 7, 9, 0 }, |
||||
{ 7, 9, 180 }, |
||||
}, ReaderOptions().formats(UPCA)); |
||||
|
||||
runTests("upca-extension-1", UPCA, 6, { |
||||
{ 4, 5, 0 }, |
||||
{ 4, 5, 180 }, |
||||
}, ReaderOptions().eanAddOnSymbol(EanAddOnSymbol::Require).formats(UPCA)); |
||||
|
||||
runTests("upce-1", UPCE, 3, { |
||||
{ 3, 3, 0 }, |
||||
{ 3, 3, 180 }, |
||||
{ 3, 0, pure }, |
||||
}); |
||||
|
||||
runTests("upce-2", UPCE, 15, { |
||||
{ 9, 10, 0 }, |
||||
{ 9, 10, 180 }, |
||||
}); |
||||
|
||||
runTests("upce-3", UPCE, 11, { |
||||
{ 5, 7, 0 }, |
||||
{ 5, 7, 180 }, |
||||
}); |
||||
|
||||
runTests("rss14-1", DataBarOmni, 13, { |
||||
{ 11, 11, 0 }, |
||||
{ 11, 11, 180 }, |
||||
}); |
||||
|
||||
runTests("rss14-2", DataBarStk, 7, { |
||||
{ 5, 6, 0 }, |
||||
{ 6, 6, 180 }, |
||||
}); |
||||
|
||||
runTests("rssexpanded-1", DataBarExp, 37, { |
||||
{ 37, 37, 0 }, |
||||
{ 37, 37, 180 }, |
||||
{ 37, 0, pure }, |
||||
}); |
||||
|
||||
runTests("rssexpanded-2", DataBarExp, 7, { |
||||
{ 5, 7, 0 }, |
||||
{ 5, 7, 180 }, |
||||
}); |
||||
|
||||
runTests("rssexpanded-3", DataBarExp, 118, { |
||||
{ 118, 118, 0 }, |
||||
{ 118, 118, 180 }, |
||||
{ 118, 0, pure }, |
||||
}); |
||||
|
||||
runTests("rssexpandedstacked-1", DataBarExpStk, 62, { |
||||
{ 52, 62, 0 }, |
||||
{ 52, 62, 180 }, |
||||
{ 57, 0, pure }, |
||||
}); |
||||
|
||||
runTests("rssexpandedstacked-2", DataBarExpStk, 2, { |
||||
{ 2, 2, 0 }, |
||||
{ 2, 2, 180 }, |
||||
}); |
||||
|
||||
runTests("databarltd-1", DataBarLtd, 2, { |
||||
{ 2, 2, 0 }, |
||||
{ 2, 2, 180 }, |
||||
{ 1, 0, pure }, |
||||
}); |
||||
|
||||
runTests("maxicode-1", MaxiCode, 9, { |
||||
{ 9, 9, 0 }, |
||||
}); |
||||
|
||||
runTests("maxicode-2", MaxiCode, 4, { |
||||
{ 0, 0, 0 }, |
||||
}); |
||||
|
||||
runTests("qrcode-1", QRCode, 8, { |
||||
{ 8, 8, 0 }, |
||||
{ 8, 8, 90 }, |
||||
{ 8, 8, 180 }, |
||||
{ 8, 8, 270 }, |
||||
}); |
||||
|
||||
runTests("qrcode-2", QRCode, 54, { |
||||
{ 48, 51, 0 }, |
||||
{ 49, 52, 90 }, |
||||
{ 48, 51, 180 }, |
||||
{ 48, 51, 270 }, |
||||
{ 23, 1, pure }, // the misread is the 'outer' symbol in 16.png
|
||||
}); |
||||
|
||||
runTests("qrcode-3", QRCode, 18, { |
||||
{ 18, 18, 0 }, |
||||
{ 18, 18, 90 }, |
||||
{ 18, 18, 180 }, |
||||
{ 18, 18, 270 }, |
||||
}); |
||||
|
||||
runTests("qrcode-4", QRCode, 24, { |
||||
{ 15, 15, 0 }, |
||||
{ 15, 15, 90 }, |
||||
{ 15, 15, 180 }, |
||||
{ 15, 15, 270 }, |
||||
}); |
||||
|
||||
runTests("qrcode-5", QRCode, 16, { |
||||
{ 16, 16, 0 }, |
||||
{ 16, 16, 90 }, |
||||
{ 16, 16, 180 }, |
||||
{ 16, 16, 270 }, |
||||
{ 4, 0, pure }, |
||||
}); |
||||
|
||||
runTests("qrcode-6", QRCode, 15, { |
||||
{ 15, 15, 0 }, |
||||
{ 15, 15, 90 }, |
||||
{ 15, 15, 180 }, |
||||
{ 15, 15, 270 }, |
||||
}); |
||||
|
||||
runStructuredAppendTest("qrcode-7", QRCode, 1, { |
||||
{ 1, 1, 0 }, |
||||
}); |
||||
|
||||
runTests("microqrcode-1", MicroQRCode, 16, { |
||||
{ 15, 15, 0 }, |
||||
{ 14, 14, 90 }, |
||||
{ 14, 14, 180 }, // ughs: 1 result is platform/compiler dependent (e.g. -march=core2 vs. haswell)
|
||||
{ 15, 15, 270 }, |
||||
{ 9, 0, pure }, |
||||
}); |
||||
|
||||
runTests("rmqrcode-1", RMQRCode, 3, { |
||||
{ 2, 3, 0 }, |
||||
{ 2, 3, 90 }, |
||||
{ 2, 3, 180 }, |
||||
{ 2, 3, 270 }, |
||||
{ 2, 2, pure }, |
||||
}); |
||||
|
||||
runTests("pdf417-1", PDF417, 17, { |
||||
{ 16, 17, 0 }, |
||||
{ 1, 17, 90 }, |
||||
{ 16, 17, 180 }, |
||||
{ 1, 17, 270 }, |
||||
{ 16, 0, pure }, |
||||
}); |
||||
|
||||
runTests("pdf417-2", PDF417, 25, { |
||||
{ 25, 25, 0 }, |
||||
{ 0, 25, 90 }, |
||||
{ 25, 25, 180 }, |
||||
{ 0, 25, 270 }, |
||||
}); |
||||
|
||||
runTests("pdf417-3", PDF417, 16, { |
||||
{ 16, 16, 0 }, |
||||
{ 0, 16, 90 }, |
||||
{ 16, 16, 180 }, |
||||
{ 0, 16, 270 }, |
||||
{ 7, 0, pure }, |
||||
}); |
||||
|
||||
runStructuredAppendTest("pdf417-4", PDF417, 3, { |
||||
{ 3, 3, 0 }, |
||||
}); |
||||
|
||||
runTests("micropdf417-1", MicroPDF417, 5, { |
||||
{ 4, 4, 0 }, |
||||
{ 0, 4, 90 }, |
||||
{ 4, 4, 180 }, |
||||
{ 0, 4, 270 }, |
||||
{ 1, 0, pure }, |
||||
}); |
||||
|
||||
runTests("falsepositives-1", None, 21, { |
||||
{ 0, 0, 0, 0, 0 }, |
||||
{ 0, 0, 0, 0, 90 }, |
||||
{ 0, 0, 0, 0, 180 }, |
||||
{ 0, 0, 0, 0, 270 }, |
||||
{ 0, 0, pure }, |
||||
}); |
||||
|
||||
runTests("falsepositives-2", None, 25, { |
||||
{ 0, 0, 0, 0, 0 }, |
||||
{ 0, 0, 0, 0, 90 }, |
||||
{ 0, 0, 0, 0, 180 }, |
||||
{ 0, 0, 0, 0, 270 }, |
||||
{ 0, 0, pure }, |
||||
}); |
||||
// clang-format on
|
||||
|
||||
int totalTime = timeSince(startTime); |
||||
int decodeTime = totalTime - totalImageLoadTime; |
||||
std::println("load time: {} ms.", totalImageLoadTime); |
||||
std::println("decode time: {} ms.", decodeTime); |
||||
std::println("total time: {} ms.", totalTime); |
||||
if (failed) |
||||
std::println("WARNING: {} tests failed.", failed); |
||||
if (extra) |
||||
std::println("INFO: {} tests succeeded unexpectedly.", extra); |
||||
|
||||
return failed; |
||||
} |
||||
catch (const std::exception& e) { |
||||
std::println("{}", e.what()); |
||||
} |
||||
catch (...) { |
||||
std::println("Internal error"); |
||||
} |
||||
return -1; |
||||
} |
||||
|
||||
} // ZXing::Test
|
||||
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2016 Nu-book Inc. |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#pragma once |
||||
|
||||
#include <filesystem> |
||||
#include <set> |
||||
#include <string> |
||||
|
||||
namespace fs = std::filesystem; |
||||
|
||||
namespace ZXing::Test { |
||||
|
||||
int runBlackBoxTests(const fs::path& testPathPrefix, const std::set<std::string>& includedTests); |
||||
|
||||
} // ZXing::Test
|
||||
@ -0,0 +1,37 @@
|
||||
zxing_add_package_stb() |
||||
zxing_add_package(WebP WebP https://github.com/webmproject/libwebp.git v1.6.0) |
||||
|
||||
if (ZXING_READERS) |
||||
add_executable (ReaderTest |
||||
TestReaderMain.cpp |
||||
ImageLoader.h |
||||
ImageLoader.cpp |
||||
BlackboxTestRunner.h |
||||
BlackboxTestRunner.cpp |
||||
) |
||||
|
||||
target_link_libraries(ReaderTest |
||||
ZXing::ZXing stb::stb WebP::webp |
||||
$<$<AND:$<CXX_COMPILER_ID:GNU>,$<VERSION_LESS:$<CXX_COMPILER_VERSION>,9.0>>:stdc++fs> |
||||
) |
||||
|
||||
if (WIN32) |
||||
add_custom_command(TARGET ReaderTest POST_BUILD |
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different |
||||
$<TARGET_RUNTIME_DLLS:ReaderTest> |
||||
$<TARGET_FILE_DIR:ReaderTest> |
||||
COMMAND_EXPAND_LISTS |
||||
VERBATIM |
||||
) |
||||
endif() |
||||
|
||||
add_test(NAME ReaderTest COMMAND ReaderTest ${CMAKE_CURRENT_SOURCE_DIR}/../samples) |
||||
endif() |
||||
|
||||
if (ZXING_WRITERS) |
||||
add_executable (WriterTest TestWriterMain.cpp) |
||||
|
||||
target_link_libraries (WriterTest ZXing::ZXing stb::stb) |
||||
|
||||
add_test(NAME WriterTest COMMAND WriterTest) |
||||
endif() |
||||
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2016 Nu-book Inc. |
||||
* Copyright 2019 Axel Waggersauser. |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "ImageLoader.h" |
||||
|
||||
#include "BinaryBitmap.h" |
||||
#include "ImageView.h" |
||||
|
||||
#include <array> |
||||
#include <cctype> |
||||
#include <cstdlib> |
||||
#include <fstream> |
||||
#include <iterator> |
||||
#include <map> |
||||
#include <memory> |
||||
#include <mutex> |
||||
#include <stdexcept> |
||||
#include <vector> |
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION |
||||
#include <stb_image.h> |
||||
|
||||
#include <webp/decode.h> |
||||
|
||||
namespace ZXing::Test { |
||||
|
||||
class STBImage : public ImageView |
||||
{ |
||||
std::unique_ptr<uint8_t[], void (*)(void*)> _memory = {nullptr, nullptr}; |
||||
|
||||
public: |
||||
void load(const fs::path& imgPath) |
||||
{ |
||||
int width = 0, height = 0, channels = 0; |
||||
int rowStride = 0; |
||||
|
||||
if (imgPath.extension() == ".webp") { |
||||
std::ifstream file(imgPath, std::ios::binary); |
||||
if (!file) |
||||
throw std::runtime_error("Failed to read image: " + imgPath.string() + " (failed to open file)"); |
||||
|
||||
std::vector<uint8_t> compressedData(std::istreambuf_iterator<char>(file), {}); |
||||
if (compressedData.empty()) |
||||
throw std::runtime_error("Failed to read image: " + imgPath.string() + " (empty file)"); |
||||
|
||||
#if 0 |
||||
// directly decoding to YUV is faster and makes more sense but a couple of tests fail with this, so we decode to RGB and convert to Lum instead
|
||||
channels = 1; |
||||
uint8_t *u = nullptr, *v = nullptr; |
||||
int uvStride = 0; |
||||
_memory = std::unique_ptr<uint8_t[], void (*)(void*)>( |
||||
WebPDecodeYUV(compressedData.data(), compressedData.size(), &width, &height, &u, &v, &rowStride, &uvStride), WebPFree); |
||||
#else |
||||
channels = 3; |
||||
_memory = std::unique_ptr<uint8_t[], void (*)(void*)>( |
||||
WebPDecodeRGB(compressedData.data(), compressedData.size(), &width, &height), WebPFree); |
||||
#endif |
||||
if (_memory == nullptr) |
||||
throw std::runtime_error("Failed to read image: " + imgPath.string() + " (WebP decode failed)"); |
||||
|
||||
} else { |
||||
_memory = std::unique_ptr<uint8_t[], void (*)(void*)>(stbi_load(imgPath.string().c_str(), &width, &height, &channels, 0), stbi_image_free); |
||||
if (_memory == nullptr) |
||||
throw std::runtime_error("Failed to read image: " + imgPath.string() + " (" + stbi_failure_reason() + ")"); |
||||
rowStride = width * channels; |
||||
} |
||||
|
||||
auto ImageFormatFromChannels = std::array{ImageFormat::None, ImageFormat::Lum, ImageFormat::LumA, ImageFormat::RGB, ImageFormat::RGBA}; |
||||
ImageView::operator=({_memory.get(), width, height, ImageFormatFromChannels.at(channels), rowStride}); |
||||
|
||||
// preconvert from RGB -> Lum to do this only once instead of for each rotation
|
||||
if (_format == ImageFormat::RGB) { |
||||
auto* img = _memory.get(); |
||||
for (int i = 0; i < width * height; ++i) |
||||
img[i] = RGBToLum(img[3 * i + 0], img[3 * i + 1], img[3 * i + 2]); |
||||
ImageView::operator=({_memory.get(), width, height, ImageFormat::Lum}); |
||||
} |
||||
} |
||||
|
||||
operator bool() const { return _data; } |
||||
}; |
||||
|
||||
std::map<fs::path, STBImage> cache; |
||||
std::mutex cacheMutex; |
||||
|
||||
void ImageLoader::clearCache() |
||||
{ |
||||
std::scoped_lock lock(cacheMutex); |
||||
cache.clear(); |
||||
} |
||||
|
||||
const ImageView& ImageLoader::load(const fs::path& imgPath) |
||||
{ |
||||
std::scoped_lock lock(cacheMutex); |
||||
auto& binImg = cache[imgPath]; |
||||
if (!binImg) |
||||
binImg.load(imgPath); |
||||
|
||||
return binImg; |
||||
} |
||||
|
||||
} // namespace ZXing::Test
|
||||
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2016 Nu-book Inc. |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#pragma once |
||||
|
||||
#include "ImageView.h" |
||||
|
||||
#include <filesystem> |
||||
namespace fs = std::filesystem; |
||||
|
||||
namespace ZXing::Test::ImageLoader { |
||||
|
||||
void clearCache(); |
||||
const ImageView& load(const fs::path& imgPath); |
||||
|
||||
} // namespace ZXing::Test::ImageLoader
|
||||
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2016 Nu-book Inc. |
||||
* Copyright 2017 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "BlackboxTestRunner.h" |
||||
#include "ImageLoader.h" |
||||
#include "ReadBarcode.h" |
||||
#include "StdPrint.h" |
||||
#include "ZXAlgorithms.h" |
||||
|
||||
#include <cstdlib> |
||||
#include <cstring> |
||||
#include <fstream> |
||||
#include <set> |
||||
|
||||
using namespace ZXing; |
||||
using namespace ZXing::Test; |
||||
|
||||
int getEnv(const char* name, int fallback = 0) |
||||
{ |
||||
auto var = getenv(name); |
||||
return var ? std::stoi(var) : fallback; |
||||
} |
||||
|
||||
int main(int argc, char** argv) |
||||
{ |
||||
if (argc <= 1) { |
||||
std::println("Usage: {} <test_path_prefix>", argv[0]); |
||||
return 0; |
||||
} |
||||
|
||||
fs::path pathPrefix = argv[1]; |
||||
|
||||
if (Contains({".png", ".jpg", ".pgm", ".gif", ".webp", ".jxl"}, pathPrefix.extension())) { |
||||
auto opts = ReaderOptions().tryHarder(!getEnv("FAST", false)).tryRotate(true).isPure(getEnv("IS_PURE")); |
||||
if (getenv("FORMATS")) |
||||
opts.formats(BarcodeFormatsFromString(getenv("FORMATS"))); |
||||
int rotation = getEnv("ROTATION"); |
||||
|
||||
for (int i = 1; i < argc; ++i) { |
||||
Barcode barcode = ReadBarcode(ImageLoader::load(argv[i]).rotated(rotation), opts); |
||||
std::print("{}: ", argv[i]); |
||||
if (barcode.isValid()) |
||||
std::println("{}: {}", ToString(barcode.format()), barcode.text()); |
||||
else |
||||
std::println("FAILED"); |
||||
if (barcode.isValid() && getenv("WRITE_TEXT")) { |
||||
std::ofstream f(fs::path(argv[i]).replace_extension(".txt")); |
||||
f << barcode.text(); |
||||
} |
||||
} |
||||
return 0; |
||||
} else { |
||||
std::set<std::string> includedTests; |
||||
for (int i = 2; i < argc; ++i) { |
||||
if (std::strlen(argv[i]) > 2 && argv[i][0] == '-' && argv[i][1] == 't') { |
||||
includedTests.insert(argv[i] + 2); |
||||
} |
||||
} |
||||
|
||||
return runBlackBoxTests(pathPrefix, includedTests); |
||||
} |
||||
} |
||||
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2016 Nu-book Inc. |
||||
* Copyright 2019 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "ZXingCpp.h" |
||||
|
||||
#include <vector> |
||||
|
||||
using namespace ZXing; |
||||
using namespace std::literals; |
||||
using enum BarcodeFormat; |
||||
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION |
||||
#include <stb_image_write.h> |
||||
|
||||
void savePng(ImageView iv, BarcodeFormat format) |
||||
{ |
||||
stbi_write_png((ToString(format) + ".png"s).c_str(), iv.width(), iv.height(), iv.pixStride(), iv.data(), iv.rowStride()); |
||||
} |
||||
|
||||
int main() |
||||
{ |
||||
std::string text = "zxing-cpp"; |
||||
for (auto format : BarcodeFormats::list(AllMatrix)) |
||||
if (format & AllCreatable && format != AztecRune) |
||||
savePng(CreateBarcodeFromText(text, format).symbol(), format); |
||||
|
||||
text = "012345678901234567890123456789"; |
||||
using FormatSpecs = std::vector<std::pair<BarcodeFormat, size_t>>; |
||||
for (const auto& [format, length] : FormatSpecs({ |
||||
// {Codabar, 0}, // needs to start with A, B, C or D
|
||||
{Code39, 0}, |
||||
{Code93, 0}, |
||||
{Code128, 0}, |
||||
{EAN8, 7}, |
||||
{EAN13, 12}, |
||||
{ITF, 0}, |
||||
{UPCA, 11}, |
||||
{UPCE, 7} |
||||
})) |
||||
{ |
||||
auto input = length > 0 ? text.substr(0, length) : text; |
||||
if (format & AllCreatable) |
||||
savePng(WriteBarcodeToImage(CreateBarcodeFromText(input, format)), format); |
||||
} |
||||
} |
||||
@ -0,0 +1,34 @@
|
||||
cmake_minimum_required (VERSION 3.16) |
||||
|
||||
|
||||
# -fsanitize=fuzzer only available in clang / llvm |
||||
# cmake -DCMAKE_CXX_COMPILER=/path/to/your/clang++ |
||||
|
||||
project (ZXingFuzz) |
||||
|
||||
set (CMAKE_CXX_STANDARD 20) |
||||
set (CMAKE_CXX_STANDARD_REQUIRED ON) |
||||
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -march=native -fsanitize=address,undefined,fuzzer") |
||||
|
||||
set (ZXING_WRITERS BOTH) |
||||
set (ZXING_READERS ON) |
||||
|
||||
add_definitions (-DZXING_BUILD_FOR_TEST) |
||||
add_subdirectory (${CMAKE_CURRENT_SOURCE_DIR}/../../core ${CMAKE_BINARY_DIR}/ZXing) |
||||
|
||||
set (TESTS |
||||
DBEDecoder |
||||
DMEncoder |
||||
ReadLinear |
||||
ReadMatrix |
||||
DecodeMatrix |
||||
) |
||||
|
||||
foreach (test ${TESTS}) |
||||
set (name "fuzz${test}") |
||||
add_executable (${name} "${name}.cpp") |
||||
target_compile_options (${name} PRIVATE -DZXING_INTERNAL) |
||||
target_link_libraries (${name} ZXing::ZXing) |
||||
endforeach() |
||||
|
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2021 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
#include "BitArray.h" |
||||
#include "Error.h" |
||||
#include "oned/ODDataBarExpandedBitDecoder.h" |
||||
|
||||
using namespace ZXing; |
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) |
||||
{ |
||||
if (size < 2) |
||||
return 0; |
||||
|
||||
BitArray bits; |
||||
for (size_t i = 0; i < size; ++i) |
||||
bits.appendBits(data[i], 8); |
||||
|
||||
try { |
||||
OneD::DataBar::DecodeExpandedBits(bits); |
||||
} catch (std::out_of_range) { |
||||
} catch (Error) { |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2021 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
#include "ByteArray.h" |
||||
#include "datamatrix/DMHighLevelEncoder.h" |
||||
|
||||
using namespace ZXing; |
||||
using namespace ZXing::DataMatrix; |
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) |
||||
{ |
||||
std::wstring txt(data, data + size); |
||||
try { |
||||
Encode(txt); |
||||
} catch (...) { |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2023 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "BitArray.h" |
||||
#include "ByteArray.h" |
||||
#include "DecoderResult.h" |
||||
#include "qrcode/QRErrorCorrectionLevel.h" |
||||
#include "qrcode/QRVersion.h" |
||||
#include "pdf417/PDFDecoder.h" |
||||
|
||||
#include <algorithm> |
||||
#include <cstddef> |
||||
#include <cstdint> |
||||
|
||||
using namespace ZXing; |
||||
|
||||
namespace ZXing::Aztec { |
||||
DecoderResult Decode(const BitArray& bits); |
||||
} |
||||
|
||||
namespace ZXing::DataMatrix::DecodedBitStreamParser { |
||||
DecoderResult Decode(ByteArray&& bytes, const bool isDMRE); |
||||
} |
||||
|
||||
namespace ZXing::QRCode { |
||||
DecoderResult DecodeBitStream(ByteArray&& bytes, const Version& version, ErrorCorrectionLevel ecLevel); |
||||
} |
||||
|
||||
void az(const uint8_t* data, size_t size) |
||||
{ |
||||
BitArray bits; |
||||
for (size_t i = 1; i < size - 1; ++i) |
||||
bits.appendBits(data[i], 8); |
||||
|
||||
bits.appendBits(data[size - 1], (data[0] & 0x7) + 1); |
||||
|
||||
Aztec::Decode(bits); |
||||
} |
||||
|
||||
void dm(const uint8_t* data, size_t size) |
||||
{ |
||||
ByteArray ba; |
||||
ba.insert(ba.begin(), data, data + size); |
||||
try { |
||||
DataMatrix::DecodedBitStreamParser::Decode(std::move(ba), false); |
||||
} catch (...) { |
||||
} |
||||
} |
||||
|
||||
void qr(const uint8_t* data, size_t size) |
||||
{ |
||||
auto version = QRCode::Version::Model2(std::clamp(data[0] & 0x3F, 1, 40)); |
||||
auto ecLevel = QRCode::ECLevelFromBits(data[0] >> 6); |
||||
|
||||
ByteArray ba; |
||||
ba.insert(ba.begin(), data, data + size); |
||||
|
||||
QRCode::DecodeBitStream(std::move(ba), *version, ecLevel); |
||||
} |
||||
|
||||
void pd(const uint8_t* data, size_t size) |
||||
{ |
||||
auto codewords = std::vector<int>(size / 2); |
||||
auto u16 = reinterpret_cast<const uint16_t*>(data); |
||||
|
||||
for (int i = 0; i < static_cast<int>(codewords.size()); ++i) |
||||
codewords[i] = u16[i] % 929; |
||||
|
||||
codewords[0] = static_cast<int>(codewords.size()); |
||||
|
||||
Pdf417::Decode(codewords); |
||||
} |
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) |
||||
{ |
||||
if (size < 3) |
||||
return 0; |
||||
|
||||
az(data, size); |
||||
dm(data, size); |
||||
qr(data, size); |
||||
pd(data, size); |
||||
|
||||
return 0; |
||||
} |
||||
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2021 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
#include "oned/ODMultiUPCEANReader.h" |
||||
#include "oned/ODCode39Reader.h" |
||||
#include "oned/ODCode93Reader.h" |
||||
#include "oned/ODCode128Reader.h" |
||||
#include "oned/ODDataBarReader.h" |
||||
#include "oned/ODDataBarExpandedReader.h" |
||||
#include "oned/ODDXFilmEdgeReader.h" |
||||
#include "oned/ODITFReader.h" |
||||
#include "oned/ODCodabarReader.h" |
||||
#include "ReaderOptions.h" |
||||
#include "Barcode.h" |
||||
|
||||
using namespace ZXing; |
||||
using namespace ZXing::OneD; |
||||
|
||||
static std::vector<std::unique_ptr<RowReader>> readers; |
||||
|
||||
bool init() |
||||
{ |
||||
static auto opts = ReaderOptions().returnErrors(true); |
||||
readers.emplace_back(new MultiUPCEANReader(opts)); |
||||
readers.emplace_back(new Code39Reader(opts)); |
||||
readers.emplace_back(new Code93Reader(opts)); |
||||
readers.emplace_back(new Code128Reader(opts)); |
||||
readers.emplace_back(new ITFReader(opts)); |
||||
readers.emplace_back(new CodabarReader(opts)); |
||||
readers.emplace_back(new DataBarReader(opts)); |
||||
readers.emplace_back(new DataBarExpandedReader(opts)); |
||||
readers.emplace_back(new DXFilmEdgeReader(opts)); |
||||
return true; |
||||
} |
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) |
||||
{ |
||||
if (size < 1) |
||||
return 0; |
||||
|
||||
static bool inited [[maybe_unused]] = init(); |
||||
std::vector<std::unique_ptr<RowReader::DecodingState>> decodingState(readers.size()); |
||||
|
||||
PatternRow row(size * 2 + 1); |
||||
for (size_t i = 0; i < size; ++i){ |
||||
auto v = data[i]; |
||||
row[i * 2 + 0] = (v & 0xf) + 1; |
||||
row[i * 2 + 1] = (v >> 4) + 1; |
||||
} |
||||
row.back() = 0; |
||||
|
||||
for (size_t r = 0; r < readers.size(); ++r) { |
||||
PatternView next(row); |
||||
while (next.isValid()) { |
||||
readers[r]->decodePattern(0, next, decodingState[r]); |
||||
// make sure we make progress and we start the next try on a bar
|
||||
next.shift(2 - (next.index() % 2)); |
||||
next.extend(); |
||||
} |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2023 Axel Waggershauser |
||||
*/ |
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "ReadBarcode.h" |
||||
|
||||
#include <algorithm> |
||||
#include <cassert> |
||||
#include <csignal> |
||||
#include <cstddef> |
||||
#include <cstdint> |
||||
#include <cstdlib> |
||||
|
||||
using namespace ZXing; |
||||
|
||||
uint64_t Expand(uint8_t b) |
||||
{ |
||||
uint64_t shift = 0x0000040810204081ul; // bits set: 0, 7, 14, 21, 28, 35, 42
|
||||
uint64_t mask = 0x0001010101010101ul; // bits set: 0, 8, 16, 24, 32, 40, 48
|
||||
return ((uint64_t)(b & 127) * shift & mask) | (uint64_t)(b & 128) << 49; |
||||
} |
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) |
||||
{ |
||||
if (size < 3) |
||||
return 0; |
||||
|
||||
static auto opts = ReaderOptions() |
||||
.formats(BarcodeFormat::AllMatrix) |
||||
.binarizer(Binarizer::BoolCast) |
||||
.returnErrors(true) |
||||
.tryInvert(false) |
||||
.tryRotate(false); |
||||
int ratio = data[0] + 1; |
||||
int nBits = (size - 1) * 8; |
||||
int width = std::clamp(nBits * ratio / 256, 1, nBits); |
||||
int height = std::clamp(nBits / width, 1, nBits); |
||||
|
||||
assert(width * height <= nBits); |
||||
|
||||
std::vector<uint8_t> buffer(nBits); |
||||
for (size_t i = 1; i < size; ++i) |
||||
*reinterpret_cast<uint64_t*>(&buffer[(i - 1) * 8]) = Expand(data[i]); |
||||
|
||||
#ifdef PRINT_DEBUG |
||||
printf("s: %zu, r: %d, n: %d -> %d x %d\n", size, ratio, nBits, width, height); |
||||
#endif |
||||
|
||||
auto image = ImageView(buffer.data(), width, height, ImageFormat::Lum); |
||||
auto res = ReadBarcodes(image, opts); |
||||
|
||||
#ifdef PRINT_DEBUG |
||||
for (const auto& r : res) |
||||
printf("%s: %s / %s\n", ToString(r.format()).c_str(), r.text().c_str(), ToString(r.error()).c_str()); |
||||
#endif |
||||
|
||||
static int detectedSybols = 0; |
||||
detectedSybols += static_cast<int>(res.size()); |
||||
if (!res.empty() && detectedSybols % 100 == 0) |
||||
printf("detected barcode symbols: %d\n", detectedSybols); |
||||
|
||||
return 0; |
||||
} |
||||
@ -0,0 +1,17 @@
|
||||
cmake_minimum_required(VERSION 3.16) |
||||
|
||||
project(ZXingInstallTest LANGUAGES CXX) |
||||
|
||||
set(CMAKE_CXX_STANDARD 17) # make sure the public API is still c++17 compatible |
||||
|
||||
find_package(ZXing CONFIG REQUIRED) |
||||
|
||||
add_executable(InstallTest main.cpp) |
||||
|
||||
target_link_libraries(InstallTest PRIVATE ZXing::ZXing) |
||||
|
||||
if (ZXING_WRITERS MATCHES "BOTH|OLD") |
||||
add_executable(InstallTestOLD mainOLD.cpp) |
||||
|
||||
target_link_libraries(InstallTestOLD PRIVATE ZXing::ZXing) |
||||
endif() |
||||
@ -0,0 +1,22 @@
|
||||
#include "ZXing/ZXingCpp.h" |
||||
|
||||
#include <iostream> |
||||
|
||||
int main(int argc, char** argv) |
||||
{ |
||||
using namespace ZXing; |
||||
|
||||
auto copts = CreatorOptions(BarcodeFormat::QRCode, "eclevel=L"); |
||||
auto barcode = CreateBarcodeFromText("Test", copts); |
||||
auto wopts = WriterOptions().scale(2); |
||||
auto img = WriteBarcodeToImage(barcode, wopts); |
||||
|
||||
auto iv = ImageView(img.data(), img.width(), img.height(), img.format()); |
||||
auto ropts = ReaderOptions().formats(BarcodeFormat::AllLinear); |
||||
auto barcodes = ReadBarcodes(iv, ropts); |
||||
|
||||
for (const auto& b : barcodes) |
||||
std::cout << ToString(b.format()) << ": " << b.text() << "\n"; |
||||
|
||||
return 0; |
||||
} |
||||
@ -0,0 +1,20 @@
|
||||
#include "ZXing/MultiFormatWriter.h" |
||||
#include "ZXing/BitMatrix.h" |
||||
#include "ZXing/BitMatrixIO.h" |
||||
#include "ZXing/ZXVersion.h" |
||||
|
||||
#include <iostream> |
||||
|
||||
int main(int argc, char** argv) |
||||
{ |
||||
using namespace ZXing; |
||||
|
||||
std::cout << "zxing-cpp version " << ZXING_VERSION_STR << "\n"; |
||||
|
||||
MultiFormatWriter writer(BarcodeFormat::QRCode); |
||||
BitMatrix bitmatrix = writer.encode("Hello, World!", 200, 200); |
||||
auto matrix = ToMatrix<int8_t>(bitmatrix); |
||||
std::cout << ToString(bitmatrix) << "\n"; |
||||
|
||||
return 0; |
||||
} |
||||
@ -0,0 +1,34 @@
|
||||
set(install_prefix "${PROJECT_BINARY_DIR}/InstallTest-prefix") |
||||
set(consumer_build "${PROJECT_BINARY_DIR}/InstallTest-build") |
||||
|
||||
# 1. Install zxing-cpp |
||||
execute_process( |
||||
COMMAND "${CMAKE_COMMAND}" --install "${PROJECT_BINARY_DIR}" --prefix "${install_prefix}" |
||||
RESULT_VARIABLE install_result |
||||
) |
||||
if(NOT install_result EQUAL 0) |
||||
message(FATAL_ERROR "Install step failed") |
||||
endif() |
||||
|
||||
# 2. Configure consumer project |
||||
execute_process( |
||||
COMMAND |
||||
"${CMAKE_COMMAND}" |
||||
-S "${SOURCE_DIR}" |
||||
-B "${consumer_build}" |
||||
-DCMAKE_PREFIX_PATH=${install_prefix} |
||||
-DZXING_WRITERS=${ZXING_WRITERS} |
||||
RESULT_VARIABLE configure_result |
||||
) |
||||
if(NOT configure_result EQUAL 0) |
||||
message(FATAL_ERROR "Consumer configure failed") |
||||
endif() |
||||
|
||||
# 3. Build consumer project |
||||
execute_process( |
||||
COMMAND "${CMAKE_COMMAND}" --build "${consumer_build}" |
||||
RESULT_VARIABLE build_result |
||||
) |
||||
if(NOT build_result EQUAL 0) |
||||
message(FATAL_ERROR "Consumer build failed") |
||||
endif() |
||||
@ -0,0 +1 @@
|
||||
* binary |
||||
@ -0,0 +1 @@
|
||||
This is an example Aztec symbol for Wikipedia. |
||||
|
After Width: | Height: | Size: 164 B |
|
After Width: | Height: | Size: 170 B |
@ -0,0 +1 @@
|
||||
I have the best words. |
||||
|
After Width: | Height: | Size: 192 B |
@ -0,0 +1 @@
|
||||
symbologyIdentifier=]z0 |
||||
|
After Width: | Height: | Size: 456 B |
@ -0,0 +1 @@
|
||||
This is an example Aztec symbol for Wikipedia. |
||||
|
After Width: | Height: | Size: 196 B |
@ -0,0 +1 @@
|
||||
Histórico |
||||
|
After Width: | Height: | Size: 527 B |
@ -0,0 +1 @@
|
||||
Históóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóórico |
||||
|
After Width: | Height: | Size: 195 B |
@ -0,0 +1,4 @@
|
||||
symbologyIdentifier=]z6 |
||||
sequenceSize=7 |
||||
sequenceIndex=3 |
||||
sequenceId=Z1.txt |
||||
@ -0,0 +1 @@
|
||||
3456 |
||||
|
After Width: | Height: | Size: 226 B |