新二维码/条形码编辑逻辑

This commit is contained in:
2026-05-06 10:34:48 +08:00
parent a06c926162
commit a9e99f4425
76 changed files with 12679 additions and 12744 deletions
+454 -4
View File
@@ -1,4 +1,312 @@
#include "ItemCommon.h"
#include "DlgQrcodeFormat.h"
#include "DlgBarcodeFormat.h"
#include <QtQREnCode/qrencode.h>
#include "Matrix/dm200.h"
#include "BarCode/barcode.h"
#include <QPainter>
#include <cstring>
/**
* @brief 将libqrencode的QRcode结构渲染成QImage
*
* 避免依赖文件系统或PostScript输出,直接在内存中生成可用于UI显示或打印的图像。
* 使用2D像素数组而非向量,支持任意缩放因子。
*/
static QImage renderQRCode(const QRcode *qrcode, int moduleSize = 4, QRgb foreground = 0xff000000, QRgb background = 0xffffffff)
{
if (!qrcode || qrcode->width <= 0) {
return QImage();
}
int margin = 1;
int imageSize = (qrcode->width + margin * 2) * moduleSize;
QImage image(imageSize, imageSize, QImage::Format_ARGB32);
image.fill(background);
const unsigned char *data = qrcode->data;
for (int y = 0; y < qrcode->width; ++y) {
for (int x = 0; x < qrcode->width; ++x) {
bool moduleOn = (data[y * qrcode->width + x] & 0x01) != 0;
QRgb color = moduleOn ? foreground : background;
int px = (margin + x) * moduleSize;
int py = (margin + y) * moduleSize;
for (int dy = 0; dy < moduleSize; ++dy) {
for (int dx = 0; dx < moduleSize; ++dx) {
image.setPixel(px + dx, py + dy, color);
}
}
}
}
return image;
}
/**
* @brief 将DataMatrix位图网格渲染成QImage
*
* iec16022ecc200返回原始位数组,需转换为可视化图像以供UI或打印使用。
* 与renderQRCode采用相同的像素化策略,确保两种编码方式的输出一致性。
*/
static QImage renderDataMatrix(const unsigned char *grid, int cols, int rows, int moduleSize = 1, QRgb foreground = 0xff000000, QRgb background = 0xffffffff)
{
if (!grid || cols <= 0 || rows <= 0) {
return QImage();
}
int width = cols * moduleSize;
int height = rows * moduleSize;
QImage image(width, height, QImage::Format_ARGB32);
image.fill(background);
for (int y = 0; y < rows; ++y) {
for (int x = 0; x < cols; ++x) {
bool bit = (grid[y * cols + x] & 0x01) != 0;
QRgb color = bit ? foreground : background;
int px = x * moduleSize;
int py = y * moduleSize;
for (int dy = 0; dy < moduleSize; ++dy) {
for (int dx = 0; dx < moduleSize; ++dx) {
image.setPixel(px + dx, py + dy, color);
}
}
}
}
return image;
}
static QRecLevel qrCodeLevelFromEnum(QRCodeLevel level)
{
switch (level) {
case QR_CODE_LEVEL_M: return QR_ECLEVEL_M;
case QR_CODE_LEVEL_Q: return QR_ECLEVEL_Q;
case QR_CODE_LEVEL_H: return QR_ECLEVEL_H;
default: return QR_ECLEVEL_L;
}
}
/**
* @brief 根据格式配置生成二维码或DataMatrix图像
*
* 支持两种编码方式:
* - DataMatrix:适合小容量数据和有限空间环境(如工业标签)
* - QR Code:高容量、高可靠性,广泛应用于商业和物流
*
* 直接在内存中渲染,避免对dlgQrCode或外部库的依赖。
* 颜色反转机制(INK_TYPE_WHITE)用于支持黑底白码的特殊打印场景。
*/
static QImage generateQRCodeImage(const QString &content, const DlgQrcodeFormatVM &vm)
{
if (content.isEmpty()) {
return QImage();
}
QRecLevel level = qrCodeLevelFromEnum(vm.level);
// 适配黑底白码场景(逆色):打印机某些模式下需要反转前景/背景色
QRgb foreground = vm.inkType == INK_TYPE_WHITE ? 0x00ffffff : 0xff000000;
QRgb background = vm.inkType == INK_TYPE_WHITE ? 0xff000000 : 0x00ffffff;
QByteArray bytes = content.toUtf8();
// DataMatrix:调用者指定尺寸约束(sizeType),库返回实际分配的网格
if (vm.type == QR_CODE_TYPE_DATA_MATRIX) {
short cols = 0;
short rows = 0;
short sizeType = static_cast<short>(vm.size);
// iec16022ecc200会根据sizeType调整输出尺寸,支持灵活的条码宽高比
unsigned char *grid = iec16022ecc200(&cols, &rows, reinterpret_cast<unsigned char *>(bytes.data()), &sizeType);
if (!grid || cols <= 0 || rows <= 0) {
if (grid) {
free(grid);
}
return QImage();
}
QImage image = renderDataMatrix(grid, cols, rows, 1, foreground, background);
free(grid);
return image;
}
// QR Code:版本0表示自动选择版本(1-40),确保内容总是被编码
QRcode *qrcode = QRcode_encodeString(bytes.constData(), 0, level, QR_MODE_8, 1);
if (!qrcode) {
return QImage();
}
QImage image = renderQRCode(qrcode, 1, foreground, background);
QRcode_free(qrcode);
return image;
}
/**
* @brief 将Barcode_Item的编码数据(partial)渲染成QImage
*
* partial字段存储压缩的条形码宽度数据(高4位=空白宽度,低3位=黑条宽度),
* 需展开为像素数组。barWidth参数控制每个模块的物理宽度(用于放大)。
*
* 这种直接渲染方式避免了PostScript生成和文件I/O的开销。
*/
static QImage renderBarcodeImage(const Barcode_Item *bc, int barHeight = 12, int barWidth = 1, QRgb foreground = 0xff000000, QRgb background = 0xffffffff)
{
if (!bc || !bc->partial) {
return QImage();
}
// 约束:barHeight和barWidth必须为正值。默认值支持小尺寸标签的视觉清晰度
if (barHeight <= 0) {
barHeight = 12;
}
if (barWidth <= 0) {
barWidth = 1;
}
// xoffset=8提供左边距(4像素),用于对齐打印布局和改善美观度
int xoffset = 8;
int len = static_cast<int>(strlen(bc->partial));
// 计算条形码总宽度:每字节的低4位表示条纹宽度单位数之和
int bcWidth1 = 0;
for (int i = 0; i < len; ++i) {
bcWidth1 += (bc->partial[i] & 0x0f);
}
// 最终宽度 = 左边距 + 条纹单位总数 * 每单位像素数
int imageWidth = (xoffset >> 1) + bcWidth1 * barWidth;
if (imageWidth <= 0 || barHeight <= 0) {
return QImage();
}
QImage image(imageWidth, barHeight, QImage::Format_ARGB32);
image.fill(background);
for (int y = 0; y < barHeight; ++y) {
int xpos = xoffset >> 1;
for (int n = 0; n < len; ++n) {
// partial数据编码方案:奇数索引=黑条,偶数索引=空白
// 低3位存储实际宽度,需乘以barWidth得到像素宽度
int run = (bc->partial[n] & 0x07) * barWidth;
if (n & 1) {
// 奇数索引:绘制黑条(前景色)
for (int k = 0; k < run; ++k) {
if (xpos >= 0 && xpos < imageWidth) {
image.setPixel(xpos, y, foreground);
}
++xpos;
}
} else {
// 偶数索引:跳过空白区域(已由背景色填充)
xpos += run;
}
}
}
return image;
}
static int barcodeTypeFromString(const QString &type)
{
QString t = type.trimmed().toUpper();
if (t == "BARCODE_128") return BARCODE_128;
if (t == "BARCODE_93") return BARCODE_93;
if (t == "BARCODE_ISBN") return BARCODE_ISBN;
if (t == "BARCODE_EAN") return BARCODE_EAN;
if (t == "BARCODE_UPC") return BARCODE_UPC;
if (t == "BARCODE_39") return BARCODE_39;
if (t == "BARCODE_CBR") return BARCODE_CBR;
if (t == "BARCODE_PLS") return BARCODE_PLS;
if (t == "BARCODE_MSI") return BARCODE_MSI;
return BARCODE_128;
}
static QString barcodeTypeNameFromIndex(int index)
{
switch (index) {
case 0: return QLatin1String("BARCODE_128");
case 1: return QLatin1String("BARCODE_93");
case 2: return QLatin1String("BARCODE_ISBN");
case 3: return QLatin1String("BARCODE_EAN");
case 4: return QLatin1String("BARCODE_UPC");
case 5: return QLatin1String("BARCODE_39");
case 6: return QLatin1String("BARCODE_CBR");
case 7: return QLatin1String("BARCODE_PLS");
case 8: return QLatin1String("BARCODE_MSI");
default: return QLatin1String("BARCODE_128");
}
}
static QImage generateQRCodeImage(const QString &content, int qrType)
{
DlgQrcodeFormatVM vm;
vm.type = qrType == 0 ? QR_CODE_TYPE_DATA_MATRIX : QR_CODE_TYPE_QRCODE;
return generateQRCodeImage(content, vm);
}
/**
* @brief 根据格式配置生成条形码图像
*
* 支持多种条形码标准(128、93、EAN等),直接渲染到QImage,无需文件操作。
* 避免对QBarCode类的依赖,实现自包含的编码流程。
*/
static QImage generateBarCodeImage(const QString &content, const DlgBarcodeFormatVM &vm)
{
if (content.isEmpty()) {
return QImage();
}
// UTF-8编码确保国际字符被正确处理,与打印驱动兼容
QByteArray bytes = content.toUtf8();
// Barcode_Create内部用strdup复制字符串,无需手动生命周期管理
struct Barcode_Item *bc = Barcode_Create(bytes.data());
if (!bc) {
return QImage();
}
int encodingFlag = barcodeTypeFromString(vm.type);
// BARCODE_OUT_PS: 预期PostScript格式输出(internal flag),BARCODE_OUT_NOHEADERS: 不生成PS头
int flags = encodingFlag | BARCODE_OUT_PS | BARCODE_OUT_NOHEADERS;
// 传递缩放倍数到库,影响条纹宽度和字体大小的计算(510/60为库内部单位转换)
int width = qMax(1, vm.scale) * 510;
int height = qMax(1, vm.scale) * 60;
if (Barcode_Position(bc, width, height, 0, 0, 0.0) < 0 || Barcode_Encode(bc, flags) < 0) {
Barcode_Delete(bc);
return QImage();
}
// 黑底白码支持(INK_TYPE_WHITE表示反向模式)
QRgb foreground = vm.inkType == INK_TYPE_WHITE ? 0x00ffffff : 0xff000000;
QRgb background = vm.inkType == INK_TYPE_WHITE ? 0xff000000 : 0x00ffffff;
QImage image = renderBarcodeImage(bc, vm.height, vm.width, foreground, background);
Barcode_Delete(bc);
// 可选文本叠加:某些应用场景(如物流标签)需要条码旁显示人类可读的内容
if (vm.txtVisible) {
QPainter painter(&image);
painter.setPen(foreground);
QFont font = painter.font();
// 字体大小自适应:防止文本溢出图像,同时保持可读性
font.setPixelSize(qMin(12, image.height()));
painter.setFont(font);
painter.drawText(image.rect().adjusted(1, 1, -1, -1), Qt::AlignBottom | Qt::AlignHCenter, content);
}
// 最终放大:scale>1时对整个图像进行像素放大(用于低DPI打印环保模式)
if (vm.scale > 1) {
image = image.scaled(image.width() * vm.scale, image.height() * vm.scale, Qt::KeepAspectRatio, Qt::FastTransformation);
}
return image;
}
static QImage generateBarCodeImage(const QString &content, int barCodeType)
{
DlgBarcodeFormatVM vm;
vm.type = barcodeTypeNameFromIndex(barCodeType);
return generateBarCodeImage(content, vm);
}
QString generateTxtFromDateItemModel(const DateItemModel &model, const QDateTime &time)
{
@@ -73,7 +381,7 @@ QString generateTxtFromDateItemModel(const DateItemModel &model, const QDateTime
if (result.contains(weekNumberPattern)) {
result.replace(weekNumberPattern, QString::number(applyDateTime.date().weekNumber()));
}
return result;
}
@@ -87,12 +395,154 @@ QString generateTxtFromBarcodeViewModel(const BarcodeViewModel &vm, const std::a
}
pos = vm.contentPattern.indexOf("{c}");
if(pos != 1) {
if(pos != -1) {
auto cntValue = vm.counterViewModel.counterType == COUNTER_TYPE_1 ? counterValues[0] : counterValues[1];
auto valueStr = QString("%1").arg(cntValue, 10, QChar('0'));;
valueStr.right(vm.counterViewModel.digitalNum);
valueStr = valueStr.right(vm.counterViewModel.digitalNum);
txt.replace(pos, 3, valueStr);
}
return txt;
}
}
/**
* @brief 统一的条码/二维码生成入口
*
* @details
* 目的:将二维码和条形码编码逻辑独立出来,避免对UI对话框类(dlgQrCode、QBarCode)的依赖,
* 实现在数据层直接进行编码,支持后端打印任务、异步处理等场景。
*
* 多态format参数设计的理由:
* - 支持便利调用(int):向后兼容现有代码路径,使用库默认配置
* - 支持细粒度控制(DlgQrcodeFormatVM/DlgBarcodeFormatVM):UI配置直通生成函数
* - 避免序列化开销:QVariant作为类型安全的通用容器
*
* @param content 已格式化的文本内容(由调用者负责模板替换如{t}、{c})
* @param type 条码类型(BARCODE_TYPE_QRCODE 或 BARCODE_TYPE_BARCODE)
* @param format 配置参数容器,支持以下类型:
* - BARCODE_TYPE_QRCODE: DlgQrcodeFormatVM(完整配置)或 int(0=DataMatrix, 1=QRCode,使用默认值)
* - BARCODE_TYPE_BARCODE: DlgBarcodeFormatVM(完整配置)或 int(条形码类型索引,使用默认值)
* @return QImage 生成的图像;失败返回空QImage
*/
QImage generateBarcodeImage(const QString &content, BarcodeType type, const QVariant &format)
{
if (content.isEmpty()) {
return QImage();
}
if (type == BARCODE_TYPE_QRCODE) {
// 优先级:完整VM配置 > 简化int配置 > 默认配置
// 原因:支持从UI直接传递结构体,同时保留向后兼容的快速路径
if (format.canConvert<DlgQrcodeFormatVM>()) {
DlgQrcodeFormatVM vm = format.value<DlgQrcodeFormatVM>();
return generateQRCodeImage(content, vm);
}
if (format.isValid() && format.canConvert<int>()) {
return generateQRCodeImage(content, format.toInt());
}
return generateQRCodeImage(content, static_cast<int>(QR_CODE_TYPE_QRCODE));
}
if (type == BARCODE_TYPE_BARCODE) {
if (format.canConvert<DlgBarcodeFormatVM>()) {
DlgBarcodeFormatVM vm = format.value<DlgBarcodeFormatVM>();
return generateBarCodeImage(content, vm);
}
if (format.isValid() && format.canConvert<int>()) {
return generateBarCodeImage(content, format.toInt());
}
return generateBarCodeImage(content, DlgBarcodeFormatVM());
}
return QImage();
}
/**
* @brief 将 QImage 中所有等于 bg 的边缘像素裁掉,仅保留有效内容的最小外接矩形
*
* 复刻自 PicTextItem 的 clipImage:用四个方向扫描确定 left/top/right/bottom 边界,
* 然后调用 QImage::copy 输出裁剪后的图像。这样字符串如 "aaa" 不会保留 "j" 才会用到的下行空白,
* 节省后续打印或合成时的画布空间。
*/
static QImage clipTxtImage(const QImage &img, QRgb bg)
{
if (img.isNull()) {
return img;
}
QRect clipRect;
for (int x = 0; x < img.width(); ++x) {
for (int y = 0; y < img.height(); ++y) {
if (img.pixel(x, y) != bg) {
clipRect.setLeft(x);
goto top;
}
}
}
// 整张图都是背景色,直接返回原图(无可裁剪内容)
return img;
top:
for (int y = 0; y < img.height(); ++y) {
for (int x = 0; x < img.width(); ++x) {
if (img.pixel(x, y) != bg) {
clipRect.setTop(y);
goto right;
}
}
}
right:
for (int x = img.width() - 1; x >= 0; --x) {
for (int y = 0; y < img.height(); ++y) {
if (img.pixel(x, y) != bg) {
clipRect.setRight(x + 1);
goto bottom;
}
}
}
bottom:
for (int y = img.height() - 1; y >= 0; --y) {
for (int x = 0; x < img.width(); ++x) {
if (img.pixel(x, y) != bg) {
clipRect.setBottom(y + 1);
goto end;
}
}
}
end:
return img.copy(clipRect);
}
QImage txtToImage(const QString &txt, const QFont &font, const QColor &fgColor, const QColor &bgColor)
{
if (txt.isEmpty()) {
return QImage();
}
// 用 QFontMetrics 计算给定字体下文本的水平推进宽度和单行高度,
// 这样初始画布刚好能容纳整段文本,避免后续裁剪时出现截断
QFontMetrics fm(font);
int width = fm.horizontalAdvance(txt);
int height = fm.height();
if (width <= 0 || height <= 0) {
return QImage();
}
QImage img(width, height, QImage::Format_ARGB32);
QRgb bgRgb = bgColor.rgba();
img.fill(bgRgb);
QPainter painter;
painter.begin(&img);
painter.setFont(font);
painter.setPen(fgColor);
painter.drawText(img.rect(), txt);
painter.end();
// 按背景色裁剪,使输出图像紧贴文字像素的最小包围盒(详见函数注释)
return clipTxtImage(img, bgRgb);
}