Files
VX5_KRO_GearPump/scene/ItemCommon.cpp
T
2026-06-25 11:04:34 +08:00

579 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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>
#include <QDebug>
/**
* @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 ItemUtils::generateTxtFromDateItemModel(const DateItemModel &model, const QDateTime &time)
{
QString result = model.datetimePattern;
if (!time.isValid()) {
qWarning() << "generateTxtFromDateItemModel: invalid QDateTime passed";
return result;
}
// 应用偏移量
QDateTime applyDateTime = time;
if(model.monthsOffset != 0) {
applyDateTime = time.addMonths(model.monthsOffset);
}
else if(model.daysOffset != 0) {
applyDateTime = time.addDays(model.daysOffset);
}
// 使用正则表达式匹配并替换各种格式化模式
// 匹配四位数年份 yyyy
int i = result.indexOf("yyyy");
if(i != -1) {
int year = applyDateTime.date().year();
QString yearStr = model.yearsMap.contains(year) ? model.yearsMap[year] : QString::number(year);
result.replace(i, 4, yearStr);
}
// 匹配两位数年份 yy
i = result.indexOf("YY");
if(i != -1) {
QString yearStr = QString::number(applyDateTime.date().year()).right(2);
result.replace(i, 2, yearStr);
}
// 匹配两位数月份 MM
i = result.indexOf("MM");
if(i != -1) {
int monthIdx = applyDateTime.date().month() - 1;
if (monthIdx >= 0 && monthIdx < model.monthsMap.size()) {
result.replace(i, 2, model.monthsMap[monthIdx]);
} else {
qWarning() << "generateTxtFromDateItemModel: monthsMap index out of range:" << monthIdx;
result.replace(i, 2, QString::number(applyDateTime.date().month()).rightJustified(2, '0'));
}
}
// 匹配两位数日期 dd
i = result.indexOf("dd");
if(i != -1) {
int dayIdx = applyDateTime.date().day() - 1;
if (dayIdx >= 0 && dayIdx < model.daysMap.size()) {
result.replace(i, 2, model.daysMap[dayIdx]);
} else {
qWarning() << "generateTxtFromDateItemModel: daysMap index out of range:" << dayIdx;
result.replace(i, 2, QString::number(applyDateTime.date().day()).rightJustified(2, '0'));
}
}
// 匹配两位数小时 HH
i = result.indexOf("HH");
if(i != -1) {
int hourIdx = applyDateTime.time().hour();
if (hourIdx >= 0 && hourIdx < model.hoursMap.size()) {
result.replace(i, 2, model.hoursMap[hourIdx]);
} else {
qWarning() << "generateTxtFromDateItemModel: hoursMap index out of range:" << hourIdx;
result.replace(i, 2, QString::number(applyDateTime.time().hour()).rightJustified(2, '0'));
}
}
// 匹配两位数分钟 mm
QRegExp minutePattern("mm");
if (result.contains(minutePattern)) {
int minute = applyDateTime.time().minute();
result.replace(minutePattern, QString::number(minute).rightJustified(2, '0'));
}
// 匹配两位数秒钟 ss
QRegExp secondPattern("ss");
if (result.contains(secondPattern)) {
int second = applyDateTime.time().second();
result.replace(secondPattern, QString::number(second).rightJustified(2, '0'));
}
// 格式化周几 w
i = result.indexOf("w");
if (i != -1) {
int weekIdx = applyDateTime.date().dayOfWeek() - 1;
if (weekIdx >= 0 && weekIdx < model.weeksMap.size()) {
result.replace(i, 1, model.weeksMap[weekIdx]);
} else {
qWarning() << "generateTxtFromDateItemModel: weeksMap index out of range:" << weekIdx;
result.replace(i, 1, QString::number(applyDateTime.date().dayOfWeek()));
}
}
// 格式化年中周数 c
QRegExp weekNumberPattern("c");
if (result.contains(weekNumberPattern)) {
result.replace(weekNumberPattern, QString::number(applyDateTime.date().weekNumber()));
}
return result;
}
QString ItemUtils::generateTxtFromBarcodeViewModel(const BarcodeViewModel &vm, const std::array<int, 2> counterValues, const QDateTime &time)
{
QString txt = vm.contentPattern;
auto pos = txt.indexOf("{t}");
if(pos != -1) {
txt = txt.replace(pos, 3, generateTxtFromDateItemModel(vm.dateViewModel, time));
}
pos = txt.indexOf("{c}");
if(pos != -1) {
auto cntValue = vm.counterViewModel.counterType == COUNTER_TYPE_1 ? counterValues[0] : counterValues[1];
auto valueStr = QString("%1").arg(cntValue, 8, 10, QLatin1Char('0'));
valueStr = valueStr.right(vm.counterViewModel.digitalNum);
txt = 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 ItemUtils::generateBarcodeImage(const QString &content, BarcodeType type, const QVariant &format)
{
if (content.isEmpty()) {
return QImage();
}
if (type == BARCODE_TYPE_QRCODE || type == BARCODE_TYPE_DMCODE) {
// 优先级:完整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 ItemUtils::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);
}