Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 179 additions & 34 deletions reader/uiframe/DocSheet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
#include "MainWindow.h"
#include "ddlog.h"

#include <DSpinner>

Check warning on line 24 in reader/uiframe/DocSheet.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <DSpinner> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <DPrintPreviewDialog>

Check warning on line 25 in reader/uiframe/DocSheet.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <DPrintPreviewDialog> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <DWidgetUtil>

Check warning on line 26 in reader/uiframe/DocSheet.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <DWidgetUtil> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <DFloatingMessage>

Check warning on line 27 in reader/uiframe/DocSheet.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <DFloatingMessage> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <DMessageManager>

Check warning on line 28 in reader/uiframe/DocSheet.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <DMessageManager> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <DDialogCloseButton>

Check warning on line 29 in reader/uiframe/DocSheet.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <DDialogCloseButton> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <QJsonDocument>

Check warning on line 31 in reader/uiframe/DocSheet.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QJsonDocument> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QJsonObject>

Check warning on line 32 in reader/uiframe/DocSheet.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QJsonObject> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QStackedWidget>
#include <QMimeData>
#include <QUuid>
Expand All @@ -37,9 +40,11 @@
#include <QDebug>
#include <QTemporaryDir>
#include <QProcess>
#include <QPrinter>

Check warning on line 43 in reader/uiframe/DocSheet.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QPrinter> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QtMath>

Check warning on line 44 in reader/uiframe/DocSheet.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtMath> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QGraphicsDropShadowEffect>
#include <QIcon>
#include <QTimer>

#include <signal.h>
#include <sys/types.h>
Expand All @@ -52,6 +57,24 @@
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;
Expand Down Expand Up @@ -153,6 +176,14 @@

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);
Expand Down Expand Up @@ -1995,49 +2026,163 @@
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<DPrinter *, const QVector<int> &>::of(&DPrintPreviewDialog::paintRequested), this, QOverload<DPrinter *, const QVector<int> &>::of(&DocSheet::onPrintRequested));
if (!printFromPath.isEmpty())
preview->setPrintFromPath(printFromPath);
connect(preview, QOverload<DPrinter *, const QVector<int> &>::of(&DPrintPreviewDialog::paintRequested),
this, QOverload<DPrinter *, const QVector<int> &>::of(&DocSheet::onPrintRequested));
#else
//图片打印
connect(preview, QOverload<DPrinter *>::of(&DPrintPreviewDialog::paintRequested), this, QOverload<DPrinter *>::of(&DocSheet::onPrintRequested));
Q_UNUSED(printFromPath);
connect(preview, QOverload<DPrinter *>::of(&DPrintPreviewDialog::paintRequested),
this, QOverload<DPrinter *>::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<int, QProcess::ExitStatus>::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);
Comment on lines +2116 to +2117

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Retries fail after conversion timeouts

When the user retries printing before the killed process has finished, the timeout handler calls finishPdfPrintConversion(false), clearing the in-progress guard while m_printConvertProcess is still running; the retry’s start() does not launch a new conversion, and the old process’s callback can finish the retry as a failure or use its output.

Keep retries from starting until the timed-out process has stopped, and prevent its callbacks from completing a later request.

Also at reader/uiframe/DocSheet.cpp:2098-2105, reader/uiframe/DocSheet.cpp:2129-2130.

Prompt for AI agents
In `reader/uiframe/DocSheet.cpp` at lines 2122-2123:

**Retries fail after conversion timeouts**

When the user retries printing before the killed process has finished, the timeout handler calls `finishPdfPrintConversion(false)`, clearing the in-progress guard while `m_printConvertProcess` is still running; the retry’s `start()` does not launch a new conversion, and the old process’s callback can finish the retry as a failure or use its output.

Keep retries from starting until the timed-out process has stopped, and prevent its callbacks from completing a later request.

Also at `reader/uiframe/DocSheet.cpp:2098-2105`, `reader/uiframe/DocSheet.cpp:2129-2130`.

});
}

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<QMainWindow *>(pwgt))
parent = mwd;
}
if (!parent)
parent = qApp->activeWindow();
if (!parent)
parent = const_cast<DocSheet *>(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<DDialogCloseButton *>())
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()
Expand Down
46 changes: 46 additions & 0 deletions reader/uiframe/DocSheet.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <QSet>
#include <QReadWriteLock>
#include <QSharedPointer>
#include <QPointer>
#include <QThread>
#include <QSize>
#include <QRectF>
Expand Down Expand Up @@ -57,6 +58,7 @@ struct SheetOperation {

DWIDGET_BEGIN_NAMESPACE
class DPrinter;
class DFloatingMessage;
DWIDGET_END_NAMESPACE

class QTemporaryDir;
Expand Down Expand Up @@ -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是否存活
Expand Down Expand Up @@ -1016,6 +1052,16 @@ private slots:
// 缓存的内容哈希,避免自动保存时反复读盘计算
QString m_cachedContentHash;

// 打印转化等待提示(常驻轻量 Tooltip,转化完成/失败后关闭)
QPointer<Dtk::Widget::DFloatingMessage> 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; //当前调用的命令的进程地址

Expand Down
Loading