feat(reader): add OFD reading support via rofd - #381
Open
hualet wants to merge 19 commits into
Open
Conversation
added 19 commits
September 11, 2026 12:01
Add a read-only OFD (GB/T 33190-2016) backend built on the rofd library's stable C ABI (librofd_ffi), following the XPS adapter integration pattern: - OfdDocument/OfdPage implement the Document/Page interfaces: open, page count, page size (mm to logical px via screen DPI), and page rendering through rofd_renderer_render_page_cairo into a QImage. Text extraction and search return empty until the rofd C ABI exposes text APIs. - New Dr::OFD file type detected by .ofd extension, factory branch, open-dialog filter, sidebar (thumbnail | bookmark), big-image rendering path, title widget enablement, save-as filter and format string. - CMake option OFD_SUPPORT (default ON) locates rofd.h and librofd_ffi via -DROFD_ROOT, degrading gracefully when absent; qmake gains a matching ofd_support CONFIG block. - Install application/ofd MIME definition and register it in the desktop file. - Add GTest coverage (open, page size, out-of-range, render content check, invalid size, missing/broken file) with tests/files/normal.ofd. Verified: full build with OFD on/off, 7 OFD unit tests pass, and the application opens and renders an OFD invoice end to end.
OFD support now uses the Debian packages shipped by upstream rofd instead of a local rofd checkout: - debian/control Build-Depends on librofd-ffi-dev; the runtime dependency on librofd-ffi0 (>= 0.2.2) is derived automatically by dh_shlibdeps from the library's shlibs file. - CMake keeps locating rofd.h and librofd_ffi through find_path and find_library, now also looking in lib/<multiarch> so a custom install prefix works, and still degrades gracefully (OFD disabled with a warning) when the dependency is absent; -DROFD_ROOT remains available for local development against a rofd checkout. - qmake CONFIG+=ofd_support links the system librofd_ffi. - Document librofd-ffi-dev in both README dependency lists. Verified with librofd-ffi-dev 0.2.2-1: full build, TestOfdModel 7/7, 1116 unit tests pass, the application opens and renders tests/files/normal.ofd, and dpkg-shlibdeps resolves librofd-ffi0 (>= 0.2.2).
The thumbnail delegate inverted the page pixmap whenever the system theme was dark, while BrowserPage only inverts in the night eye-protection mode. Under a dark system theme with eye protection off the main view kept white pages but the sidebar thumbnails turned black, so the two disagreed - most visible with white OFD/PDF pages. - ThumbnailDelegate now follows the EyeProtectionManager mode instead of the system theme and mirrors BrowserPage::paint exactly: night mode inverts luminance and dims with the page colour, classic/green multiply-tint with the page colour, off draws the original pixmap. - Extract the HSL luminance inversion into EyeProtectionManager::invertLuminance and let BrowserPage::applyNightMode delegate to it so both paths share one algorithm; the inverted thumbnail is cached by source pixmap key to avoid per-pixel work on every repaint. - SideBarImageListView refreshes visible thumbnails on modeChanged instead of themeTypeChanged. - Cover the inversion helper, the mode-driven delegate painting and the modeChanged refresh in unit tests. Verified under a dark system theme: thumbnails render white with eye protection off (matching the main view, previously black), dark in night mode and tinted in classic mode; targeted unit tests pass. Log: 修复深色主题下OFD/PDF侧边栏缩略图黑底与主视图白底不一致的问题 Influence: 侧边栏缩略图外观改为跟随护眼模式(夜间反色、经典/绿色染色),与主视图页面保持一致;深色系统主题且未开启护眼时缩略图恢复白底
Use rofd pixel canvas and region rendering APIs instead of rendering and cropping a full page. Validate tile bounds and raster budgets before allocation, probe the required API at configure time, and add a lightweight OFD adapter test target.
Map rofd metadata snapshots to reader properties, preserve raw dates, and use valid document dates in file attributes. Add metadata and identifier coverage and extend the required API probe.
Refresh owned warning snapshots after lazy page loads and rendering, log each warning once, and display warning details in file attributes. Add Chinese translations and lazy-load warning coverage.
There was a problem hiding this comment.
Sorry @hualet, your pull request is larger than the review limit of 150,000 diff characters
|
Warning
|
|
Note
详情{
"tests/document/ut_ofdmodel.cpp": [
{
"line": " EXPECT_EQ(uri.navigation->uri, QUrl(\"https://example.invalid/base/child?q=1&x=2\"));",
"line_number": 183,
"rule": "S35",
"reason": "Url link | 443f9cf4ab"
},
{
"line": " \"<Action Event=\\"PO\\"><URI URI=\\"https://example.invalid/\\"/></Action>\"",
"line_number": 274,
"rule": "S35",
"reason": "Url link | 12f87b2550"
},
{
"line": " + linkAction(\"<URI URI=\\"https://ignored.invalid/\\"/>\", {}, \"PO\")",
"line_number": 360,
"rule": "S35",
"reason": "Url link | 2b4aa39828"
},
{
"line": " + linkAction(\"<URI URI=\\"https://later.invalid/\\"/>\", separatedLinkRegions) + \"</Actions>\";",
"line_number": 363,
"rule": "S35",
"reason": "Url link | d3815d1565"
},
{
"line": " linkAction(\"<URI URI=\\"https://explicit.invalid/\\"/>\", explicitRegion))",
"line_number": 412,
"rule": "S35",
"reason": "Url link | 183a779763"
},
{
"line": " + object(\"11\", \"100 110 20 10\", linkAction(\"<URI URI=\\"https://fallback.invalid/\\"/>\"));",
"line_number": 413,
"rule": "S35",
"reason": "Url link | cca6461ea0"
},
{
"line": " for (const auto &sample : {std::make_pair(QPointF(55, 65), QString(\"https://explicit.invalid/\")),",
"line_number": 421,
"rule": "S35",
"reason": "Url link | 183a779763"
},
{
"line": " std::make_pair(QPointF(105, 115), QString(\"https://fallback.invalid/\"))}) {",
"line_number": 422,
"rule": "S35",
"reason": "Url link | cca6461ea0"
},
{
"line": " linkAction(\"<URI URI=\\"https://ignored.invalid/\\"/>\", {}, \"PO\"),",
"line_number": 437,
"rule": "S35",
"reason": "Url link | 2b4aa39828"
},
{
"line": " linkAction(\"<URI URI=\\"https://ignored.invalid/\\"/>\", {}, \"CUSTOM\"),",
"line_number": 439,
"rule": "S35",
"reason": "Url link | 2b4aa39828"
},
{
"line": " ? linkAction(\"<URI URI=\\"child?q=1&x=2\\" Base=\\"https://example.invalid/base/\\"/>\")",
"line_number": 455,
"rule": "S35",
"reason": "Url link | 275ea50db3"
},
{
"line": " + linkAction(\"<URI URI=\\"https://later.invalid/\\"/>\") : QByteArray();",
"line_number": 456,
"rule": "S35",
"reason": "Url link | d3815d1565"
},
{
"line": " EXPECT_EQ(hit.navigation->uri, QUrl(\"https://example.invalid/base/child?q=1&x=2\"));",
"line_number": 469,
"rule": "S35",
"reason": "Url link | 443f9cf4ab"
},
{
"line": " EXPECT_EQ(hit.urlOrFileName, QStringLiteral(\"https://example.invalid/base/child?q=1&x=2\"));",
"line_number": 470,
"rule": "S35",
"reason": "Url link | 443f9cf4ab"
},
{
"line": " + linkAction(\"<URI URI=\\"https://bad-region.invalid/\\"/>\", \"<Region><Area Start=\\"NaN 0\\"/></Region>\")",
"line_number": 509,
"rule": "S35",
"reason": "Url link | eebdbaccbf"
}
],
"tests/document/ut_catalogoutlinemodel.cpp": [
{
"line": " external.navigation = NavigationTarget{{}, QUrl(\"https://example.invalid/\")};",
"line_number": 94,
"rule": "S35",
"reason": "Url link | 12f87b2550"
}
],
"tests/document/ut_navigation.cpp": [
{
"line": " target.uri = QUrl(QStringLiteral(\"https://example.org/document\"));",
"line_number": 128,
"rule": "S35",
"reason": "Url link | e4287474a7"
},
{
"line": " for (const auto &uri : {QStringLiteral(\"http://example.org/\"),",
"line_number": 354,
"rule": "S35",
"reason": "Url link | 4ae2026102"
},
{
"line": " QStringLiteral(\"https://example.org:443/a%20b?q=x#section\"),",
"line_number": 355,
"rule": "S35",
"reason": "Url link | 4c2c7e6a14"
},
{
"line": " EXPECT_EQ(resolveNavigationUri(\"../next.html#heading\", \"https://example.org/docs/book/\"),",
"line_number": 368,
"rule": "S35",
"reason": "Url link | 09cc6ee688"
},
{
"line": " QUrl(\"https://example.org/docs/next.html#heading\"));",
"line_number": 369,
"rule": "S35",
"reason": "Url link | 83ae76601b"
},
{
"line": " EXPECT_EQ(resolveNavigationUri(\"/page\", \"http://example.org/docs/book\"),",
"line_number": 370,
"rule": "S35",
"reason": "Url link | a9a46ad780"
},
{
"line": " QUrl(\"http://example.org/page\"));",
"line_number": 371,
"rule": "S35",
"reason": "Url link | f927d94e81"
},
{
"line": " EXPECT_EQ(resolveNavigationUri(\"#part\", \"https://example.org/document.html\"),",
"line_number": 372,
"rule": "S35",
"reason": "Url link | d82c59b640"
},
{
"line": " QUrl(\"https://example.org/document.html#part\"));",
"line_number": 373,
"rule": "S35",
"reason": "Url link | e576c85fbe"
},
{
"line": " EXPECT_EQ(resolveNavigationUri(\"//cdn.example.org/image\", \"https://example.org/\"),",
"line_number": 374,
"rule": "S35",
"reason": "Url link | 48800cefc9"
},
{
"line": " QUrl(\"https://cdn.example.org/image\"));",
"line_number": 375,
"rule": "S35",
"reason": "Url link | a29bb02fca"
},
{
"line": " QStringLiteral(\"ftp://example.org/file\"), QStringLiteral(\"custom:target\"),",
"line_number": 387,
"rule": "S35",
"reason": "Url link | 92f7b93b67"
},
{
"line": " QStringLiteral(\"http://example.org/%zz\"), QStringLiteral(\"http://exa mple.org/\"),",
"line_number": 390,
"rule": "S35",
"reason": "Url link | f81bd3b3c6"
},
{
"line": " QStringLiteral(\"https://example.org/a b\"), QStringLiteral(\"\nhttps://example.org/\"),",
"line_number": 391,
"rule": "S35",
"reason": "Url link | 48800cefc9"
},
{
"line": " EXPECT_TRUE(resolveNavigationUri(uri, \"https://example.org/docs/\").isEmpty())",
"line_number": 395,
"rule": "S35",
"reason": "Url link | e912a44d45"
},
{
"line": " Link link(QPainterPath(), QStringLiteral(\"https://legacy.example.org\"));",
"line_number": 423,
"rule": "S35",
"reason": "Url link | 586e8c0f46"
}
],
"tests/ofd-model/navigation_smoke.cc": [
{
"line": " \"<URI URI=\\"https://example.invalid/navigation\\"/></Action></Actions></OutlineElem>\";",
"line_number": 128,
"rule": "S35",
"reason": "Url link | 34f9245507"
},
{
"line": " \"<Actions><Action Event=\\"CLICK\\"><URI URI=\\"https://example.invalid/page\\"/>\"",
"line_number": 141,
"rule": "S35",
"reason": "Url link | 58cd5f0d75"
},
{
"line": " verify(uriLink.urlOrFileName == \"https://example.invalid/page\", \"page link hover URL\");",
"line_number": 359,
"rule": "S35",
"reason": "Url link | 58cd5f0d75"
}
],
"reader/Application.cpp": [
{
"line": " setApplicationAcknowledgementPage(\"https://www.deepin.org/acknowledgments/deepin_reader\");",
"line_number": 30,
"rule": "S35",
"reason": "Url link | c08ef69076"
}
],
"reader/app/Global.h": [
{
"line": "const QString key_up = \"Up\";",
"line_number": 112,
"rule": "S106",
"reason": "Var naming | 20293a700f"
},
{
"line": "const QString key_down = \"Down\";",
"line_number": 113,
"rule": "S106",
"reason": "Var naming | 21f2799b08"
},
{
"line": "const QString key_left = \"Left\";",
"line_number": 114,
"rule": "S106",
"reason": "Var naming | ebc87ca254"
},
{
"line": "const QString key_right = \"Right\";",
"line_number": 115,
"rule": "S106",
"reason": "Var naming | ce51450b6e"
},
{
"line": "const QString key_space = \"Space\"; // 空格用于停止启动幻灯片播放",
"line_number": 116,
"rule": "S106",
"reason": "Var naming | 19eddccbbe"
},
{
"line": "const QString key_pgUp = \"PgUp\"; // 上一页",
"line_number": 117,
"rule": "S106",
"reason": "Var naming | 9c0713cb36"
},
{
"line": "const QString key_pgDown = \"PgDown\"; // 下一页",
"line_number": 118,
"rule": "S106",
"reason": "Var naming | 73cb3b091d"
},
{
"line": "const QString key_delete = \"Del\"; // 删除",
"line_number": 119,
"rule": "S106",
"reason": "Var naming | 8309604c6a"
},
{
"line": "const QString key_esc = \"Esc\"; // 退出全屏\退出放映\退出放大镜",
"line_number": 120,
"rule": "S106",
"reason": "Var naming | 7dc55b084c"
},
{
"line": "const QString key_f1 = \"F1\"; // 帮助",
"line_number": 121,
"rule": "S106",
"reason": "Var naming | cfc950dca5"
},
{
"line": "const QString key_f5 = \"F5\"; // 播放幻灯片",
"line_number": 122,
"rule": "S106",
"reason": "Var naming | 8ce983fd5e"
},
{
"line": "const QString key_f11 = \"F11\"; // 全屏",
"line_number": 123,
"rule": "S106",
"reason": "Var naming | a2272d0c5c"
},
{
"line": "const QString key_ctrl_1 = \"Ctrl+1\"; // 适合页面状态",
"line_number": 124,
"rule": "S106",
"reason": "Var naming | 298036410c"
},
{
"line": "const QString key_ctrl_2 = \"Ctrl+2\"; // 适合高度",
"line_number": 125,
"rule": "S106",
"reason": "Var naming | bb89826f32"
},
{
"line": "const QString key_ctrl_3 = \"Ctrl+3\"; // 适合宽度",
"line_number": 126,
"rule": "S106",
"reason": "Var naming | 0ec78a6f33"
},
{
"line": "const QString key_ctrl_d = \"Ctrl+D\"; // 添加书签",
"line_number": 127,
"rule": "S106",
"reason": "Var naming | 55b590dac6"
},
{
"line": "const QString key_ctrl_f = \"Ctrl+F\"; // 搜索",
"line_number": 128,
"rule": "S106",
"reason": "Var naming | c37c14ca83"
},
{
"line": "const QString key_ctrl_o = \"Ctrl+O\"; // 打开",
"line_number": 129,
"rule": "S106",
"reason": "Var naming | 71c792ad13"
},
{
"line": "const QString key_ctrl_e = \"Ctrl+E\"; // 导出",
"line_number": 130,
"rule": "S106",
"reason": "Var naming | f21c130ba1"
},
{
"line": "const QString key_ctrl_p = \"Ctrl+P\"; // 打印",
"line_number": 131,
"rule": "S106",
"reason": "Var naming | c6591979a9"
},
{
"line": "const QString key_ctrl_s = \"Ctrl+S\"; // 保存",
"line_number": 132,
"rule": "S106",
"reason": "Var naming | 7c224c84ee"
},
{
"line": "const QString key_ctrl_m = \"Ctrl+M\"; // 打开目标缩略图",
"line_number": 133,
"rule": "S106",
"reason": "Var naming | f723ab894f"
},
{
"line": "const QString key_ctrl_r = \"Ctrl+R\"; // 左旋转",
"line_number": 134,
"rule": "S106",
"reason": "Var naming | d3f2cb7d2b"
},
{
"line": "const QString key_ctrl_c = \"Ctrl+C\";",
"line_number": 135,
"rule": "S106",
"reason": "Var naming | 7191938108"
},
{
"line": "const QString key_ctrl_x = \"Ctrl+X\";",
"line_number": 136,
"rule": "S106",
"reason": "Var naming | a6dd4ba68a"
},
{
"line": "const QString key_ctrl_v = \"Ctrl+V\";",
"line_number": 137,
"rule": "S106",
"reason": "Var naming | 241a1788e0"
},
{
"line": "const QString key_ctrl_z = \"Ctrl+Z\";",
"line_number": 138,
"rule": "S106",
"reason": "Var naming | f78601e55e"
},
{
"line": "const QString key_ctrl_a = \"Ctrl+A\";",
"line_number": 139,
"rule": "S106",
"reason": "Var naming | 4774d3cc37"
},
{
"line": "const QString key_ctrl_equal = \"Ctrl+=\"; // 放大",
"line_number": 140,
"rule": "S106",
"reason": "Var naming | 51a1643479"
},
{
"line": "const QString key_ctrl_smaller = \"Ctrl+-\"; // 缩小",
"line_number": 141,
"rule": "S106",
"reason": "Var naming | 2c983162a1"
},
{
"line": "const QString key_alt_1 = \"Alt+1\"; // 选择工具",
"line_number": 142,
"rule": "S106",
"reason": "Var naming | a0378f280b"
},
{
"line": "const QString key_alt_2 = \"Alt+2\"; // 手型工具",
"line_number": 143,
"rule": "S106",
"reason": "Var naming | 57ced1cd00"
},
{
"line": "const QString key_alt_a = \"Alt+A\"; // 添加注释",
"line_number": 144,
"rule": "S106",
"reason": "Var naming | 3c60d1d86d"
},
{
"line": "const QString key_alt_h = \"Alt+H\"; // 添加高亮",
"line_number": 145,
"rule": "S106",
"reason": "Var naming | 1878434593"
},
{
"line": "const QString key_alt_z = \"Alt+Z\"; // 放大镜",
"line_number": 146,
"rule": "S106",
"reason": "Var naming | e48b2efc36"
},
{
"line": "const QString key_alt_f4 = \"Alt+F4\"; // 退出应用程序",
"line_number": 147,
"rule": "S106",
"reason": "Var naming | d225da4d6e"
},
{
"line": "const QString key_alt_harger = \"Ctrl++\"; // 放大",
"line_number": 148,
"rule": "S106",
"reason": "Var naming | 9e19ceeedd"
},
{
"line": "const QString key_ctrl_shift_r = \"Ctrl+Shift+R\"; // 右旋转",
"line_number": 149,
"rule": "S106",
"reason": "Var naming | 6be0727d79"
},
{
"line": "const QString key_ctrl_shift_s = \"Ctrl+Shift+S\"; // 另存为",
"line_number": 150,
"rule": "S106",
"reason": "Var naming | 8f81237f85"
},
{
"line": "const QString key_ctrl_shift_slash = \"Ctrl+Shift+/\";",
"line_number": 151,
"rule": "S106",
"reason": "Var naming | bad1d49dcc"
},
{
"line": "const QString key_ctrl_home = \"Ctrl+Home\"; //第一页",
"line_number": 152,
"rule": "S106",
"reason": "Var naming | bff733899f"
},
{
"line": "const QString key_ctrl_end = \"Ctrl+End\"; //最后一页",
"line_number": 153,
"rule": "S106",
"reason": "Var naming | 7cbe7a3f8b"
}
],
"assets/mimetype/ofd.xml": [
{
"line": "<mime-info xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">",
"line_number": 2,
"rule": "S35",
"reason": "Url link | 4072667606"
}
]
} |
Contributor
|
Hi, @hualet thanks for your PR! There's two issues here: 1. render region outside canvas
_._20260911152618.mp42.renderer returned a diagnostic kind unknown to C ABI v1
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

