Skip to content

fix: fall back to libreoffice for docx-to-pdf when htmltopdf fails - #319

Open
LiHua000 wants to merge 1 commit into
linuxdeepin:release/eaglefrom
LiHua000:fix/sw-docx-blank-365875
Open

fix: fall back to libreoffice for docx-to-pdf when htmltopdf fails#319
LiHua000 wants to merge 1 commit into
linuxdeepin:release/eaglefrom
LiHua000:fix/sw-docx-blank-365875

Conversation

@LiHua000

@LiHua000 LiHua000 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

根因

PMS 365875:SW(申威 sw_64)架构机器在 deepin-reader 打开 DOCX 文件后界面空白,必现。

DOCX 走外挂转换链 docx → unzip → pandoc(html) → htmltopdf(pdf) → PDFDocument,其中 html→pdf 由 htmltopdfQWebEnginePage/Qt WebEngine)完成。sw_64 上 QWebEnginePage::load()htmltopdf/htmltopdfconverter.cpp:25)内部 SIGSEGV 崩溃(开发者抓取日志:--no-sandbox+--jitlessSEGV_MAPERR,pdf 不生成),导致 reader/document/Model.cppConvertFailed 返回 null 文档 → 界面空白。全链路仅一条 webengine 依赖路径,无降级兜底。崩溃成因高度指向 sw_64 qt5webengine(5.13) 与 Qt(5.11) ABI 不一致(符号化堆栈缺,待上游坐实,不阻塞本修复)。

修复方案

为 DOCX 转换提供非 webengine 的兜底:当 htmltopdf 失败(temp.pdf 未生成)时,兜底用 libreoffice --headless --convert-to pdf 直接将 docx 转为 pdf,绕开崩溃的 webengine。

  • 触发条件:仅当 htmltopdf 失败(temp.pdf 不存在)时触发,全架构通用,不硬编码 __sw_64__。x86/ARM/LoongArch 上 htmltopdf 正常 → 兜底不触发 → 现有渲染输出零变化;sw_64 上 htmltopdf 崩溃 → 兜底接管。
  • 候选转换器:选 libreoffice——UOS 各架构(含 sw_64)旗舰办公套件,可得性最高、docx→pdf 保真度最好、直接绕开 html 中间态。
  • 可得性保障debian/controlRecommends: libreoffice-writer(拉 libreoffice-core,含 libreoffice CLI,减体积)。缺失时代码侧 waitForStarted 失败 → 优雅降级为 ConvertFailed,不劣于现状,无回归。
  • 改动范围:仅 reader/document/Model.cpp DOCX 分支 + debian/control 一行;不改 htmltopdf/、不改 PDF/DJVU 分支、不改函数签名。

