From b4e123b046d67e6910c0d3ddcafd1381d293116c Mon Sep 17 00:00:00 2001 From: zhangjiarui Date: Mon, 19 Oct 2026 15:44:14 +0800 Subject: [PATCH] fix(print): run PDF print conversion asynchronously with a wait tip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pdftocairo was executed synchronously on the UI thread before the print preview was shown, so large PDFs blocked the print dialog from popping up. Now a persistent lightweight tip is shown immediately, the conversion runs in a background process, and the preview opens once it finishes. If the conversion fails or pdftocairo is unavailable, it falls back to the original PDF so printing still proceeds (same behaviour as before). The conversion is performed on every print. The conversion is still performed for every PDF (not gated by the Linearized flag) so that bug-348017 (non-linearized PDFs with non-embedded CJK fonts printing blank/garbled) is not regressed. Log: 打印 PDF 时异步执行 pdftocairo 转化并先显示等待提示,避免大文件阻塞打印界面 Bug: https://pms.uniontech.com/bug-view-379067.html --- reader/uiframe/DocSheet.cpp | 213 ++++++++++++++++++++++++++++++------ reader/uiframe/DocSheet.h | 46 ++++++++ 2 files changed, 225 insertions(+), 34 deletions(-) diff --git a/reader/uiframe/DocSheet.cpp b/reader/uiframe/DocSheet.cpp index 31ef6c014..892f919c1 100644 --- a/reader/uiframe/DocSheet.cpp +++ b/reader/uiframe/DocSheet.cpp @@ -24,6 +24,9 @@ #include #include #include +#include +#include +#include #include #include @@ -40,6 +43,8 @@ #include #include #include +#include +#include #include #include @@ -52,6 +57,24 @@ namespace { constexpr double kXpsLogicalDpi = 96.0; constexpr int kFallbackPrintDpi = 300; constexpr int kMaxPrintPixelsPerSide = 10000; + +// pdftocairo 转化超时时间:大文件耗时较长,保留 5 分钟预算 +constexpr int kPdfPrintConvertTimeoutMs = 5 * 60 * 1000; + +// 校验 pdftocairo 输出 PDF 是否结构完整:pdftocairo 偶发 exit code 为 0 +// 但输出缺少 xref/trailer/%%EOF(如 Typora/Electron 生成的 PDF), +// 这种文件交给 CUPS 会导致 pdftopdf 报 "unable to find trailer dictionary" +bool isConvertedPdfValid(const QString &path) +{ + QFile outFile(path); + if (!outFile.open(QIODevice::ReadOnly)) + return false; + + outFile.seek(qMax(0LL, outFile.size() - 1024)); + const QByteArray tail = outFile.readAll(); + outFile.close(); + return tail.contains("startxref") && tail.contains("%%EOF"); +} } QReadWriteLock DocSheet::g_lock; @@ -153,6 +176,14 @@ DocSheet::~DocSheet() setAlive(false); + // 关闭打印转化等待提示(其父在主窗口,不会随本 sheet 一起销毁) + closePrintWaitTip(); + m_printConvertInProgress = false; + if (m_printConvertTimer) + m_printConvertTimer->stop(); + if (m_printConvertProcess && m_printConvertProcess->state() != QProcess::NotRunning) + m_printConvertProcess->kill(); + // 排空引用本 sheet 的所有待处理渲染任务(仅优化,避免为将死文档做无用渲染); // 正确性由任务中捕获的renderer共享引用与uuid校验保证,不再依赖析构与线程间的时序 PageRenderThread::clearAllTasksForSheet(this); @@ -1995,49 +2026,163 @@ void DocSheet::onPopPrintDialog() return; } +#if (DTK_VERSION >= DTK_VERSION_CHECK(5,4,10,0)) + // PDF 需要额外处理:用 pdftocairo 转换为嵌入矢量 PDF,解决打印乱码/模糊。 + // 注意:不能按 Linearized 判定跳过,否则会回归 bug-348017(复现件为非线化+字体未嵌入)。 + if (Dr::PDF == fileType()) { + if (m_printConvertInProgress) { + qCWarning(appLog) << "Print conversion already in progress, ignore repeated request"; + return; + } + + m_printConvertedPath = convertedFileDir() + "/pdftocairoPrint.pdf"; + + // 每次打印都重新转化,保证与当前文件内容一致 + // (文档可能在会话内被保存/加注解改写,复用会产生陈旧内容) + m_printConvertInProgress = true; + showPrintWaitTip(); + // 延迟到下一轮事件循环启动耗时处理,保证提示先绘制、在处理开始前可见 + QTimer::singleShot(0, this, [this]() { startPdfPrintConversion(); }); + return; + } + + if (Dr::DOCX == fileType()) { + // DOCX 打开时已生成 temp.pdf,直接按文件路径打印 + showPrintPreviewWithPath(openedFilePath()); + return; + } + + // 其它类型无需额外处理,保持原流程 + showPrintPreviewWithPath(QString()); +#else + // 旧版 DTK 走图片打印,无额外处理 + showPrintPreviewWithPath(QString()); +#endif + qCDebug(appLog) << "onPopPrintDialog end"; +} + +void DocSheet::showPrintPreviewWithPath(const QString &printFromPath) +{ DPrintPreviewDialog *preview = new DPrintPreviewDialog(this); preview->setAttribute(Qt::WA_DeleteOnClose); #if (DTK_VERSION >= DTK_VERSION_CHECK(5,4,10,0)) - //文件打印 preview->setAsynPreview(pageCount()); preview->setDocName(QFileInfo(filePath()).fileName()); - if (Dr::DOCX == fileType()) {//旧版本和最新版本使用新接口,PDF文件直接传,解决打印模糊问题 - preview->setPrintFromPath(openedFilePath()); - } - - //pdf若是Linearized类型的,需要另存为Normal类型,然后打印 - if (Dr::PDF == fileType()) { - QString pdfPath = filePath(); - // 使用 pdftocairo 转换为嵌入矢量pdf,解决打印乱码和模糊问题 - QString tmpPdfPath = convertedFileDir() + "/pdftocairoPrint.pdf"; - QProcess process; - process.start("pdftocairo", QStringList() << "-pdf" << pdfPath << tmpPdfPath); - bool converted = process.waitForFinished(5 * 60 * 1000) && process.exitCode() == 0; - if (converted) { - // pdftocairo 有时 exit code 为 0 但输出 PDF 结构不完整,验证关键标记是否存在 - QFile outFile(tmpPdfPath); - if (outFile.open(QIODevice::ReadOnly)) { - outFile.seek(qMax(0LL, outFile.size() - 1024)); - const QByteArray tail = outFile.readAll(); - outFile.close(); - if (!tail.contains("startxref") || !tail.contains("%%EOF")) - converted = false; - } else { - converted = false; - } - } - if (!converted) - qCWarning(appLog) << "pdftocairo conversion failed or produced invalid PDF, using original"; - preview->setPrintFromPath(converted ? tmpPdfPath : pdfPath); - } - connect(preview, QOverload &>::of(&DPrintPreviewDialog::paintRequested), this, QOverload &>::of(&DocSheet::onPrintRequested)); + if (!printFromPath.isEmpty()) + preview->setPrintFromPath(printFromPath); + connect(preview, QOverload &>::of(&DPrintPreviewDialog::paintRequested), + this, QOverload &>::of(&DocSheet::onPrintRequested)); #else - //图片打印 - connect(preview, QOverload::of(&DPrintPreviewDialog::paintRequested), this, QOverload::of(&DocSheet::onPrintRequested)); + Q_UNUSED(printFromPath); + connect(preview, QOverload::of(&DPrintPreviewDialog::paintRequested), + this, QOverload::of(&DocSheet::onPrintRequested)); #endif preview->open(); - qCDebug(appLog) << "onPopPrintDialog end"; +} + +void DocSheet::startPdfPrintConversion() +{ + if (!m_printConvertInProgress) + return; + + if (!m_printConvertProcess) { + m_printConvertProcess = new QProcess(this); + connect(m_printConvertProcess, QOverload::of(&QProcess::finished), + this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + if (m_printConvertTimer) + m_printConvertTimer->stop(); + const bool success = (exitStatus == QProcess::NormalExit) + && exitCode == 0 + && isConvertedPdfValid(m_printConvertedPath); + finishPdfPrintConversion(success); + }); + connect(m_printConvertProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) { + if (error != QProcess::FailedToStart) + return; + qCWarning(appLog) << "pdftocairo failed to start:" << m_printConvertProcess->errorString(); + if (m_printConvertTimer) + m_printConvertTimer->stop(); + finishPdfPrintConversion(false); + }); + } + + if (!m_printConvertTimer) { + m_printConvertTimer = new QTimer(this); + m_printConvertTimer->setSingleShot(true); + connect(m_printConvertTimer, &QTimer::timeout, this, [this]() { + qCWarning(appLog) << "pdftocairo conversion timeout, fall back to original PDF"; + if (m_printConvertProcess && m_printConvertProcess->state() != QProcess::NotRunning) + m_printConvertProcess->kill(); + finishPdfPrintConversion(false); + }); + } + + qCDebug(appLog) << "start pdftocairo conversion:" << filePath() << "->" << m_printConvertedPath; + m_printConvertTimer->start(kPdfPrintConvertTimeoutMs); + m_printConvertProcess->start("pdftocairo", QStringList() << "-pdf" << filePath() << m_printConvertedPath); +} + +void DocSheet::finishPdfPrintConversion(bool success) +{ + if (!m_printConvertInProgress) + return; + + m_printConvertInProgress = false; + closePrintWaitTip(); + + if (success) { + qCDebug(appLog) << "pdftocairo conversion succeeded:" << m_printConvertedPath; + showPrintPreviewWithPath(m_printConvertedPath); + } else { + // 降级:pdftocairo 失败或不存在时回退原始 PDF,保证打印仍可用(与改动前行为一致) + qCWarning(appLog) << "pdftocairo conversion failed, fall back to original PDF for printing"; + showPrintPreviewWithPath(filePath()); + } +} + +QWidget *DocSheet::printTipParent() const +{ + QWidget *parent = nullptr; + for (QWidget *pwgt : qApp->topLevelWidgets()) { + if (QMainWindow *mwd = dynamic_cast(pwgt)) + parent = mwd; + } + if (!parent) + parent = qApp->activeWindow(); + if (!parent) + parent = const_cast(this); + return parent; +} + +void DocSheet::showPrintWaitTip() +{ + if (m_printWaitTip) + return; + + QWidget *parent = printTipParent(); + if (!parent) + return; + + DFloatingMessage *tip = new DFloatingMessage(DFloatingMessage::ResidentType); + tip->setAttribute(Qt::WA_DeleteOnClose); + tip->setIcon(QIcon::fromTheme(QString("dr_") + "ok")); + tip->setMessage(tr("Preparing the content for printing. This may take a while. Please wait…")); + // 等待期间不允许用户提前关闭提示(同时满足"不提供取消操作") + if (DDialogCloseButton *closeBtn = tip->findChild()) + closeBtn->hide(); + + m_printWaitTip = tip; + DMessageManager::instance()->sendMessage(parent, tip); + qCDebug(appLog) << "print wait tip shown"; +} + +void DocSheet::closePrintWaitTip() +{ + if (m_printWaitTip) { + m_printWaitTip->close(); + m_printWaitTip = nullptr; + } } void DocSheet::onPopInfoDialog() diff --git a/reader/uiframe/DocSheet.h b/reader/uiframe/DocSheet.h index f239607d5..125a90a2b 100644 --- a/reader/uiframe/DocSheet.h +++ b/reader/uiframe/DocSheet.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -57,6 +58,7 @@ struct SheetOperation { DWIDGET_BEGIN_NAMESPACE class DPrinter; +class DFloatingMessage; DWIDGET_END_NAMESPACE class QTemporaryDir; @@ -953,6 +955,40 @@ private slots: private: QSize calculatePrintTargetSize(int pageIndex, const QPrinter &printer, const QRectF &pageRect) const; + + /** + * @brief showPrintPreviewWithPath + * 创建并打开打印预览对话框,printFromPath 非空时按文件路径打印 + * @param printFromPath 交给打印系统的文件路径(空表示走 paintRequested 光栅打印) + */ + void showPrintPreviewWithPath(const QString &printFromPath); + + /** + * @brief startPdfPrintConversion + * 异步执行 pdftocairo 把 PDF 转换为嵌入矢量 PDF(不阻塞 UI) + */ + void startPdfPrintConversion(); + + /** + * @brief finishPdfPrintConversion + * 转化结束统一收口:关闭等待提示,成功用转化文件开预览,失败回退原始 PDF 继续打印 + * @param success 是否转化成功且输出合法 + */ + void finishPdfPrintConversion(bool success); + + /** + * @brief showPrintWaitTip / closePrintWaitTip + * 打印处理期间常驻的轻量提示(复用 DFloatingMessage Tooltip 样式,隐藏关闭按钮) + */ + void showPrintWaitTip(); + void closePrintWaitTip(); + + /** + * @brief printTipParent + * 获取打印相关轻量提示的承载窗口(主窗口优先) + */ + QWidget *printTipParent() const; + /** * @brief setAlive * 设置当前sheet是否存活 @@ -1016,6 +1052,16 @@ private slots: // 缓存的内容哈希,避免自动保存时反复读盘计算 QString m_cachedContentHash; + // 打印转化等待提示(常驻轻量 Tooltip,转化完成/失败后关闭) + QPointer m_printWaitTip; + // 后台 pdftocairo 转化进程与超时定时器(懒创建并复用,父对象为本 sheet) + QProcess *m_printConvertProcess = nullptr; + QTimer *m_printConvertTimer = nullptr; + // 转化输出文件路径(每次打印重新生成,随会话临时目录清理) + QString m_printConvertedPath; + // 是否有 PDF 转化正在进行(重入保护) + bool m_printConvertInProgress = false; + public: QProcess *m_process = nullptr; //当前调用的命令的进程地址