概述
为 deepin-reader 增加 OFD 文档支持。当前以只读阅读为目标,主要阅读链路已经基本完善:不仅能够打开和显示 OFD,也已接入文本选择/复制、全文搜索、目录与页面链接、阅读状态恢复、文档信息等常用能力。
底层使用 linuxdeepin/rofd 提供的 OFD 解析与渲染能力,通过
rofd-ffi的 C ABI 接入现有Document/Page抽象。reader 负责界面和交互,格式解析、文本语义和渲染由 rofd 提供。当前支持状态
XYZ、Fit、FitH、FitV、FitR目标及旋转/双页布局下定位与主干保持一致
master(4862a03c)完成 rebase,保留主干的异步NightFilter、主视图图片区域蒙版和阅读状态恢复机制。依赖与构建
librofd-ffi-dev (>= 0.4.0),渲染适配使用 Cairo。ROFD_ROOT或ROFD_INCLUDE_DIR/ROFD_FFI_LIBRARY指定本地构建。OFD_SUPPORT控制可选接入;配置时检查所需的区域渲染、元数据、警告、目录和页面链接 C ABI,避免误用接口不完整的旧库。DPdfPage::imageObjectRects()接口。当前边界
验证
本地使用 Qt 6 和 rofd 0.4.0:
deepin-reader本体,-j1,未触发 OOM。git diff --check。定向测试不链接体积较大的
test-deepin-reader。Qt 控件检查使用 offscreen 平台,不替代可见桌面窗口、实体打印机和更广泛文档样本的验收;本地结果也不等同于 GitHub CI 已通过。复现命令和范围见 定向验证说明。