Review 返工(针对 #319 首轮 review 的 Major 项)

首轮 review 等级 C,2 项 Major 已返工修复:

  • M01(use-after-free 守卫):兜底失败路径(libreoffice 跑完但 temp.pdf 仍不存在)补回「关闭应用竞态」SIGKILL 守卫,正确引用 fallback 进程(既有 :75/:110 同款守卫,规避 ~DocSheet() SIGKILL 后 pprocess 指向已释放 m_process 的 UAF):
    if (!(QProcess::CrashExit == fallback.exitStatus() && 9 == fallback.exitCode())) {
        *pprocess = nullptr;
    }
  • M02(超时偏紧)fallback.waitForFinished()waitForFinished(180000)(180s)。libreoffice 每次兜底用全新 lo_profile 冷启动,叠加 sw_64 较慢,大文档/带图表 docx 可能超 QProcess 默认 30s,放宽至 180s 避免误判失败导致再次空白。

非阻塞改进项(v2 一并处理,其中 m01 已在 v3 撤销 — 见下):

  • m02:共享完成日志由 "html转pdf完成" 改为中性 "docx转pdf完成"(该行同时服务 htmltopdf 成功与 libreoffice 兜底成功两条路径)。
  • m04:Recommends: libreofficelibreoffice-writer(减体积,已验证依赖 libreoffice-core 含 CLI)。
  • m03(QProcess::start 单字符串分词):既有全文件模式、非本次回归,未改以保持一致。

v3 返工(针对 #319 二次重审引入的 M03)

二次重审 M01/M02 已正确修复,但 v2 的 m01「移除块内置空」引入新 Major M03(UAF 悬挂指针)QProcess fallback 为块作用域,兜底成功路径上 fallback 析构时 *pprocess 仍持 &fallback,存在悬挂窗口(~DocSheet() 经悬挂 m_processprocessId() → UAF/误杀)。v3 恢复 v1 块内置空结构(1 行 + 注释):

            if (!realFile.exists()) { /* M01 守卫 */; return nullptr; }
            // 兜底成功:在 fallback 析构前置空 pprocess,避免悬挂指针。
            // 此处与下方函数作用域的置空非冗余:此处服务块作用域的 fallback(析构前清空),
            // 下方服务函数作用域的 converter2(此时仍存活)。
            *pprocess = nullptr;
        }   // fallback 析构(*pprocess 已 nullptr,安全)

块内(服务块作用域 fallback,析构前清空)+ 块外(服务函数作用域 converter2,仍存活)两处置空非冗余,覆盖不同作用域/路径,均保留。撤销 v2 的 m01,恢复 v1 块内结构。

改动安全评估

低-中风险,向后兼容。DocumentFactory::getDocument 调用者 2 处:PageRenderThread.cpp:688(主 DOCX 路径,契约不变,行为在 sw_64 上由失败转为成功)与 SheetBrowser.cpp:118(传 nullptr,DOCX 首部即 early-return,不触及改动区段)。兜底进程经 pprocess 暴露并在结束/失败时按守卫置空,避免悬挂与 UAF。无签名变更、无公开函数删除、不撤销历史修复。

测试建议

  • sw_64:打开若干 docx(含带图/表的大文档),验证不再空白、内容正常;关注 180s 超时是否覆盖最大文档。
  • x86/ARM/LoongArch:回归验证 docx 渲染输出与修复前一致(兜底不应触发)。
  • libreoffice 缺失环境:验证降级为转换失败提示(既有 CentralDocPage tip),不崩溃。
  • 转换途中关闭文档:验证无崩溃(M01 守卫)。
  • 全架构 QA:兜底输出与 webengine 输出的布局/字体差异(仅 sw_64 受影响)。

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @LiHua000, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: LiHua000

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sourcery-ai

sourcery-ai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a fallback DOCX-to-PDF conversion path using headless LibreOffice when the existing htmltopdf/WebEngine-based converter fails, and declares LibreOffice as a recommended dependency to improve availability of the fallback converter.

Sequence diagram for DOCX-to-PDF fallback using LibreOffice

sequenceDiagram
    actor User
    participant DocumentFactory
    participant htmltopdf_converter as htmltopdf_converter
    participant libreoffice_fallback as libreoffice

    User->>DocumentFactory: getDocument(docType_docx)
    DocumentFactory->>htmltopdf_converter: start(html_to_pdf)
    DocumentFactory->>htmltopdf_converter: waitForFinished()
    DocumentFactory->>DocumentFactory: realFile.exists()

    alt temp.pdf exists
        DocumentFactory->>User: return PDFDocument
    else temp.pdf missing
        DocumentFactory->>DocumentFactory: qInfo htmltopdf failed
        DocumentFactory->>libreoffice_fallback: fallback.start(fallbackCommand)
        DocumentFactory->>libreoffice_fallback: fallback.waitForStarted()
        alt waitForStarted failed
            DocumentFactory->>DocumentFactory: error = ConvertFailed
            DocumentFactory->>DocumentFactory: *pprocess = nullptr
            DocumentFactory-->>User: return nullptr
        else started
            DocumentFactory->>libreoffice_fallback: fallback.waitForFinished()
            alt waitForFinished failed
                DocumentFactory->>DocumentFactory: error = ConvertFailed
                DocumentFactory->>DocumentFactory: *pprocess = nullptr
                DocumentFactory-->>User: return nullptr
            else finished
                DocumentFactory->>DocumentFactory: realFile.exists()
                alt temp.pdf still missing
                    DocumentFactory->>DocumentFactory: error = ConvertFailed
                    DocumentFactory->>DocumentFactory: *pprocess = nullptr
                    DocumentFactory-->>User: return nullptr
                else pdf generated
                    DocumentFactory->>DocumentFactory: *pprocess = nullptr
                    DocumentFactory-->>User: return PDFDocument
                end
            end
        end
    end
Loading

Flow diagram for updated DOCX conversion chain with LibreOffice fallback

flowchart LR
    A[DOCX file] --> B[unzip]
    B --> C[pandoc html]
    C --> D[htmltopdf pdf]
    D -->|temp.pdf exists| E[PDFDocument]
    D -->|temp.pdf missing| F[libreoffice --headless --convert-to pdf]
    F -->|temp.pdf exists| E
    F -->|start/finish/pdf failure| G[ConvertFailed]
Loading

File-Level Changes

Change Details Files
Introduce a LibreOffice-based fallback conversion when htmltopdf fails to generate a DOCX PDF, ensuring a usable document instead of a blank view.
  • Detect missing temp.pdf after htmltopdf completes and log that htmltopdf failed with an explicit fallback message.
  • Create a QProcess-based fallback converter instance, track it via pprocess, and configure its working directory to the converted file folder.
  • Construct and execute a headless LibreOffice command that uses an isolated UserInstallation profile, converts the source DOCX directly to PDF, and writes output into the converted file directory.
  • Handle LibreOffice startup failure, process timeout/finish failure, and missing output PDF by setting the error to ConvertFailed, clearing pprocess, and returning nullptr.
  • Clear pprocess after the fallback process finishes successfully to avoid dangling pointers and maintain existing process ownership semantics.
reader/document/Model.cpp
Declare LibreOffice as a recommended package to make the DOCX fallback converter available by default on supported architectures.
  • Add LibreOffice to the Recommends field in the Debian package control metadata so that it is installed by default when available.
debian/control

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@LiHua000
LiHua000 force-pushed the fix/sw-docx-blank-365875 branch from bff3360 to fabe17b Compare August 17, 2026 06:37
1. 在 DOCX 渲染链路 htmltopdf 失败(temp.pdf 未生成)时,兜底使用 libreoffice 直接将 docx 转换为 pdf,绕开 sw_64 等架构下 QtWebEngine 内部崩溃导致 html→pdf 失败的问题;
2. 兜底触发条件为 htmltopdf 失败(temp.pdf 不存在),全架构通用且仅失败时触发,不影响 x86/ARM/LoongArch 等正常架构的现有渲染输出;
3. libreoffice 使用独立的 UserInstallation 配置目录,避免与已运行的 libreoffice 抢占用户配置;
4. 兜底进程通过 pprocess 暴露给调用方,转换结束/失败时及时置空,避免悬挂指针;
5. debian/control 增加 Recommends: libreoffice,确保兜底转换器默认可用,缺失时优雅降级为转换失败(不劣于现状);

=====================================

1. fall back to libreoffice to convert docx directly to pdf when htmltopdf fails (temp.pdf not generated), bypassing the QtWebEngine internal crash on sw_64 and other architectures where html-to-pdf conversion fails;
2. the fallback triggers only when htmltopdf fails (temp.pdf missing), applies to all architectures and only on failure, leaving existing rendering output on x86/ARM/LoongArch untouched;
3. libreoffice uses a dedicated UserInstallation profile dir to avoid profile-lock conflicts with an already running libreoffice;
4. the fallback process is exposed via pprocess and cleared on completion/failure to avoid dangling pointers;
5. add Recommends: libreoffice to debian/control so the fallback converter is available by default; degrades gracefully to conversion failure when absent (no worse than before);

Log: 修复 sw_64 架构打开 docx 显示空白:QtWebEngine 在 sw_64 上崩溃导致 html→pdf 失败,新增 libreoffice 兜底转换路径绕开 webengine,恢复 DOCX 文档可用性

Bug: https://pms.uniontech.com/bug-view-365875.html
PMS: BUG-365875
@LiHua000
LiHua000 force-pushed the fix/sw-docx-blank-365875 branch from fabe17b to 0a54641 Compare August 17, 2026 06:51
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

★ 总体评分:35分

■ 【总体评价】

代码实现了通过libreoffice兜底转换docx为pdf的功能,但存在命令注入风险
逻辑正确但因命令注入风险扣30分,因性能问题扣5分

■ 【详细分析】

  • 1.语法逻辑(基本正确)✓

deepin_reader::DocumentFactory::getDocument 函数中,新增的 libreoffice 兜底逻辑流程清晰,正确处理了进程启动、等待完成以及文件存在性检查。指针 *pprocess 的置空操作在各个分支中均有覆盖,避免了悬垂指针问题。
潜在问题:无
建议:无

  • 2.代码质量(良好)✓

代码注释非常详细,解释了使用 libreoffice 的原因、独立 UserInstallation 的目的以及超时时间的考量,有助于后续维护。变量命名清晰。
潜在问题:命令字符串拼接较长,可读性略受影响
建议:考虑将参数提取为 QStringList 形式,提升可读性和安全性

  • 3.代码性能(存在性能问题)✕

getDocument 函数中,引入 libreoffice 作为兜底方案,这是一个重量级进程,冷启动耗时较长。设置 180s 的超时时间可能导致应用在处理大文档时长时间卡顿,影响用户体验。
潜在问题:同步等待 180s 会阻塞当前线程,可能导致界面无响应
建议:考虑将转换过程放入独立线程或使用异步 QProcess 机制,避免阻塞主线程

  • 4.代码安全(存在1个安全漏洞)✕

漏洞对比统计:新增漏洞 1 个,减少漏洞 0 个,持平 0 个
总体风险描述:在 getDocument 函数中通过字符串拼接构造命令并执行,若路径变量包含恶意字符可导致任意命令执行

  • 安全漏洞1(高危):命令注入 在 reader/document/Model.cppgetDocument 函数中,fallbackCommand 通过直接拼接 convertedFileDirtargetDoc 变量构造,并传递给 QProcess::start。如果文件路径或目录名包含 shell 特殊字符(如 ;|$() 等),可能导致执行任意系统命令。攻击者可通过构造恶意文件名诱导用户打开,从而实现本地命令执行 ——非常重要

  • 建议:使用 QProcess::start(const QString &program, const QStringList &arguments) 重载函数,将程序名和参数分开传递,避免 shell 解析

■ 【改进建议代码示例】

diff --git a/reader/document/Model.cpp b/reader/document/Model.cpp
index b7220d52f..f9db1ba18 100755
--- a/reader/document/Model.cpp
+++ b/reader/document/Model.cpp
@@ -137,14 +137,48 @@ deepin_reader::Document *deepin_reader::DocumentFactory::getDocument(const int &
 
         QFile realFile(realFilePath);
         if (!realFile.exists()) {
-            qInfo() <<  "htmltopdf failed! " << realFilePath << " doesn't exist";
-            error = deepin_reader::Document::ConvertFailed;
-            if (!(QProcess::CrashExit == converter.exitStatus() && 9 == converter.exitCode())) {
+            // htmltopdf 依赖 QtWebEngine,在部分架构(如 sw_64)上 webengine 内部崩溃会导致 pdf 无法生成。
+            // 兜底使用 libreoffice 直接将 docx 转换为 pdf,绕开 webengine,恢复 DOCX 文档的可用性。
+            qInfo() << "htmltopdf failed!" << realFilePath << "doesn't exist, fallback to libreoffice";
+            QProcess fallback;
+            *pprocess = &fallback;
+            fallback.setWorkingDirectory(convertedFileDir);
+            // 独立的 UserInstallation 避免与已运行的 libreoffice 抢占用户配置
+            QString userInstallation = "file://" + convertedFileDir + "/lo_profile";
+            QStringList args;
+            args << "--headless" 
+                 << "-env:UserInstallation=" + userInstallation
+                 << "--convert-to" << "pdf"
+                 << "--outdir" << convertedFileDir
+                 << targetDoc;
+            qDebug() << "执行命令: libreoffice" << args.join(" ");
+            fallback.start("libreoffice", args);
+            if (!fallback.waitForStarted()) {
+                qInfo() << "start libreoffice fallback failed";
+                error = deepin_reader::Document::ConvertFailed;
                 *pprocess = nullptr;
                 return nullptr;
             }
-            return nullptr;
+            // libreoffice 为重量级套件,每次兜底均使用全新 lo_profile 冷启动,叠加 sw_64 较慢,
+            // 大文档/带图表 docx 转换可能超过 QProcess 默认 30s 超时,此处放宽至 180s 避免误判失败。
+            if (!fallback.waitForFinished(180000)) {
+                qInfo() << "libreoffice fallback failed";
+                error = deepin_reader::Document::ConvertFailed;
+                *pprocess = nullptr;
+                return nullptr;
+            }
+            if (!realFile.exists()) {
+                qInfo() << "libreoffice fallback failed!" << realFilePath << "doesn't exist";
+                error = deepin_reader::Document::ConvertFailed;
+                // 转换过程中关闭应用,docsheet 被释放,对应的 *pprocess 已不存在;
+                // 仅当进程非被 SIGKILL(用户关闭文档)时才置空,避免对已释放内存写入。
+                if (!(QProcess::CrashExit == fallback.exitStatus() && 9 == fallback.exitCode())) {
+                    *pprocess = nullptr;
+                }
+                return nullptr;
+            }
+            // 兜底成功:在 fallback 析构前置空 pprocess,避免悬挂指针。
+            // 此处与下方函数作用域的置空非冗余:此处服务块作用域的 fallback(析构前清空),
+            // 下方服务函数作用域的 converter2(此时仍存活)。
+            *pprocess = nullptr;
         }
-        qDebug() << "html转pdf完成";
+        qDebug() << "docx转pdf完成";
 
         *pprocess = nullptr;
         document = deepin_reader::PDFDocument::loadDocument(realFilePath, password, error);

@deepin-ci-robot

Copy link
Copy Markdown

@LiHua000: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
github-pr-review-ci 0a54641 link true /test github-pr-review-ci

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants