Change qr library to properly support utf8 characters
This commit is contained in:
+58
-36
@@ -1,66 +1,88 @@
|
||||
#include "QrUtils.h"
|
||||
|
||||
#include <Utf8.h>
|
||||
#include <qrcode.h>
|
||||
#include <qrcodegen.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#include "Logging.h"
|
||||
|
||||
void QrUtils::drawQrCode(const GfxRenderer& renderer, const Rect& bounds, const std::string& textPayload) {
|
||||
// Dynamically calculate the QR code version based on text length
|
||||
// Version 4 holds ~114 bytes, Version 10 ~395, Version 20 ~1066, up to 40
|
||||
// qrcode.h max version is 40.
|
||||
// Formula: approx version = size / 26 + 1 (very rough estimate, better to find best fit)
|
||||
size_t len = textPayload.length();
|
||||
namespace {
|
||||
|
||||
// Truncate to max QR capacity at a UTF-8 safe boundary to avoid splitting multi-byte sequences
|
||||
static constexpr size_t MAX_QR_CAPACITY = 2953; // Version 40, ECC_LOW, byte mode
|
||||
std::string truncated;
|
||||
const char* payload = textPayload.c_str();
|
||||
bool hasNonAscii(const uint8_t* data, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (data[i] > 0x7F) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool QrUtils::drawQrCode(const GfxRenderer& renderer, const Rect& bounds, const std::string& textPayload) {
|
||||
bool truncated = false;
|
||||
size_t len = textPayload.size();
|
||||
const char* text = textPayload.c_str();
|
||||
|
||||
// Truncate at a UTF-8 safe boundary if needed
|
||||
std::string truncatedStr;
|
||||
if (len > MAX_QR_CAPACITY) {
|
||||
len = utf8SafeTruncateBuffer(textPayload.c_str(), static_cast<int>(MAX_QR_CAPACITY));
|
||||
truncated = textPayload.substr(0, len);
|
||||
payload = truncated.c_str();
|
||||
len = utf8SafeTruncateBuffer(text, static_cast<int>(MAX_QR_CAPACITY));
|
||||
truncatedStr = textPayload.substr(0, len);
|
||||
text = truncatedStr.c_str();
|
||||
truncated = true;
|
||||
LOG_DBG("QR", "Truncated payload from %u to %u bytes", textPayload.size(), len);
|
||||
}
|
||||
|
||||
int version = 4;
|
||||
if (len > 114) version = 10;
|
||||
if (len > 395) version = 20;
|
||||
if (len > 1066) version = 30;
|
||||
if (len > 2110) version = 40;
|
||||
// Heap-allocate both buffers (each ~3918 bytes for version 40)
|
||||
constexpr size_t bufLen = qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX);
|
||||
auto qrcode = std::make_unique<uint8_t[]>(bufLen);
|
||||
auto tempBuf = std::make_unique<uint8_t[]>(bufLen);
|
||||
|
||||
// Make sure we have a large enough buffer on the heap to avoid blowing the stack
|
||||
uint32_t bufferSize = qrcode_getBufferSize(version);
|
||||
auto qrcodeBytes = std::make_unique<uint8_t[]>(bufferSize);
|
||||
bool ok = false;
|
||||
const auto* rawData = reinterpret_cast<const uint8_t*>(text);
|
||||
|
||||
QRCode qrcode;
|
||||
// Initialize the QR code. We use ECC_LOW for max capacity.
|
||||
int8_t res = qrcode_initText(&qrcode, qrcodeBytes.get(), version, ECC_LOW, payload);
|
||||
if (hasNonAscii(rawData, len)) {
|
||||
// Non-ASCII content: use ECI mode 26 (UTF-8) + byte segment via the low-level API
|
||||
// so scanners know the encoding rather than assuming ISO 8859-1.
|
||||
uint8_t eciBuf[4] = {};
|
||||
struct qrcodegen_Segment eciSeg = qrcodegen_makeEci(26, eciBuf);
|
||||
|
||||
if (res == 0) {
|
||||
// Determine the optimal pixel size.
|
||||
// Build byte segment — the segment data buffer can overlap with tempBuf
|
||||
const size_t segBufSize = qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_BYTE, len);
|
||||
auto segBuf = std::make_unique<uint8_t[]>(segBufSize);
|
||||
struct qrcodegen_Segment byteSeg = qrcodegen_makeBytes(rawData, len, segBuf.get());
|
||||
|
||||
struct qrcodegen_Segment segs[2] = {eciSeg, byteSeg};
|
||||
ok = qrcodegen_encodeSegmentsAdvanced(segs, 2, qrcodegen_Ecc_LOW, qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX,
|
||||
qrcodegen_Mask_AUTO, false, tempBuf.get(), qrcode.get());
|
||||
} else {
|
||||
// ASCII-only: let the library auto-select the optimal mode (numeric/alphanumeric/byte)
|
||||
ok = qrcodegen_encodeText(text, tempBuf.get(), qrcode.get(), qrcodegen_Ecc_LOW, qrcodegen_VERSION_MIN,
|
||||
qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, false);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
const int size = qrcodegen_getSize(qrcode.get());
|
||||
const int maxDim = std::min(bounds.width, bounds.height);
|
||||
|
||||
int px = maxDim / qrcode.size;
|
||||
int px = maxDim / size;
|
||||
if (px < 1) px = 1;
|
||||
|
||||
// Calculate centering X and Y
|
||||
const int qrDisplaySize = qrcode.size * px;
|
||||
const int qrDisplaySize = size * px;
|
||||
const int xOff = bounds.x + (bounds.width - qrDisplaySize) / 2;
|
||||
const int yOff = bounds.y + (bounds.height - qrDisplaySize) / 2;
|
||||
|
||||
// Draw the QR Code
|
||||
for (uint8_t cy = 0; cy < qrcode.size; cy++) {
|
||||
for (uint8_t cx = 0; cx < qrcode.size; cx++) {
|
||||
if (qrcode_getModule(&qrcode, cx, cy)) {
|
||||
for (int cy = 0; cy < size; cy++) {
|
||||
for (int cx = 0; cx < size; cx++) {
|
||||
if (qrcodegen_getModule(qrcode.get(), cx, cy)) {
|
||||
renderer.fillRect(xOff + px * cx, yOff + px * cy, px, px, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If it fails (e.g. text too large), log an error
|
||||
LOG_ERR("QR", "Text too large for QR Code version %d", version);
|
||||
LOG_ERR("QR", "Failed to encode QR code (%u bytes)", len);
|
||||
}
|
||||
|
||||
return truncated;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user