From d5adbef052c71bcb2aa423a4593681e86731d374 Mon Sep 17 00:00:00 2001 From: ZhangTingan Date: Tue, 4 Nov 2025 14:38:01 +0800 Subject: [PATCH 1/4] fix: build error in qt5 env Log: as title --- CMakeLists.txt | 6 +++--- htmltopdf/CMakeLists.txt | 2 +- reader/CMakeLists.txt | 25 ++++++++++++++++++++++--- reader/document/XpsDocument.cpp | 10 ++++++++-- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 95b13f65..9c95576a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # XPS支持选项(默认关闭) option(XPS_SUPPORT "Enable XPS document format support" OFF) + if(XPS_SUPPORT) message(">>> XPS support enabled") add_compile_definitions(XPS_SUPPORT_ENABLED) @@ -37,11 +38,10 @@ set(QT_DESIRED_VERSION ${QT_VERSION_MAJOR}) # 设置DTK版本 if (QT_VERSION_MAJOR MATCHES 6) set(DTK_VERSION_MAJOR 6) - set(PDF_LIB_VERSION "") else() set(DTK_VERSION_MAJOR "") - set(PDF_LIB_VERSION 5) endif() +set(PDF_LIB_VERSION "") message(">>> Build with DTK: ${DTK_VERSION_MAJOR}") # 定义必需的Qt组件 @@ -100,4 +100,4 @@ add_custom_target(${PROJECT_NAME}_qm_files DEPENDS ${QM_FILES}) add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}_qm_files) # Install translations -install(FILES ${QM_FILES} DESTINATION share/${PROJECT_NAME}/translations) \ No newline at end of file +install(FILES ${QM_FILES} DESTINATION share/${PROJECT_NAME}/translations) diff --git a/htmltopdf/CMakeLists.txt b/htmltopdf/CMakeLists.txt index 635a0396..e42619e9 100644 --- a/htmltopdf/CMakeLists.txt +++ b/htmltopdf/CMakeLists.txt @@ -50,4 +50,4 @@ endif() # 安装目标 install(TARGETS htmltopdf RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/deepin-reader -) \ No newline at end of file +) diff --git a/reader/CMakeLists.txt b/reader/CMakeLists.txt index 11ca53a4..0e1159fd 100644 --- a/reader/CMakeLists.txt +++ b/reader/CMakeLists.txt @@ -87,7 +87,9 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE APP_VERSION="${APP_VERSION}" ) -# 链接库 +# 添加资源文件 +if (QT_VERSION_MAJOR MATCHES 6) + # 链接库 target_link_libraries(deepin-reader PRIVATE ${LINK_LIBS} ${LIBJPEG_LIBRARIES} @@ -95,10 +97,27 @@ target_link_libraries(deepin-reader PRIVATE ${PDFIUM_LIBRARIES} ) -# 添加资源文件 qt_add_resources(reader_RESOURCES ${CMAKE_SOURCE_DIR}/resources/resources.qrc ) + +else() + +# 链接库 +target_link_libraries(deepin-reader PRIVATE + ${LINK_LIBS} + ${LIBJPEG_LIBRARIES} + ${DDJVU_LIBRARIES} + ${PDFIUM_LIBRARIES} + dl + pthread +) + +qt5_add_resources(reader_RESOURCES + ${CMAKE_SOURCE_DIR}/resources/resources.qrc +) +endif() + target_sources(deepin-reader PRIVATE ${reader_RESOURCES}) # 安装目标 @@ -140,4 +159,4 @@ if(DEFINED DSG_DATA_DIR) else() install(FILES ${DCONFIG_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/share/dsg/configs/${DCONFIG_APPID}) message("-- DConfig is not supported by DTK") -endif() \ No newline at end of file +endif() diff --git a/reader/document/XpsDocument.cpp b/reader/document/XpsDocument.cpp index 0e02de93..cff0e6a3 100644 --- a/reader/document/XpsDocument.cpp +++ b/reader/document/XpsDocument.cpp @@ -302,7 +302,11 @@ QList XpsPage::words() m_words.clear(); for (int i = 0; i < m_pageData.textStrings.size(); ++i) { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QStringList wordList = m_pageData.textStrings[i].split(QRegularExpression("\\s+"), QString::SkipEmptyParts); +#else QStringList wordList = m_pageData.textStrings[i].split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); +#endif for (const QString &word : wordList) { if (!word.isEmpty()) { m_words.append(Word(word, m_pageData.textRects[i])); @@ -629,8 +633,6 @@ void XpsDocument::parseXpsContent() QXmlStreamAttributes attrs = docReader.attributes(); QString pageSource = attrs.value("Source").toString(); - - // 解析页面路径: // - 以 '/' 开头:包内绝对路径,直接去掉开头的 '/' // - 否则:相对于主文档 FixedDoc.fdoc 所在目录 @@ -924,7 +926,11 @@ void XpsDocument::parseTextData(QXmlStreamReader &reader, XpsPageData &pageInfo) // 去掉分号,将逗号分隔的数值提取 QString cleaned = idx; cleaned.replace(';', ' '); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + const QStringList parts = cleaned.split(',', QString::SkipEmptyParts); +#else const QStringList parts = cleaned.split(',', Qt::SkipEmptyParts); +#endif for (const QString &part : parts) { bool okv = false; qreal v = part.trimmed().toDouble(&okv); From 7856d81a0dda6d4bb842055a18e46b1c0f0618d1 Mon Sep 17 00:00:00 2001 From: ZhangTingan Date: Mon, 10 Nov 2025 11:32:09 +0800 Subject: [PATCH 2/4] feat: expose XPS support in UI and docs - show PDF, DJVU, DOCX, XPS on the home page banner and app description - include .xps in open dialog filters and desktop MimeType registration - update EN/ZH manuals plus plan constraints to reflect XPS coverage - refresh prompt instructions to avoid touching translation resources Log: as title task: https://pms.uniontech.com/task-view-383459.html --- CMakeLists.txt | 2 +- .../document-viewer/en_US/d_document-viewer.md | 2 +- .../document-viewer/en_US/p_document-viewer.md | 2 +- .../document-viewer/zh_CN/d_document-viewer.md | 2 +- .../document-viewer/zh_CN/p_document-viewer.md | 2 +- reader/Application.cpp | 11 ++++++++--- reader/deepin-reader.desktop | 2 +- reader/uiframe/Central.cpp | 6 +++--- reader/uiframe/CentralNavPage.cpp | 11 +++++++---- 9 files changed, 24 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c95576a..e4635640 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # XPS支持选项(默认关闭) option(XPS_SUPPORT "Enable XPS document format support" OFF) - +set (XPS_SUPPORT ON) if(XPS_SUPPORT) message(">>> XPS support enabled") add_compile_definitions(XPS_SUPPORT_ENABLED) diff --git a/assets/deepin-reader/document-viewer/en_US/d_document-viewer.md b/assets/deepin-reader/document-viewer/en_US/d_document-viewer.md index 954f9063..a5aaec19 100755 --- a/assets/deepin-reader/document-viewer/en_US/d_document-viewer.md +++ b/assets/deepin-reader/document-viewer/en_US/d_document-viewer.md @@ -44,7 +44,7 @@ Document Viewer realizes basic document management such as opening files, saving ### Open files -Document Viewer supports DOCX, PDF and DJVU formats. You can open a file by: +Document Viewer supports DOCX, PDF, DJVU, and XPS formats. You can open a file by: - Dragging it directly into the interface or onto the icon. - Right-clicking it and selecting **Open with** > **Document Viewer**. After setting Document Viewer as the default program, you can open it by double-clicking it directly. diff --git a/assets/deepin-reader/document-viewer/en_US/p_document-viewer.md b/assets/deepin-reader/document-viewer/en_US/p_document-viewer.md index 954f9063..a5aaec19 100755 --- a/assets/deepin-reader/document-viewer/en_US/p_document-viewer.md +++ b/assets/deepin-reader/document-viewer/en_US/p_document-viewer.md @@ -44,7 +44,7 @@ Document Viewer realizes basic document management such as opening files, saving ### Open files -Document Viewer supports DOCX, PDF and DJVU formats. You can open a file by: +Document Viewer supports DOCX, PDF, DJVU, and XPS formats. You can open a file by: - Dragging it directly into the interface or onto the icon. - Right-clicking it and selecting **Open with** > **Document Viewer**. After setting Document Viewer as the default program, you can open it by double-clicking it directly. diff --git a/assets/deepin-reader/document-viewer/zh_CN/d_document-viewer.md b/assets/deepin-reader/document-viewer/zh_CN/d_document-viewer.md index 2b725de3..5df402be 100755 --- a/assets/deepin-reader/document-viewer/zh_CN/d_document-viewer.md +++ b/assets/deepin-reader/document-viewer/zh_CN/d_document-viewer.md @@ -43,7 +43,7 @@ ### 打开文件 -文档查看器支持查看PDF、DJVU和DOCX格式的文件,您可以采用以下方式打开文件。 +文档查看器支持查看 PDF、DJVU、DOCX、XPS 等格式的文件,您可以采用以下方式打开文件。 - 直接将文件拖拽到界面或其图标上。 - 右键单击文件,选择 **打开方式 > 文档查看器**。将文档查看器设为默认打开程序后,可以直接双击打开。 diff --git a/assets/deepin-reader/document-viewer/zh_CN/p_document-viewer.md b/assets/deepin-reader/document-viewer/zh_CN/p_document-viewer.md index 2b725de3..5df402be 100755 --- a/assets/deepin-reader/document-viewer/zh_CN/p_document-viewer.md +++ b/assets/deepin-reader/document-viewer/zh_CN/p_document-viewer.md @@ -43,7 +43,7 @@ ### 打开文件 -文档查看器支持查看PDF、DJVU和DOCX格式的文件,您可以采用以下方式打开文件。 +文档查看器支持查看 PDF、DJVU、DOCX、XPS 等格式的文件,您可以采用以下方式打开文件。 - 直接将文件拖拽到界面或其图标上。 - 右键单击文件,选择 **打开方式 > 文档查看器**。将文档查看器设为默认打开程序后,可以直接双击打开。 diff --git a/reader/Application.cpp b/reader/Application.cpp index 479a34db..0d819fb1 100644 --- a/reader/Application.cpp +++ b/reader/Application.cpp @@ -29,11 +29,16 @@ Application::Application(int &argc, char **argv) setApplicationVersion(DApplication::buildVersion(APP_VERSION)); setApplicationAcknowledgementPage("https://www.deepin.org/acknowledgments/deepin_reader"); setApplicationDisplayName(tr("Document Viewer")); + const QStringList supportedFormats = { + QStringLiteral("PDF"), + QStringLiteral("DJVU"), + QStringLiteral("DOCX") #ifdef XPS_SUPPORT_ENABLED - setApplicationDescription(tr("Document Viewer is a tool for reading document files, supporting PDF, DJVU, DOCX, XPS etc.")); -#else - setApplicationDescription(tr("Document Viewer is a tool for reading document files, supporting PDF, DJVU, DOCX etc.")); + , QStringLiteral("XPS") #endif + }; + setApplicationDescription(tr("Document Viewer is a tool for reading document files, supporting %1.") + .arg(supportedFormats.join(QStringLiteral(", ")))); setProductIcon(QIcon::fromTheme("deepin-reader")); } diff --git a/reader/deepin-reader.desktop b/reader/deepin-reader.desktop index a632c267..6d3d45ff 100644 --- a/reader/deepin-reader.desktop +++ b/reader/deepin-reader.desktop @@ -3,7 +3,7 @@ Categories=Office; Exec=deepin-reader %F GenericName=Document Viewer Icon=deepin-reader -MimeType=application/pdf;image/vnd.djvu;image/vnd.djvu+multipage;application/wps-office.docx;application/vnd.openxmlformats-officedocument.wordprocessingml.document; +MimeType=application/pdf;image/vnd.djvu;image/vnd.djvu+multipage;application/wps-office.docx;application/vnd.openxmlformats-officedocument.wordprocessingml.document;application/vnd.ms-xpsdocument;application/oxps; Name=Document Viewer StartupNotify=true TryExec=deepin-reader diff --git a/reader/uiframe/Central.cpp b/reader/uiframe/Central.cpp index 56fb9a8d..d8448504 100644 --- a/reader/uiframe/Central.cpp +++ b/reader/uiframe/Central.cpp @@ -129,11 +129,11 @@ void Central::addFilesWithDialog() qCDebug(appLog) << "Opening file selection dialog..."; DFileDialog dialog(this); dialog.setFileMode(DFileDialog::ExistingFiles); + QStringList filters = {"*.pdf", "*.djvu", "*.docx"}; #ifdef XPS_SUPPORT_ENABLED - dialog.setNameFilter(tr("Documents") + " (*.pdf *.djvu *.docx *.xps)"); -#else - dialog.setNameFilter(tr("Documents") + " (*.pdf *.djvu *.docx)"); + filters << "*.xps"; #endif + dialog.setNameFilter(tr("Documents") + QStringLiteral(" (") + filters.join(' ') + QLatin1Char(')')); dialog.setDirectory(QDir::homePath()); if (QDialog::Accepted != dialog.exec()) { diff --git a/reader/uiframe/CentralNavPage.cpp b/reader/uiframe/CentralNavPage.cpp index f3ec87a6..f3458ad0 100644 --- a/reader/uiframe/CentralNavPage.cpp +++ b/reader/uiframe/CentralNavPage.cpp @@ -25,13 +25,16 @@ CentralNavPage::CentralNavPage(DWidget *parent) tipsLabel->setForegroundRole(DPalette::TextTips); DFontSizeManager::instance()->bind(tipsLabel, DFontSizeManager::T8); + constexpr auto kFormatsWithXps = "PDF,DJVU,DOCX,XPS"; + constexpr auto kFormatsWithoutXps = "PDF,DJVU,DOCX"; #ifdef XPS_SUPPORT_ENABLED - auto formatLabel = new DLabel(tr("Format supported: %1").arg("PDF,DJVU,DOCX,XPS"), this); - formatLabel->setAccessibleName(QString("Label_format supported: %1").arg("PDF,DJVU,DOCX,XPS")); + auto supportedFormats = QString::fromLatin1(kFormatsWithXps); #else - auto formatLabel = new DLabel(tr("Format supported: %1").arg("PDF,DJVU,DOCX"), this); - formatLabel->setAccessibleName(QString("Label_format supported: %1").arg("PDF,DJVU,DOCX")); + auto supportedFormats = QString::fromLatin1(kFormatsWithoutXps); #endif + + auto formatLabel = new DLabel(tr("Format supported: %1").arg(supportedFormats), this); + formatLabel->setAccessibleName(QString("Label_format supported: %1").arg(supportedFormats)); formatLabel->setAlignment(Qt::AlignHCenter); formatLabel->setForegroundRole(DPalette::TextTips); DFontSizeManager::instance()->bind(formatLabel, DFontSizeManager::T8); From ffd054617ebdd75e84c456f91e849c744d14c7f3 Mon Sep 17 00:00:00 2001 From: ZhangTingan Date: Mon, 10 Nov 2025 13:15:17 +0800 Subject: [PATCH 3/4] feat: [xps] wire libgxps dependencies into build pipelines detect libgxps/cairo/glib via pkg-config and auto-toggle XPS_SUPPORT link reader target with gxps stack and propagate include/compile flags declare runtime/build deps in debian control and linglong manifest document clean reconfigure path to refresh resolved XPS availability Log: as title task: https://pms.uniontech.com/task-view-383459.html --- CMakeLists.txt | 41 ++++++++++++++++++++++++++++++------- debian/control | 7 +++++-- linglong.yaml | 2 +- reader/CMakeLists.txt | 47 +++++++++++++++++++++++++------------------ 4 files changed, 67 insertions(+), 30 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e4635640..f4a61ca9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,14 +11,41 @@ project(deepin-reader set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# XPS支持选项(默认关闭) -option(XPS_SUPPORT "Enable XPS document format support" OFF) -set (XPS_SUPPORT ON) -if(XPS_SUPPORT) - message(">>> XPS support enabled") - add_compile_definitions(XPS_SUPPORT_ENABLED) +# XPS支持选项(根据依赖检测自动启用) +find_package(PkgConfig REQUIRED) +option(XPS_SUPPORT "Enable XPS document format support" ON) + +set(XPS_SUPPORT_RESOLVED ${XPS_SUPPORT}) +set(XPS_DEPS_FOUND OFF) + +if (XPS_SUPPORT) + if (PkgConfig_FOUND) + pkg_check_modules(XPS_DEPS QUIET libgxps cairo glib-2.0 gobject-2.0) + if (XPS_DEPS_FOUND) + message(STATUS ">>> XPS support enabled (libgxps dependencies detected)") + add_compile_definitions(XPS_SUPPORT_ENABLED) + set(XPS_SUPPORT_RESOLVED ON) + else() + message(WARNING ">>> XPS support disabled: missing libgxps/cairo/glib-2.0/gobject-2.0 dependencies") + set(XPS_SUPPORT_RESOLVED OFF) + endif() + else() + message(WARNING ">>> XPS support disabled: pkg-config not available") + set(XPS_SUPPORT_RESOLVED OFF) + endif() else() - message(">>> XPS support disabled") + message(STATUS ">>> XPS support disabled by configuration") +endif() + +set(XPS_SUPPORT_ENABLED ${XPS_SUPPORT_RESOLVED}) +if (XPS_SUPPORT_ENABLED) + set(XPS_DEPS_INCLUDE_DIRS ${XPS_DEPS_INCLUDE_DIRS}) + set(XPS_DEPS_LIBRARIES ${XPS_DEPS_LIBRARIES}) + set(XPS_DEPS_CFLAGS_OTHER ${XPS_DEPS_CFLAGS_OTHER}) +else() + unset(XPS_DEPS_INCLUDE_DIRS) + unset(XPS_DEPS_LIBRARIES) + unset(XPS_DEPS_CFLAGS_OTHER) endif() include(GNUInstallDirs) diff --git a/debian/control b/debian/control index 95f2689f..03492295 100644 --- a/debian/control +++ b/debian/control @@ -15,7 +15,10 @@ Build-Depends: libdtk6widget-dev [!mipsel !mips64el] | libdtkwidget-dev, libdtk6gui-dev [!mipsel !mips64el] | libdtkgui-dev, libdtk6core-dev [!mipsel !mips64el] | libdtkcore-dev, - libdeepin-pdfium-dev (>= 1.5.1) [!mipsel !mips64el] | libdeepin-pdfium5-dev (>= 1.5.1), + libdeepin-pdfium-dev [!mipsel !mips64el] | libdeepin-pdfium-dev, + libgxps-dev, + libcairo2-dev, + libglib2.0-dev, libdjvulibre-dev, libtiff-dev, libjpeg-dev, @@ -35,6 +38,6 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, pandoc, - libqt6webenginecore6-bin [!mipsel !mips64el] | libqt5core5a + libqt6webenginecore6-bin [!mipsel !mips64el] | libqt5core5a, Description: a tool for reading document files. Document Viewer is a tool for reading document files, supporting PDF, DJVU, DOCX etc. diff --git a/linglong.yaml b/linglong.yaml index 346ece61..9820b034 100644 --- a/linglong.yaml +++ b/linglong.yaml @@ -79,7 +79,7 @@ build: | sources: # linglong:gen_deb_source sources amd64 http://10.20.64.92:8080/crimson_25.1 stable main - # linglong:gen_deb_source install qt6-base-dev, qt6-base-dev-tools, qt6-tools-dev, libdtk6widget-dev, libspectre-dev, libdjvulibre-dev, libtiff-dev, libjpeg-dev, libicu-dev, libpng-dev, zlib1g-dev, liblcms2-dev, libopenjp2-7-dev, libfreetype6-dev, libgtest-dev, libchardet-dev, qt6-webengine-dev, qt6-5compat-dev, libdtk6gui-dev, libdtk6core-dev, qt6-svg-dev, pandoc, libdeepin-pdfium-dev + # linglong:gen_deb_source install qt6-base-dev, qt6-base-dev-tools, qt6-tools-dev, libdtk6widget-dev, libspectre-dev, libdjvulibre-dev, libtiff-dev, libjpeg-dev, libicu-dev, libpng-dev, zlib1g-dev, liblcms2-dev, libopenjp2-7-dev, libfreetype6-dev, libgtest-dev, libchardet-dev, qt6-webengine-dev, qt6-5compat-dev, libdtk6gui-dev, libdtk6core-dev, qt6-svg-dev, pandoc, libdeepin-pdfium-dev, libgxps-dev, libcairo2-dev, libglib2.0-dev, libgobject-2.0-dev - kind: file url: http://10.20.64.92:8080/crimson_25.1/pool/main/q/qt6-tools/assistant-qt6_6.8.0-0deepin3_amd64.deb digest: 8b5df0b28f8a7cfc63a1ba4ac369f09a66e19d47a462c4bc8fa8bc552f18a714 diff --git a/reader/CMakeLists.txt b/reader/CMakeLists.txt index 0e1159fd..b5036c5f 100644 --- a/reader/CMakeLists.txt +++ b/reader/CMakeLists.txt @@ -3,6 +3,9 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(LIBJPEG REQUIRED libjpeg) pkg_check_modules(DDJVU REQUIRED ddjvuapi) pkg_check_modules(PDFIUM REQUIRED deepin-pdfium${PDF_LIB_VERSION}) +if (XPS_SUPPORT_ENABLED) + pkg_check_modules(XPS_DEPS REQUIRED libgxps cairo glib-2.0 gobject-2.0) +endif() # 添加位置无关代码编译标志 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIE") @@ -75,6 +78,7 @@ target_include_directories(deepin-reader PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/uiframe ${CMAKE_CURRENT_SOURCE_DIR}/widgets ${PDFIUM_INCLUDE_DIRS} + $<$:${XPS_DEPS_INCLUDE_DIRS}> ) if (NOT APP_VERSION) @@ -87,35 +91,38 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE APP_VERSION="${APP_VERSION}" ) +if (XPS_SUPPORT_ENABLED) + target_compile_options(deepin-reader PRIVATE ${XPS_DEPS_CFLAGS_OTHER}) +endif() + # 添加资源文件 -if (QT_VERSION_MAJOR MATCHES 6) - # 链接库 -target_link_libraries(deepin-reader PRIVATE +set(READER_LINK_LIBS ${LINK_LIBS} ${LIBJPEG_LIBRARIES} ${DDJVU_LIBRARIES} ${PDFIUM_LIBRARIES} ) -qt_add_resources(reader_RESOURCES - ${CMAKE_SOURCE_DIR}/resources/resources.qrc -) - -else() +if (XPS_SUPPORT_ENABLED) + list(APPEND READER_LINK_LIBS ${XPS_DEPS_LIBRARIES}) +endif() -# 链接库 -target_link_libraries(deepin-reader PRIVATE - ${LINK_LIBS} - ${LIBJPEG_LIBRARIES} - ${DDJVU_LIBRARIES} - ${PDFIUM_LIBRARIES} - dl - pthread -) +if (QT_VERSION_MAJOR MATCHES 6) + target_link_libraries(deepin-reader PRIVATE ${READER_LINK_LIBS}) -qt5_add_resources(reader_RESOURCES - ${CMAKE_SOURCE_DIR}/resources/resources.qrc -) + qt_add_resources(reader_RESOURCES + ${CMAKE_SOURCE_DIR}/resources/resources.qrc + ) +else() + target_link_libraries(deepin-reader PRIVATE + ${READER_LINK_LIBS} + dl + pthread + ) + + qt5_add_resources(reader_RESOURCES + ${CMAKE_SOURCE_DIR}/resources/resources.qrc + ) endif() target_sources(deepin-reader PRIVATE ${reader_RESOURCES}) From 69a2c00277b68054aa7db9f28ecaee8d1a82e198 Mon Sep 17 00:00:00 2001 From: ZhangTingan Date: Mon, 10 Nov 2025 14:42:07 +0800 Subject: [PATCH 4/4] feat: [xps] replace legacy parser with libgxps adapter - add XpsDocumentAdapter to manage libgxps/cairo handles via RAII - remove the old manual XpsDocument implementation and update document.pri - hook the adapter into DocumentFactory to load XPS files - normalize parsed page sizes with gxps_page_get_size fallback - clamp render task dimensions in BrowserPage/PageRenderThread to avoid blank frames task:https://pms.uniontech.com/task-view-383459.html --- reader/browser/BrowserPage.cpp | 14 +- reader/browser/PageRenderThread.cpp | 16 +- reader/document/Model.cpp | 4 +- reader/document/XpsDocument.cpp | 948 ------------------------- reader/document/XpsDocument.h | 131 ---- reader/document/XpsDocumentAdapter.cpp | 788 ++++++++++++++++++++ reader/document/XpsDocumentAdapter.h | 85 +++ reader/document/document.pri | 4 +- 8 files changed, 900 insertions(+), 1090 deletions(-) delete mode 100644 reader/document/XpsDocument.cpp delete mode 100644 reader/document/XpsDocument.h create mode 100644 reader/document/XpsDocumentAdapter.cpp create mode 100644 reader/document/XpsDocumentAdapter.h diff --git a/reader/browser/BrowserPage.cpp b/reader/browser/BrowserPage.cpp index d7afae2c..53c89277 100644 --- a/reader/browser/BrowserPage.cpp +++ b/reader/browser/BrowserPage.cpp @@ -251,9 +251,10 @@ void BrowserPage::render(const double &scaleFactor, const Dr::Rotation &rotation task.pixmapId = m_pixmapId; - task.rect = QRect(0, 0, - static_cast(boundingRect().width() * dApp->devicePixelRatio()), - static_cast(boundingRect().height() * dApp->devicePixelRatio())); + const qreal deviceRatio = dApp ? dApp->devicePixelRatio() : 1.0; + const int targetWidth = qMax(1, qRound(boundingRect().width() * deviceRatio)); + const int targetHeight = qMax(1, qRound(boundingRect().height() * deviceRatio)); + task.rect = QRect(0, 0, targetWidth, targetHeight); PageRenderThread::appendTask(task); } else { @@ -266,9 +267,10 @@ void BrowserPage::render(const double &scaleFactor, const Dr::Rotation &rotation task.pixmapId = m_pixmapId; - task.rect = QRect(0, 0, - static_cast(boundingRect().width() * dApp->devicePixelRatio()), - static_cast(boundingRect().height() * dApp->devicePixelRatio())); + const qreal deviceRatio = dApp ? dApp->devicePixelRatio() : 1.0; + const int targetWidth = qMax(1, qRound(boundingRect().width() * deviceRatio)); + const int targetHeight = qMax(1, qRound(boundingRect().height() * deviceRatio)); + task.rect = QRect(0, 0, targetWidth, targetHeight); PageRenderThread::appendTask(task); } diff --git a/reader/browser/PageRenderThread.cpp b/reader/browser/PageRenderThread.cpp index 32617604..85a1d6bd 100644 --- a/reader/browser/PageRenderThread.cpp +++ b/reader/browser/PageRenderThread.cpp @@ -9,6 +9,7 @@ #include "DocSheet.h" #include "SheetRenderer.h" #include "SideBarImageViewModel.h" +#include "Application.h" #include "ddlog.h" #include @@ -594,7 +595,20 @@ bool PageRenderThread::execNextDocPageNormalImageTask() return true; } - QImage image = task.sheet->getImage(task.page->itemIndex(), task.rect.width(), task.rect.height()); + int targetWidth = task.rect.width(); + int targetHeight = task.rect.height(); + if (targetWidth <= 0 || targetHeight <= 0) { + const qreal deviceRatio = dApp ? dApp->devicePixelRatio() : 1.0; + const double pageScale = (task.page && task.page->m_scaleFactor > 0.0) + ? task.page->m_scaleFactor + : (task.sheet ? task.sheet->operation().scaleFactor : 1.0); + const QSizeF pageSize = task.page ? task.page->m_originSizeF : QSizeF(); + targetWidth = qMax(1, qRound(pageSize.width() * pageScale * deviceRatio)); + targetHeight = qMax(1, qRound(pageSize.height() * pageScale * deviceRatio)); + task.rect = QRect(0, 0, targetWidth, targetHeight); + } + + QImage image = task.sheet->getImage(task.page->itemIndex(), targetWidth, targetHeight); if (image.isNull()) { qCWarning(appLog) << "Failed to get image for page:" << task.page->itemIndex(); diff --git a/reader/document/Model.cpp b/reader/document/Model.cpp index 7e3f6f71..f8401564 100755 --- a/reader/document/Model.cpp +++ b/reader/document/Model.cpp @@ -4,7 +4,7 @@ #include "Model.h" #ifdef XPS_SUPPORT_ENABLED -#include "XpsDocument.h" +#include "XpsDocumentAdapter.h" #endif #include "PDFModel.h" #include "DjVuModel.h" @@ -68,7 +68,7 @@ deepin_reader::Document *deepin_reader::DocumentFactory::getDocument(const int & #ifdef XPS_SUPPORT_ENABLED } else if (Dr::XPS == fileType) { qCDebug(appLog) << "Handling XPS document"; - document = deepin_reader::XpsDocument::loadDocument(filePath, error); + document = deepin_reader::XpsDocumentAdapter::loadDocument(filePath, error); #endif } else if (Dr::DOCX == fileType) { qCDebug(appLog) << "Starting DOCX document conversion process"; diff --git a/reader/document/XpsDocument.cpp b/reader/document/XpsDocument.cpp deleted file mode 100644 index cff0e6a3..00000000 --- a/reader/document/XpsDocument.cpp +++ /dev/null @@ -1,948 +0,0 @@ -// Copyright (C) 2019 ~ 2020 Uniontech Software Technology Co.,Ltd. -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "XpsDocument.h" -#include "ddlog.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -namespace deepin_reader { - -XpsPage::XpsPage(const XpsPageData &pageData, QMutex *mutex) - : m_pageData(pageData), m_docMutex(mutex) -{ - // qCDebug(appLog) << "XpsPage::XpsPage() - Starting constructor"; - // qCDebug(appLog) << "XpsPage::XpsPage() - Constructor completed"; -} - -XpsPage::~XpsPage() -{ - // qCDebug(appLog) << "XpsPage::~XpsPage() - Starting destructor"; - // qCDebug(appLog) << "XpsPage::~XpsPage() - Destructor completed"; -} - -QSizeF XpsPage::sizeF() const -{ - // qCDebug(appLog) << "XpsPage::sizeF() - Getting page size"; - QSizeF size = m_pageData.size; - // qCDebug(appLog) << "XpsPage::sizeF() - Returning size:" << size; - return size; -} - -QImage XpsPage::render(int width, int height, const QRect &slice) const -{ - // qCDebug(appLog) << "XpsPage::render() - Starting render"; - QMutexLocker lock(m_docMutex); - - QImage image(width, height, QImage::Format_RGB32); - image.fill(Qt::white); - - QPainter painter(&image); - painter.setRenderHint(QPainter::Antialiasing); - painter.setRenderHint(QPainter::SmoothPixmapTransform); - - const qreal margin = 10.0; - const qreal pageW = m_pageData.size.width(); - const qreal pageH = m_pageData.size.height(); - - const qreal contentW = qMax(1.0, width - 2.0 * margin); - const qreal contentH = qMax(1.0, height - 2.0 * margin); - - const qreal scaleX = contentW / pageW; - const qreal scaleY = contentH / pageH; - const qreal scale = qMin(scaleX, scaleY); - - const qreal drawW = pageW * scale; - const qreal drawH = pageH * scale; - const qreal offsetX = (width - drawW) / 2.0; - const qreal offsetY = (height - drawH) / 2.0; - - painter.translate(offsetX, offsetY); - painter.scale(scale, scale); - - if (!slice.isNull()) { - QRectF pageClip(slice); - pageClip = pageClip.intersected(QRectF(0, 0, pageW, pageH)); - if (!pageClip.isEmpty()) { - painter.setClipRect(pageClip); - painter.translate(-pageClip.topLeft()); - } - } - - // 保留最小化日志(如需更详细请在本处加回调试输出) - - for (int i = 0; i < m_pageData.paths.size(); ++i) { - const QPainterPath &path = m_pageData.paths[i]; - if (i == 0) { - painter.setPen(QPen(Qt::red, 2)); - painter.setBrush(QBrush(Qt::red)); - } else if (i == 1) { - painter.setPen(QPen(Qt::green, 2)); - painter.setBrush(QBrush(Qt::green)); - } else { - painter.setPen(QPen(Qt::black, 1)); - painter.setBrush(Qt::NoBrush); - } - - painter.drawPath(path); - } - - for (const auto &imgDraw : m_pageData.images) { - if (!imgDraw.image.isNull()) { - painter.drawImage(imgDraw.rect, imgDraw.image); - } - } - - for (int i = 0; i < m_pageData.textStrings.size(); ++i) { - const QString &text = m_pageData.textStrings[i]; - if (text.trimmed().isEmpty()) continue; - QPointF baseline = i < m_pageData.textOrigins.size() ? m_pageData.textOrigins[i] : QPointF(); - - QColor textColor = (i < m_pageData.textColors.size()) ? m_pageData.textColors[i] : Qt::black; - qreal fontSizeF = (i < m_pageData.textFontSizes.size()) ? m_pageData.textFontSizes[i] : 12.0; - - QFont font = painter.font(); - font.setPointSizeF(fontSizeF); - font.setKerning(false); - font.setLetterSpacing(QFont::PercentageSpacing, 100.0); - if (i < m_pageData.textFontFamilies.size() && !m_pageData.textFontFamilies[i].isEmpty()) { - font.setFamily(m_pageData.textFontFamilies[i]); - } - painter.setFont(font); - painter.setPen(textColor); - - painter.setBrush(Qt::NoBrush); - painter.setPen(textColor); - - qreal widthPx = painter.fontMetrics().horizontalAdvance(text); - qreal heightPx = painter.fontMetrics().height(); - qreal widthPage = widthPx / scale; - qreal heightPage = heightPx / scale; - - qreal indicesWidthPage = 0.0; - if (i < m_pageData.textAdvances.size() && !m_pageData.textAdvances[i].isEmpty()) { - const QVector &adv = m_pageData.textAdvances[i]; - qreal x = baseline.x(); - const int n = text.size(); - const qreal desiredExtraTrackingEm = 0.10; - - QVector baseCharWidths; - baseCharWidths.reserve(n); - qreal baseSum = 0.0; - for (int k = 0; k < n; ++k) { - const QString ch = text.mid(k, 1); - qreal measured = painter.fontMetrics().horizontalAdvance(ch) / scale; - qreal a = (k < adv.size()) ? adv[k] : 0.0; - qreal fromIndices = (a / 100.0) * fontSizeF; - qreal w = qMax(measured, fromIndices); - baseCharWidths.append(w); - baseSum += w; - } - - qreal nextRunX = std::numeric_limits::quiet_NaN(); - if (i + 1 < m_pageData.textOrigins.size()) { - const qreal y = baseline.y(); - for (int j = i + 1; j < m_pageData.textOrigins.size(); ++j) { - if (m_pageData.textStrings[j].trimmed().isEmpty()) continue; - if (qAbs(m_pageData.textOrigins[j].y() - y) < 0.6) { - nextRunX = m_pageData.textOrigins[j].x(); - break; - } else if (m_pageData.textOrigins[j].y() > y + 2.0) { - break; - } - } - } - - qreal extraPerChar = desiredExtraTrackingEm * fontSizeF; - const qreal interRunGap = 0.10 * fontSizeF; - if (!std::isnan(nextRunX)) { - qreal allowed = nextRunX - interRunGap - baseline.x(); - if (allowed > 0) { - qreal room = allowed - baseSum; - if (room <= 0) { - extraPerChar = 0.0; - } else { - extraPerChar = qMin(extraPerChar, room / qMax(1, n)); - } - } - } - - for (int k = 0; k < n; ++k) { - const QString ch = text.mid(k, 1); - painter.drawText(QPointF(x, baseline.y()), ch); - - qreal advancePage = baseCharWidths[k] + extraPerChar; - - QChar currentChar = ch.at(0); - if (currentChar.unicode() >= 0x4E00 && currentChar.unicode() <= 0x9FFF) { - qreal chineseBonus = 0.02 * fontSizeF; - if (extraPerChar > 0) { - advancePage += chineseBonus; - } - } - - x += advancePage; - indicesWidthPage += advancePage; - } - } else { - // 检查是否包含中文字符 - bool containsChinese = false; - for (int k = 0; k < text.size(); ++k) { - QChar ch = text.at(k); - if (ch.unicode() >= 0x4E00 && ch.unicode() <= 0x9FFF) { - containsChinese = true; - break; - } - } - - QFont spaced = font; - qreal letterSpacingPercent = containsChinese ? 112.0 : 108.0; - spaced.setLetterSpacing(QFont::PercentageSpacing, letterSpacingPercent); - painter.setFont(spaced); - painter.drawText(baseline, text); - indicesWidthPage = widthPage; - } - } - // qCDebug(appLog) << "XpsPage::render() - Render completed"; - return image; -} - -Link XpsPage::getLinkAtPoint(const QPointF &pos) -{ - // qCDebug(appLog) << "XpsPage::getLinkAtPoint() - Getting link at point:" << pos; - // XPS暂不支持链接 - return Link(); -} - -QString XpsPage::text(const QRectF &rect) const -{ - // qCDebug(appLog) << "XpsPage::text() - Getting text from rect:" << rect; - QString result; - for (int i = 0; i < m_pageData.textRects.size() && i < m_pageData.textStrings.size(); ++i) { - if (m_pageData.textRects[i].intersects(rect)) { - result += m_pageData.textStrings[i] + " "; - } - } - // qCDebug(appLog) << "XpsPage::text() - Returning text length:" << result.length(); - return result.trimmed(); -} - -QVector XpsPage::search(const QString &searchText, bool matchCase, bool wholeWords) const -{ - // qCDebug(appLog) << "XpsPage::search() - Starting search"; - QVector results; - - QString searchPattern = searchText; - if (!matchCase) { - searchPattern = searchPattern.toLower(); - } - - for (int i = 0; i < m_pageData.textStrings.size(); ++i) { - QString text = m_pageData.textStrings[i]; - if (!matchCase) { - text = text.toLower(); - } - - if (wholeWords) { - QRegularExpression regex(QString("\\b%1\\b").arg(QRegularExpression::escape(searchPattern))); - if (regex.match(text).hasMatch()) { - PageSection section; - PageLine line; - line.rect = m_pageData.textRects[i]; - line.text = m_pageData.textStrings[i]; - section.append(line); - results.append(section); - } - } else { - if (text.contains(searchPattern)) { - PageSection section; - PageLine line; - line.rect = m_pageData.textRects[i]; - line.text = m_pageData.textStrings[i]; - section.append(line); - results.append(section); - } - } - } - - // qCDebug(appLog) << "XpsPage::search() - Search completed, found" << results.size() << "matches"; - return results; -} - -QList XpsPage::annotations() const -{ - // qCDebug(appLog) << "XpsPage::annotations() - Getting annotations"; - // XPS暂不支持注释 - return QList(); -} - -QList XpsPage::words() -{ - // qCDebug(appLog) << "XpsPage::words() - Starting word extraction"; - if (m_wordLoaded) { - // qCDebug(appLog) << "XpsPage::words() - Words already loaded, returning cached words"; - return m_words; - } - - m_words.clear(); - for (int i = 0; i < m_pageData.textStrings.size(); ++i) { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - QStringList wordList = m_pageData.textStrings[i].split(QRegularExpression("\\s+"), QString::SkipEmptyParts); -#else - QStringList wordList = m_pageData.textStrings[i].split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); -#endif - for (const QString &word : wordList) { - if (!word.isEmpty()) { - m_words.append(Word(word, m_pageData.textRects[i])); - } - } - } - - m_wordLoaded = true; - // qCDebug(appLog) << "XpsPage::words() - Word extraction completed, total words:" << m_words.size(); - return m_words; -} - -// XpsDocument实现 -XpsDocument::XpsDocument(const QString &filePath) - : m_filePath(filePath), m_loaded(false) -{ - qCInfo(appLog) << "Creating XPS document for:" << filePath; - loadXpsFile(); - qCDebug(appLog) << "XpsDocument::XpsDocument() - Constructor completed"; -} - -QByteArray XpsDocument::deobfuscateOdttf(const QByteArray &data, const QString &fontUri) -{ - qCDebug(appLog) << "XpsDocument::deobfuscateOdttf() - Deobfuscating font data for:" << fontUri; - QRegularExpression re("([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})"); - QRegularExpressionMatch m = re.match(fontUri); - if (!m.hasMatch() || data.size() < 32) { - qCDebug(appLog) << "XpsDocument::deobfuscateOdttf() - No GUID match or data too small, returning original data"; - return data; - } - QString guid = m.captured(1); - guid.remove('-'); - if (guid.size() != 32) { - return data; - } - QByteArray key; - key.reserve(16); - for (int i = 0; i < 16; ++i) { - bool ok = false; - char b = static_cast(guid.mid(i * 2, 2).toUInt(&ok, 16)); - key.append(ok ? b : '\0'); - } - QByteArray out = data; - for (int i = 0; i < 32 && i < out.size(); ++i) { - out[i] = out[i] ^ key[i % 16]; - } - qCDebug(appLog) << "XpsDocument::deobfuscateOdttf() - Font data deobfuscated"; - return out; -} - -QString XpsDocument::resolveFontFamily(const QString &fontUri) -{ - qCDebug(appLog) << "XpsDocument::resolveFontFamily() - Resolving font family for:" << fontUri; - if (fontUri.isEmpty()) return QString(); - if (m_fontFamilyCache.contains(fontUri)) { - return m_fontFamilyCache.value(fontUri); - } - QString key = fontUri; - if (key.startsWith('/')) key = key.mid(1); - if (!m_archiveFiles.contains(key)) return QString(); - QByteArray raw = m_archiveFiles.value(key); - QByteArray real = deobfuscateOdttf(raw, fontUri); - int fontId = QFontDatabase::addApplicationFontFromData(real); - if (fontId == -1) { - qCWarning(appLog) << "Failed to add embedded font from" << key; - return QString(); - } - m_loadedFontIds.append(fontId); - const QStringList fams = QFontDatabase::applicationFontFamilies(fontId); - if (fams.isEmpty()) return QString(); - const QString fam = fams.first(); - m_fontFamilyCache.insert(fontUri, fam); - qCInfo(appLog) << "Loaded embedded font" << fontUri << "-> family" << fam; - return fam; -} - -XpsDocument::~XpsDocument() -{ - // qCDebug(appLog) << "XpsDocument::~XpsDocument() - Starting destructor"; - // qCDebug(appLog) << "XpsDocument::~XpsDocument() - Destructor completed"; -} - -int XpsDocument::pageCount() const -{ - // qCDebug(appLog) << "XpsDocument::pageCount() - Getting page count"; - int count = m_pages.size(); - // qCDebug(appLog) << "XpsDocument::pageCount() - Page count:" << count; - return count; -} - -Page *XpsDocument::page(int index) const -{ - // qCDebug(appLog) << "XpsDocument::page() - Getting page at index:" << index; - if (index >= 0 && index < m_pages.size()) { - // qCDebug(appLog) << "XpsDocument::page() - Valid index, creating XpsPage"; - return new XpsPage(m_pages[index], const_cast(&m_mutex)); - } - // qCWarning(appLog) << "XpsDocument::page() - Invalid index:" << index << "total pages:" << m_pages.size(); - return nullptr; -} - -QStringList XpsDocument::saveFilter() const -{ - qCDebug(appLog) << "XpsDocument::saveFilter() - Returning save filter"; - return QStringList() << "XPS files (*.xps)"; -} - -bool XpsDocument::save() const -{ - qCDebug(appLog) << "XpsDocument::save() - Saving XPS document"; - // XPS暂不支持保存 - return false; -} - -bool XpsDocument::saveAs(const QString &filePath) const -{ - qCDebug(appLog) << "XpsDocument::saveAs() - Saving XPS document as:" << filePath; - // XPS暂不支持另存为 - Q_UNUSED(filePath) - return false; -} - -Outline XpsDocument::outline() const -{ - qCDebug(appLog) << "XpsDocument::outline() - Returning outline"; - // XPS暂不支持大纲 - return Outline(); -} - -Properties XpsDocument::properties() const -{ - qCDebug(appLog) << "XpsDocument::properties() - Returning properties"; - Properties props; - props["Title"] = m_title; - props["Author"] = m_author; - props["Subject"] = m_subject; - props["Creator"] = m_creator; - props["CreationDate"] = m_creationDate; - props["ModificationDate"] = m_modificationDate; - props["PageCount"] = m_pages.size(); - props["Width"] = m_documentSize.width(); - props["Height"] = m_documentSize.height(); - return props; -} - -XpsDocument *XpsDocument::loadDocument(const QString &filePath, Document::Error &error) -{ - qCInfo(appLog) << "Loading XPS document:" << filePath; - - XpsDocument *doc = new XpsDocument(filePath); - if (doc->m_loaded) { - qCDebug(appLog) << "XpsDocument::loadDocument() - XPS document loaded successfully"; - error = Document::NoError; - return doc; - } else { - qCDebug(appLog) << "XpsDocument::loadDocument() - XPS document loaded failed"; - error = Document::FileError; - delete doc; - return nullptr; - } -} - -bool XpsDocument::loadXpsFile() -{ - qCInfo(appLog) << "Loading XPS file:" << m_filePath; - - // 创建临时目录来解压XPS文件 - QTemporaryDir tempDir; - if (!tempDir.isValid()) { - qCritical() << "Failed to create temporary directory for XPS extraction"; - return false; - } - - // 使用系统unzip命令解压XPS文件 - QProcess unzipProcess; - unzipProcess.setWorkingDirectory(tempDir.path()); - - QStringList arguments; - arguments << "-q" << "-o" << QFileInfo(m_filePath).absoluteFilePath(); // 使用绝对路径 - - unzipProcess.start("unzip", arguments); - - if (!unzipProcess.waitForStarted()) { - qCritical() << "Failed to start unzip process"; - return false; - } - - if (!unzipProcess.waitForFinished()) { - qCritical() << "Unzip process failed"; - return false; - } - - if (unzipProcess.exitCode() != 0) { - qCritical() << "Unzip process failed with exit code:" << unzipProcess.exitCode(); - qCritical() << "Unzip error output:" << unzipProcess.readAllStandardError(); - return false; - } - - // 读取解压后的文件,包括子目录 - QDir extractedDir(tempDir.path()); - QFileInfoList allFiles; - - // 递归获取所有文件 - // 仅在需要时查看临时目录 - - - // 先检查临时目录中的内容 - QDir tempDirObj(tempDir.path()); - QFileInfoList tempContents = tempDirObj.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot); - for (const QFileInfo &item : tempContents) { - - } - - // 使用递归函数来获取所有文件 - QList> fileList; // - collectFilesRecursively(tempDir.path(), "", fileList); - - - for (const auto &filePair : fileList) { - - } - - for (const auto &filePair : fileList) { - QString relativePath = filePair.first; - QString absolutePath = filePair.second; - - QFile file(absolutePath); - if (file.open(QIODevice::ReadOnly)) { - - - QByteArray fileData = file.readAll(); - m_archiveFiles[relativePath] = fileData; - - file.close(); - } - } - - - - parseXpsContent(); - m_loaded = true; - return true; -} - -void XpsDocument::collectFilesRecursively(const QString &dirPath, const QString &relativePath, QList> &fileList) -{ - qCDebug(appLog) << "XpsDocument::collectFilesRecursively() - Collecting files recursively from:" << dirPath << "with relative path:" << relativePath; - QDir dir(dirPath); - QFileInfoList entries = dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot); - - for (const QFileInfo &entry : entries) { - QString newRelativePath = relativePath.isEmpty() ? entry.fileName() : relativePath + "/" + entry.fileName(); - - if (entry.isFile()) { - fileList.append(qMakePair(newRelativePath, entry.absoluteFilePath())); - } else if (entry.isDir()) { - collectFilesRecursively(entry.absoluteFilePath(), newRelativePath, fileList); - } - } - qCDebug(appLog) << "XpsDocument::collectFilesRecursively() - Collected files recursively from:" << dirPath << "with relative path:" << relativePath; -} - -void XpsDocument::parseXpsContent() -{ - qCInfo(appLog) << "Parsing XPS content"; - - // 查找 fdseq:兼容 FixedDocumentSequence.fdseq 与 FixedDocSeq.fdseq,且不限定具体目录 - QString fdseqKey; - const QStringList keys = m_archiveFiles.keys(); - for (const QString &key : keys) { - const QString base = QFileInfo(key).fileName(); - if (base.compare("FixedDocumentSequence.fdseq", Qt::CaseInsensitive) == 0 || - base.compare("FixedDocSeq.fdseq", Qt::CaseInsensitive) == 0) { - fdseqKey = key; - break; - } - } - if (fdseqKey.isEmpty()) { - qCWarning(appLog) << "fdseq not found. Available keys:" << keys; - return; - } - - - QByteArray fdseqData = m_archiveFiles[fdseqKey]; - QXmlStreamReader reader(fdseqData); - - QString documentRef; - while (!reader.atEnd()) { - QXmlStreamReader::TokenType token = reader.readNext(); - if (token == QXmlStreamReader::StartElement) { - if (reader.name() == "DocumentReference") { - QXmlStreamAttributes attrs = reader.attributes(); - documentRef = attrs.value("Source").toString(); - break; - } - } - } - - if (documentRef.isEmpty()) { - qCWarning(appLog) << "Document reference not found"; - return; - } - - // 解析主文档 - // 移除开头的斜杠,因为我们的文件键不包含开头的斜杠 - QString docKey = documentRef; - if (docKey.startsWith("/")) { - docKey = docKey.mid(1); - } - - if (!m_archiveFiles.contains(docKey)) { - qCWarning(appLog) << "Main document not found:" << docKey << "(original:" << documentRef << ")"; - - return; - } - - QByteArray docData = m_archiveFiles[docKey]; - QXmlStreamReader docReader(docData); - - while (!docReader.atEnd()) { - QXmlStreamReader::TokenType token = docReader.readNext(); - if (token == QXmlStreamReader::StartElement) { - if (docReader.name() == "PageContent") { - QXmlStreamAttributes attrs = docReader.attributes(); - QString pageSource = attrs.value("Source").toString(); - - // 解析页面路径: - // - 以 '/' 开头:包内绝对路径,直接去掉开头的 '/' - // - 否则:相对于主文档 FixedDoc.fdoc 所在目录 - QString pageKey; - if (pageSource.startsWith('/')) { - pageKey = pageSource.mid(1); - } else { - const QString baseDir = QFileInfo(docKey).path(); - pageKey = QDir(baseDir).filePath(pageSource); - // 统一为正斜杠 - pageKey.replace("\\", "/"); - } - - // 解析页面 - if (m_archiveFiles.contains(pageKey)) { - qCInfo(appLog) << "Parsing page:" << pageKey; - XpsPageData pageInfo; - parsePageContent(m_archiveFiles[pageKey], pageInfo); - m_pages.append(pageInfo); - } else { - qCWarning(appLog) << "Page source not found:" << pageKey; - - } - } - } - } - - // 设置文档属性 - if (!m_pages.isEmpty()) { - qCDebug(appLog) << "XpsDocument::parseXpsContent() - Setting document size"; - m_documentSize = m_pages.first().size; - qCInfo(appLog) << "XPS document loaded with" << m_pages.size() << "pages"; - } - qCDebug(appLog) << "XpsDocument::parseXpsContent() - Parsed XPS content"; -} - -void XpsDocument::parsePageContent(const QByteArray &pageData, XpsPageData &pageInfo) -{ - qCDebug(appLog) << "XpsDocument::parsePageContent() - Parsing page content"; - QXmlStreamReader reader(pageData); - - // 获取页面尺寸 - while (!reader.atEnd()) { - QXmlStreamReader::TokenType token = reader.readNext(); - if (token == QXmlStreamReader::StartElement) { - if (reader.name() == "FixedPage") { - QXmlStreamAttributes attrs = reader.attributes(); - QString widthStr = attrs.value("Width").toString(); - QString heightStr = attrs.value("Height").toString(); - - bool ok1, ok2; - qreal width = widthStr.toDouble(&ok1); - qreal height = heightStr.toDouble(&ok2); - - if (ok1 && ok2) { - pageInfo.size = QSizeF(width, height); - - } else { - pageInfo.size = QSizeF(816, 1056); // 默认A4尺寸 - qCWarning(appLog) << "Using default page size:" << pageInfo.size; - } - break; - } - } - } - - // 重置读取器 - reader.clear(); - reader.addData(pageData); - - // 解析页面内容 - while (!reader.atEnd()) { - QXmlStreamReader::TokenType token = reader.readNext(); - if (token == QXmlStreamReader::StartElement) { - if (reader.name() == "Path") { - QPainterPath path; - parsePathData(reader, path, pageInfo); - if (!path.isEmpty()) { - pageInfo.paths.append(path); - - } - } else if (reader.name() == "Glyphs") { - parseTextData(reader, pageInfo); - } - } - } - - qCDebug(appLog) << "XpsDocument::parsePageContent() - Parsed page content"; -} - -void XpsDocument::parsePathData(QXmlStreamReader &reader, QPainterPath &path, XpsPageData &pageInfo) -{ - qCDebug(appLog) << "XpsDocument::parsePathData() - Parsing path data"; - QXmlStreamAttributes attrs = reader.attributes(); - QString fillColor = attrs.value("Fill").toString(); - - - - // 解析路径数据 - while (!reader.atEnd()) { - QXmlStreamReader::TokenType token = reader.readNext(); - if (token == QXmlStreamReader::StartElement) { - if (reader.name() == "Path.Data") { - // 解析路径数据子元素 - while (!reader.atEnd()) { - token = reader.readNext(); - if (token == QXmlStreamReader::StartElement) { - if (reader.name() == "RectangleGeometry") { - QXmlStreamAttributes rectAttrs = reader.attributes(); - QString rectStr = rectAttrs.value("Rect").toString(); - QStringList coords = rectStr.split(","); - if (coords.size() == 4) { - qreal x = coords[0].toDouble(); - qreal y = coords[1].toDouble(); - qreal w = coords[2].toDouble(); - qreal h = coords[3].toDouble(); - path.addRect(x, y, w, h); - - } - } else if (reader.name() == "EllipseGeometry") { - QXmlStreamAttributes ellipseAttrs = reader.attributes(); - QString centerStr = ellipseAttrs.value("Center").toString(); - QString radiusXStr = ellipseAttrs.value("RadiusX").toString(); - QString radiusYStr = ellipseAttrs.value("RadiusY").toString(); - - QStringList centerCoords = centerStr.split(","); - if (centerCoords.size() == 2) { - qreal centerX = centerCoords[0].toDouble(); - qreal centerY = centerCoords[1].toDouble(); - qreal radiusX = radiusXStr.toDouble(); - qreal radiusY = radiusYStr.toDouble(); - - path.addEllipse(centerX - radiusX, centerY - radiusY, - radiusX * 2, radiusY * 2); - - } - } - } - if (token == QXmlStreamReader::EndElement && reader.name() == "Path.Data") { - break; - } - } - break; - } else if (reader.name() == "Path.Fill") { - // 解析 Path.Fill -> ImageBrush -> ImageBrush.Transform -> MatrixTransform - while (!reader.atEnd()) { - token = reader.readNext(); - if (token == QXmlStreamReader::StartElement && reader.name() == "ImageBrush") { - QXmlStreamAttributes battrs = reader.attributes(); - QString imageSource = battrs.value("ImageSource").toString(); - - // 默认矩阵为单位矩阵 - qreal m11 = 1.0, m12 = 0.0, m21 = 0.0, m22 = 1.0, dx = 0.0, dy = 0.0; - - int depth = 1; - while (!reader.atEnd() && depth > 0) { - token = reader.readNext(); - if (token == QXmlStreamReader::StartElement) { - if (reader.name() == "MatrixTransform") { - QString matrix = reader.attributes().value("Matrix").toString(); - QStringList ps = matrix.split(','); - if (ps.size() == 6) { - m11 = ps[0].toDouble(); - m12 = ps[1].toDouble(); - m21 = ps[2].toDouble(); - m22 = ps[3].toDouble(); - dx = ps[4].toDouble(); - dy = ps[5].toDouble(); - } - } - depth++; - } else if (token == QXmlStreamReader::EndElement) { - depth--; - } - if (token == QXmlStreamReader::EndElement && reader.name() == "ImageBrush") { - break; - } - } - - if (!imageSource.isEmpty()) { - QString key = imageSource; - if (key.startsWith('/')) key = key.mid(1); - if (m_archiveFiles.contains(key)) { - QImage img; - img.loadFromData(m_archiveFiles[key]); - if (!img.isNull()) { - const qreal scaleBoost = 1.15; - QRectF rect(dx, dy, m11 * scaleBoost, m22 * scaleBoost); - XpsPageData::XpsImageDraw draw{img, rect}; - pageInfo.images.append(draw); - - } else { - qCWarning(appLog) << "Failed to decode image from" << key; - } - } else { - qCWarning(appLog) << "Image source not found in archive:" << key; - } - } - } - if (token == QXmlStreamReader::EndElement && reader.name() == "Path.Fill") { - break; - } - } - } - } - if (token == QXmlStreamReader::EndElement && reader.name() == "Path") { - break; - } - } - qCDebug(appLog) << "XpsDocument::parsePathData() - Parsed path data"; -} - -void XpsDocument::parseTextData(QXmlStreamReader &reader, XpsPageData &pageInfo) -{ - // qCDebug(appLog) << "XpsDocument::parseTextData() - Parsing text data"; - // 解析文本属性 - QXmlStreamAttributes attrs = reader.attributes(); - QString originX = attrs.value("OriginX").toString(); - QString originY = attrs.value("OriginY").toString(); - QString fontSize = attrs.value("FontRenderingEmSize").toString(); - QString fontUri = attrs.value("FontUri").toString(); - QString fill = attrs.value("Fill").toString(); // 可能是 #AARRGGBB 或 rgb() 等 - - - - // 读取文本内容:优先属性 UnicodeString,其次子元素 UnicodeString - QString text = attrs.value("UnicodeString").toString(); - if (text.isEmpty()) { - // qCDebug(appLog) << "XpsDocument::parseTextData() - Parsing text data"; - while (!reader.atEnd()) { - QXmlStreamReader::TokenType token = reader.readNext(); - if (token == QXmlStreamReader::StartElement && reader.name() == "UnicodeString") { - text = reader.readElementText(); - break; - } - if (token == QXmlStreamReader::EndElement && reader.name() == "Glyphs") { - break; - } - } - } - - if (!text.isEmpty()) { - // qCDebug(appLog) << "XpsDocument::parseTextData() - Parsing text data"; - - bool ok1, ok2, ok3; - qreal x = originX.toDouble(&ok1); - qreal y = originY.toDouble(&ok2); - qreal size = fontSize.toDouble(&ok3); - - if (ok1 && ok2 && ok3) { - // 记录基线点和字号;矩形顶部按近似 ascent(0.88em),高度按约 1.15em, - // 这样与实际文本基线位置更贴合,避免 y 坐标偏移 - QRectF textRect(x, y - size * 0.88, text.length() * size * 0.6, size * 1.15); - pageInfo.textRects.append(textRect); - pageInfo.textStrings.append(text); - pageInfo.textOrigins.append(QPointF(x, y)); - pageInfo.textFontSizes.append(size); - pageInfo.textFontUris.append(fontUri); - // 尝试加载嵌入字体,记录 family - QString fam = resolveFontFamily(fontUri); - pageInfo.textFontFamilies.append(fam); - - // 解析颜色 - QColor color = Qt::black; - if (!fill.isEmpty()) { - if (fill.startsWith('#')) { - color = QColor(fill); - } else if (fill.startsWith("rgb")) { - // 简单处理 rgb(r,g,b) / rgba(r,g,b,a) - QString inside = fill.mid(fill.indexOf('(') + 1); - inside.chop(1); - QStringList comps = inside.split(','); - if (comps.size() >= 3) { - int r = comps[0].trimmed().toInt(); - int g = comps[1].trimmed().toInt(); - int b = comps[2].trimmed().toInt(); - int a = 255; - if (comps.size() == 4) { - a = static_cast(comps[3].trimmed().toDouble() * 255); - } - color = QColor(r, g, b, a); - } - } - } - pageInfo.textColors.append(color); - - // 解析 Glyphs.Indices(示例:",54.545;,30.682;,..."),这里仅提取数值作为 advance 百分比(1/100 em) - QVector advances; - QString idx = attrs.value("Indices").toString(); - if (!idx.isEmpty()) { - // 去掉分号,将逗号分隔的数值提取 - QString cleaned = idx; - cleaned.replace(';', ' '); -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - const QStringList parts = cleaned.split(',', QString::SkipEmptyParts); -#else - const QStringList parts = cleaned.split(',', Qt::SkipEmptyParts); -#endif - for (const QString &part : parts) { - bool okv = false; - qreal v = part.trimmed().toDouble(&okv); - if (okv) advances.append(v); - } - } - pageInfo.textAdvances.append(advances); - - - } - } - // qCDebug(appLog) << "XpsDocument::parseTextData() - Parsed text data"; -} - -} // namespace deepin_reader diff --git a/reader/document/XpsDocument.h b/reader/document/XpsDocument.h deleted file mode 100644 index 42b95613..00000000 --- a/reader/document/XpsDocument.h +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (C) 2019 ~ 2020 Uniontech Software Technology Co.,Ltd. -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef XPSDOCUMENT_H -#define XPSDOCUMENT_H - -#include "Model.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class QPainter; -class QPainterPath; - -namespace deepin_reader { - -// XPS页面数据结构 -struct XpsPageData { - QSizeF size; - QString content; - QList paths; - QList textRects; - QList textStrings; - QList textOrigins; // Glyphs 原始基线坐标 (OriginX, OriginY) - QList textFontSizes; // 字号,用于按基线渲染 - QList textColors; // 文本颜色 - QList textFontUris; // 字体URI(用于后续映射字体族) - QList textFontFamilies; // 解析并加载后的字体家族名称 - QList> textAdvances; // 来自 Glyphs.Indices 的 advance(单位:1/100 em) - QImage thumbnail; - struct XpsImageDraw { - QImage image; - QRectF rect; - }; - QList images; -}; - -// XPS页面类,继承自Page接口 -class XpsPage : public Page -{ - Q_DISABLE_COPY(XpsPage) - - friend class XpsDocument; - -public: - ~XpsPage() override; - - QSizeF sizeF() const override; - QImage render(int width, int height, const QRect &slice = QRect()) const override; - Link getLinkAtPoint(const QPointF &point) override; - QString text(const QRectF &rect) const override; - QVector search(const QString &text, bool matchCase, bool wholeWords) const override; - QList annotations() const override; - QList words() override; - -private: - explicit XpsPage(const XpsPageData &pageData, QMutex *mutex); - - XpsPageData m_pageData; - QMutex *m_docMutex = nullptr; - bool m_wordLoaded = false; - QList m_words; -}; - -// XPS文档类,继承自Document接口 -class XpsDocument : public Document -{ - Q_DISABLE_COPY(XpsDocument) - -public: - explicit XpsDocument(const QString &filePath); - ~XpsDocument() override; - - // Document接口实现 - int pageCount() const override; - Page *page(int index) const override; - QStringList saveFilter() const override; - bool save() const override; - bool saveAs(const QString &filePath) const override; - Outline outline() const override; - Properties properties() const override; - - // 静态加载函数 - static XpsDocument *loadDocument(const QString &filePath, Document::Error &error); - -private: - // XPS解析和渲染相关成员函数 - bool loadXpsFile(); - void parseXpsContent(); - QImage renderPage(int pageIndex, int width, int height) const; - void parsePageContent(const QByteArray &pageData, XpsPageData &pageInfo); - void parsePathData(QXmlStreamReader &reader, QPainterPath &path, XpsPageData &pageInfo); - void parseTextData(QXmlStreamReader &reader, XpsPageData &pageInfo); - void collectFilesRecursively(const QString &dirPath, const QString &relativePath, QList> &fileList); - QString resolveFontFamily(const QString &fontUri); - static QByteArray deobfuscateOdttf(const QByteArray &data, const QString &fontUri); - - QString m_filePath; - QList m_pages; - QMutex m_mutex; - bool m_loaded; - QMap m_archiveFiles; - QSizeF m_documentSize; - QMap m_fontFamilyCache; // FontUri -> family - QList m_loadedFontIds; // 保持字体生命周期 - QString m_title; - QString m_author; - QString m_subject; - QString m_creator; - QDateTime m_creationDate; - QDateTime m_modificationDate; -}; - -} // namespace deepin_reader - -#endif // XPSDOCUMENT_H diff --git a/reader/document/XpsDocumentAdapter.cpp b/reader/document/XpsDocumentAdapter.cpp new file mode 100644 index 00000000..33bf3928 --- /dev/null +++ b/reader/document/XpsDocumentAdapter.cpp @@ -0,0 +1,788 @@ +// Copyright (C) 2019 ~ 2025 Uniontech Software Technology Co.,Ltd. +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "XpsDocumentAdapter.h" + +#include "ddlog.h" + +#include +#include +#include +#include +#include +#include + +#ifdef signals +#pragma push_macro("signals") +#undef signals +#endif +#ifdef slots +#pragma push_macro("slots") +#undef slots +#endif + +extern "C" { +#include +#include +#include +} + +#ifdef slots +#pragma pop_macro("slots") +#endif +#ifdef signals +#pragma pop_macro("signals") +#endif + +namespace deepin_reader { +namespace { + +template +class GObjectPtr +{ +public: + GObjectPtr() = default; + explicit GObjectPtr(T *ptr) + : m_ptr(ptr) + { + } + ~GObjectPtr() + { + reset(); + } + + GObjectPtr(const GObjectPtr &) = delete; + GObjectPtr &operator=(const GObjectPtr &) = delete; + + GObjectPtr(GObjectPtr &&other) noexcept + { + m_ptr = other.m_ptr; + other.m_ptr = nullptr; + } + + GObjectPtr &operator=(GObjectPtr &&other) noexcept + { + if (this != &other) { + reset(); + m_ptr = other.m_ptr; + other.m_ptr = nullptr; + } + return *this; + } + + void reset(T *ptr = nullptr) + { + if (m_ptr) { + g_object_unref(m_ptr); + } + m_ptr = ptr; + } + + T *release() + { + T *tmp = m_ptr; + m_ptr = nullptr; + return tmp; + } + + T *get() const + { + return m_ptr; + } + + explicit operator bool() const + { + return m_ptr != nullptr; + } + + T *operator->() const + { + return m_ptr; + } + +private: + T *m_ptr = nullptr; +}; + +struct GErrorPtr +{ + GErrorPtr() = default; + ~GErrorPtr() + { + reset(); + } + + GError **outPtr() + { + reset(); + return &m_error; + } + + GError *get() const + { + return m_error; + } + + void reset() + { + if (m_error) { + g_error_free(m_error); + m_error = nullptr; + } + } + +private: + GError *m_error = nullptr; +}; + +struct CairoSurfacePtr +{ + CairoSurfacePtr() = default; + explicit CairoSurfacePtr(cairo_surface_t *surface) + : m_surface(surface) + { + } + ~CairoSurfacePtr() + { + reset(); + } + + CairoSurfacePtr(const CairoSurfacePtr &) = delete; + CairoSurfacePtr &operator=(const CairoSurfacePtr &) = delete; + + CairoSurfacePtr(CairoSurfacePtr &&other) noexcept + { + m_surface = other.m_surface; + other.m_surface = nullptr; + } + + CairoSurfacePtr &operator=(CairoSurfacePtr &&other) noexcept + { + if (this != &other) { + reset(); + m_surface = other.m_surface; + other.m_surface = nullptr; + } + return *this; + } + + cairo_surface_t *get() const + { + return m_surface; + } + + cairo_surface_t *operator->() const + { + return m_surface; + } + + explicit operator bool() const + { + return m_surface != nullptr; + } + + void reset(cairo_surface_t *surface = nullptr) + { + if (m_surface) { + cairo_surface_destroy(m_surface); + } + m_surface = surface; + } + +private: + cairo_surface_t *m_surface = nullptr; +}; + +struct CairoContextPtr +{ + CairoContextPtr() = default; + explicit CairoContextPtr(cairo_t *context) + : m_context(context) + { + } + ~CairoContextPtr() + { + reset(); + } + + CairoContextPtr(const CairoContextPtr &) = delete; + CairoContextPtr &operator=(const CairoContextPtr &) = delete; + + CairoContextPtr(CairoContextPtr &&other) noexcept + { + m_context = other.m_context; + other.m_context = nullptr; + } + + CairoContextPtr &operator=(CairoContextPtr &&other) noexcept + { + if (this != &other) { + reset(); + m_context = other.m_context; + other.m_context = nullptr; + } + return *this; + } + + cairo_t *get() const + { + return m_context; + } + + cairo_t *operator->() const + { + return m_context; + } + + explicit operator bool() const + { + return m_context != nullptr; + } + + void reset(cairo_t *context = nullptr) + { + if (m_context) { + cairo_destroy(m_context); + } + m_context = context; + } + +private: + cairo_t *m_context = nullptr; +}; + +QString toQString(const gchar *value) +{ + if (!value) { + return QString(); + } + return QString::fromUtf8(value); +} + +QString normalizedString(const gchar *value, const QString &fallback = QStringLiteral("未知")) +{ + QString result = toQString(value).trimmed(); + return result.isEmpty() ? fallback : result; +} + +QDateTime fromUnixTime(time_t value) +{ + if (value <= 0) { + return QDateTime(); + } + return QDateTime::fromSecsSinceEpoch(static_cast(value), Qt::UTC).toLocalTime(); +} + +void logGError(const QString &prefix, GError *error) +{ + if (!error) { + qCWarning(appLog) << prefix << "- Unknown error"; + return; + } + qCWarning(appLog) << prefix << "- domain:" << error->domain << "code:" << error->code << "message:" << error->message; +} + +} // namespace + +class XpsDocumentAdapter::Handle +{ +public: + Handle(GXPSFile *fileHandle, GXPSDocument *documentHandle) + : file(fileHandle) + , document(documentHandle) + { + } + + ~Handle() + { + if (document) { + g_object_unref(document); + document = nullptr; + } + if (file) { + g_object_unref(file); + file = nullptr; + } + } + + Handle(const Handle &) = delete; + Handle &operator=(const Handle &) = delete; + + Handle(Handle &&other) noexcept + { + file = other.file; + document = other.document; + other.file = nullptr; + other.document = nullptr; + } + + Handle &operator=(Handle &&other) noexcept + { + if (this != &other) { + if (document) { + g_object_unref(document); + } + if (file) { + g_object_unref(file); + } + file = other.file; + document = other.document; + other.file = nullptr; + other.document = nullptr; + } + return *this; + } + + GXPSFile *file = nullptr; + GXPSDocument *document = nullptr; +}; + +XpsDocumentAdapter::XpsDocumentAdapter(const QString &filePath, + GXPSFile *fileHandle, + GXPSDocument *documentHandle) + : m_filePath(filePath) + , m_handle(new Handle(fileHandle, documentHandle)) +{ +} + +XpsDocumentAdapter::~XpsDocumentAdapter() = default; + +XpsDocumentAdapter *XpsDocumentAdapter::loadDocument(const QString &filePath, Document::Error &error) +{ + error = Document::FileError; + + if (filePath.isEmpty()) { + qCWarning(appLog) << "XPS load requested with empty file path"; + return nullptr; + } + + QByteArray utf8 = QFile::encodeName(filePath); + GObjectPtr gioFile(g_file_new_for_path(utf8.constData())); + if (!gioFile) { + qCWarning(appLog) << "Failed to create GFile for" << filePath; + return nullptr; + } + + GErrorPtr fileError; + GObjectPtr xpsFile(gxps_file_new(gioFile.get(), fileError.outPtr())); + if (!xpsFile) { + logGError(QStringLiteral("gxps_file_new failed"), fileError.get()); + if (fileError.get() && g_error_matches(fileError.get(), GXPS_FILE_ERROR, GXPS_FILE_ERROR_INVALID)) { + error = Document::FileDamaged; + } + return nullptr; + } + + guint documentCount = gxps_file_get_n_documents(xpsFile.get()); + if (documentCount == 0) { + qCWarning(appLog) << "XPS file contains no documents" << filePath; + return nullptr; + } + + GErrorPtr docError; + GObjectPtr document(gxps_file_get_document(xpsFile.get(), 0, docError.outPtr())); + if (!document) { + logGError(QStringLiteral("gxps_file_get_document failed"), docError.get()); + if (docError.get() && g_error_matches(docError.get(), GXPS_FILE_ERROR, GXPS_FILE_ERROR_INVALID)) { + error = Document::FileDamaged; + } + return nullptr; + } + + auto *adapter = new XpsDocumentAdapter(filePath, xpsFile.release(), document.release()); + adapter->ensurePageCache(); + adapter->ensureProperties(); + adapter->ensureOutline(); + + error = Document::NoError; + return adapter; +} + +int XpsDocumentAdapter::pageCount() const +{ + ensurePageCache(); + QMutexLocker lock(&m_mutex); + return m_pageSizes.size(); +} + +Page *XpsDocumentAdapter::page(int index) const +{ + if (index < 0 || index >= pageCount()) { + qCWarning(appLog) << "Requested XPS page out of range" << index; + return nullptr; + } + return new XpsPageAdapter(this, index); +} + +QStringList XpsDocumentAdapter::saveFilter() const +{ + return QStringList() << QStringLiteral("XPS files (*.xps)"); +} + +bool XpsDocumentAdapter::save() const +{ + qCWarning(appLog) << "XPS save() not implemented"; + return false; +} + +bool XpsDocumentAdapter::saveAs(const QString &filePath) const +{ + Q_UNUSED(filePath) + qCWarning(appLog) << "XPS saveAs() not implemented"; + return false; +} + +Outline XpsDocumentAdapter::outline() const +{ + ensureOutline(); + QMutexLocker lock(&m_mutex); + return m_outline; +} + +Properties XpsDocumentAdapter::properties() const +{ + ensureProperties(); + QMutexLocker lock(&m_mutex); + return m_properties; +} + +QSizeF XpsDocumentAdapter::pageSize(int pageIndex) const +{ + ensurePageCache(); + QMutexLocker lock(&m_mutex); + if (pageIndex < 0 || pageIndex >= m_pageSizes.size()) { + return QSizeF(); + } + return m_pageSizes.at(pageIndex); +} + +QImage XpsDocumentAdapter::renderPage(int pageIndex, int width, int height, const QRect &slice) const +{ + if (!m_handle || !m_handle->document) { + qCWarning(appLog) << "Attempting to render XPS page with invalid document handle"; + return QImage(); + } + + if (width <= 0 || height <= 0) { + qCWarning(appLog) << "Invalid render target size" << width << height; + return QImage(); + } + + ensurePageCache(); + + QMutexLocker lock(&m_mutex); + if (pageIndex < 0 || pageIndex >= m_pageSizes.size()) { + qCWarning(appLog) << "Render request out of bounds" << pageIndex; + return QImage(); + } + + qCDebug(appLog) << "Rendering XPS page" << pageIndex << "@" << width << "x" << height << "slice" << slice; + + GErrorPtr pageError; + GObjectPtr page(gxps_document_get_page(m_handle->document, static_cast(pageIndex), pageError.outPtr())); + if (!page) { + logGError(QStringLiteral("gxps_document_get_page failed"), pageError.get()); + return QImage(); + } + + const QSizeF logicalSize = m_pageSizes.at(pageIndex); + const QRectF fullRect(QPointF(0, 0), logicalSize); + QRectF clipRect = slice.isValid() ? QRectF(slice) : fullRect; + clipRect = clipRect.intersected(fullRect); + if (clipRect.isEmpty()) { + clipRect = fullRect; + } + if (clipRect.width() <= 0.0 || clipRect.height() <= 0.0) { + qCWarning(appLog) << "Invalid clip rectangle for XPS render" << clipRect; + return QImage(); + } + + const double scaleX = width / clipRect.width(); + const double scaleY = height / clipRect.height(); + + QImage image(width, height, QImage::Format_ARGB32_Premultiplied); + if (image.isNull()) { + qCWarning(appLog) << "Failed to allocate image for XPS render"; + return image; + } + image.fill(Qt::transparent); + + CairoSurfacePtr surface(cairo_image_surface_create_for_data(image.bits(), + CAIRO_FORMAT_ARGB32, + width, + height, + image.bytesPerLine())); + if (!surface || cairo_surface_status(surface.get()) != CAIRO_STATUS_SUCCESS) { + qCWarning(appLog) << "Failed to create Cairo surface for XPS render"; + return QImage(); + } + + CairoContextPtr context(cairo_create(surface.get())); + if (!context || cairo_status(context.get()) != CAIRO_STATUS_SUCCESS) { + qCWarning(appLog) << "Failed to create Cairo context for XPS render"; + return QImage(); + } + + cairo_set_source_rgb(context.get(), 1.0, 1.0, 1.0); + cairo_paint(context.get()); + + cairo_matrix_t matrix; + cairo_matrix_init(&matrix, + scaleX, 0.0, + 0.0, scaleY, + -clipRect.left() * scaleX, + -clipRect.top() * scaleY); + cairo_set_matrix(context.get(), &matrix); + + GErrorPtr renderError; + if (!gxps_page_render(page.get(), context.get(), renderError.outPtr())) { + logGError(QStringLiteral("gxps_page_render failed"), renderError.get()); + return QImage(); + } + + cairo_surface_flush(surface.get()); + return image.convertToFormat(QImage::Format_RGB32); +} + +void XpsDocumentAdapter::ensurePageCache() const +{ + if (!m_handle || !m_handle->document) { + return; + } + if (!m_pageSizes.isEmpty()) { + return; + } + + QMutexLocker lock(&m_mutex); + if (!m_pageSizes.isEmpty()) { + return; + } + + const guint count = gxps_document_get_n_pages(m_handle->document); + m_pageSizes.resize(static_cast(count)); + + for (guint i = 0; i < count; ++i) { + gdouble width = 0.0; + gdouble height = 0.0; + gboolean gotSize = gxps_document_get_page_size(m_handle->document, i, &width, &height); + + if (!gotSize || width <= 0.0 || height <= 0.0) { + GErrorPtr pageError; + GObjectPtr page(gxps_document_get_page(m_handle->document, i, pageError.outPtr())); + if (page) { + gdouble fallbackWidth = 0.0; + gdouble fallbackHeight = 0.0; + gxps_page_get_size(page.get(), &fallbackWidth, &fallbackHeight); + if (fallbackWidth > 0.0 && fallbackHeight > 0.0) { + width = fallbackWidth; + height = fallbackHeight; + } else { + width = 612.0; + height = 792.0; + } + } else { + width = 612.0; + height = 792.0; + } + } + + m_pageSizes[static_cast(i)] = QSizeF(width, height); + } + + qCInfo(appLog) << "XPS document page cache initialized, pages:" << count; +} + +void XpsDocumentAdapter::ensureProperties() const +{ + if (!m_handle || !m_handle->file) { + return; + } + if (!m_properties.isEmpty()) { + return; + } + + ensurePageCache(); + + QMutexLocker lock(&m_mutex); + if (!m_properties.isEmpty()) { + return; + } + + Properties props; + props["Format"] = QStringLiteral("XPS"); + props["FilePath"] = m_filePath; + + GErrorPtr coreError; + GObjectPtr core(gxps_file_get_core_properties(m_handle->file, coreError.outPtr())); + if (!core) { + if (coreError.get()) { + logGError(QStringLiteral("gxps_file_get_core_properties failed"), coreError.get()); + } + } else { + props["Title"] = normalizedString(gxps_core_properties_get_title(core.get())); + props["Author"] = normalizedString(gxps_core_properties_get_creator(core.get())); + props["Subject"] = normalizedString(gxps_core_properties_get_subject(core.get()), QString()); + props["Keywords"] = normalizedString(gxps_core_properties_get_keywords(core.get()), QString()); + props["Description"] = normalizedString(gxps_core_properties_get_description(core.get()), QString()); + props["Version"] = normalizedString(gxps_core_properties_get_version(core.get()), QString()); + props["Creator"] = normalizedString(gxps_core_properties_get_last_modified_by(core.get()), QString()); + + const QDateTime created = fromUnixTime(gxps_core_properties_get_created(core.get())); + const QDateTime modified = fromUnixTime(gxps_core_properties_get_modified(core.get())); + if (created.isValid()) { + props["CreationDate"] = created; + } + if (modified.isValid()) { + props["ModificationDate"] = modified; + } + } + + props["PageCount"] = m_pageSizes.size(); + const QSizeF firstSize = m_pageSizes.isEmpty() ? QSizeF() : m_pageSizes.first(); + props["Width"] = firstSize.width(); + props["Height"] = firstSize.height(); + + m_properties = props; +} + +void XpsDocumentAdapter::ensureOutline() const +{ + if (!m_handle || !m_handle->document) { + return; + } + if (!m_outline.isEmpty()) { + return; + } + + QMutexLocker lock(&m_mutex); + if (!m_outline.isEmpty()) { + return; + } + + GObjectPtr structure(gxps_document_get_structure(m_handle->document)); + if (!structure) { + return; + } + + if (!gxps_document_structure_has_outline(structure.get())) { + return; + } + + GXPSOutlineIter iter; + if (!gxps_document_structure_outline_iter_init(&iter, structure.get())) { + return; + } + + auto anchorPointFor = [&](int pageIdx, const gchar *anchor) -> QPointF { + if (pageIdx < 0 || !anchor) { + return QPointF(); + } + + GErrorPtr pageError; + GObjectPtr page(gxps_document_get_page(m_handle->document, static_cast(pageIdx), pageError.outPtr())); + if (!page) { + logGError(QStringLiteral("gxps_document_get_page failed while building outline"), pageError.get()); + return QPointF(); + } + + GErrorPtr anchorError; + cairo_rectangle_t area {0, 0, 0, 0}; + if (!gxps_page_get_anchor_destination(page.get(), anchor, &area, anchorError.outPtr())) { + if (anchorError.get()) { + logGError(QStringLiteral("gxps_page_get_anchor_destination failed while building outline"), anchorError.get()); + } + return QPointF(); + } + return QPointF(area.x, area.y); + }; + + std::function buildSection = [&](GXPSOutlineIter *it) -> Section { + Section section; + section.title = toQString(gxps_outline_iter_get_description(it)); + + GXPSLinkTarget *target = gxps_outline_iter_get_target(it); + if (target && gxps_link_target_is_internal(target)) { + const gchar *anchor = gxps_link_target_get_anchor(target); + if (anchor) { + const int pageIdx = gxps_document_get_page_for_anchor(m_handle->document, anchor); + section.nIndex = pageIdx; + section.offsetPointF = anchorPointFor(pageIdx, anchor); + } + } + + GXPSOutlineIter childIter; + if (gxps_outline_iter_children(&childIter, it)) { + do { + section.children.append(buildSection(&childIter)); + } while (gxps_outline_iter_next(&childIter)); + } + + return section; + }; + + Outline result; + do { + result.append(buildSection(&iter)); + } while (gxps_outline_iter_next(&iter)); + + m_outline = result; +} + +XpsPageAdapter::XpsPageAdapter(const XpsDocumentAdapter *document, int pageIndex) + : m_document(document) + , m_pageIndex(pageIndex) +{ +} + +XpsPageAdapter::~XpsPageAdapter() = default; + +QSizeF XpsPageAdapter::sizeF() const +{ + if (!m_document) { + return QSizeF(); + } + return m_document->pageSize(m_pageIndex); +} + +QImage XpsPageAdapter::render(int width, int height, const QRect &slice) const +{ + if (!m_document) { + return QImage(); + } + return m_document->renderPage(m_pageIndex, width, height, slice); +} + +Link XpsPageAdapter::getLinkAtPoint(const QPointF &point) +{ + Q_UNUSED(point) + return Link(); +} + +QString XpsPageAdapter::text(const QRectF &rect) const +{ + Q_UNUSED(rect) + return QString(); +} + +QVector XpsPageAdapter::search(const QString &text, bool matchCase, bool wholeWords) const +{ + Q_UNUSED(text) + Q_UNUSED(matchCase) + Q_UNUSED(wholeWords) + return {}; +} + +QList XpsPageAdapter::annotations() const +{ + return {}; +} + +QList XpsPageAdapter::words() +{ + return {}; +} + +} // namespace deepin_reader + diff --git a/reader/document/XpsDocumentAdapter.h b/reader/document/XpsDocumentAdapter.h new file mode 100644 index 00000000..d55ecec9 --- /dev/null +++ b/reader/document/XpsDocumentAdapter.h @@ -0,0 +1,85 @@ +// Copyright (C) 2019 ~ 2025 Uniontech Software Technology Co.,Ltd. +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef XPSDOCUMENTADAPTER_H +#define XPSDOCUMENTADAPTER_H + +#include "Model.h" + +#include +#include +#include +#include +#include + +struct _GXPSFile; +struct _GXPSDocument; +struct _GXPSDocumentStructure; +struct _GXPSOutlineIter; + +namespace deepin_reader { + +class XpsDocumentAdapter : public Document +{ + Q_OBJECT +public: + static XpsDocumentAdapter *loadDocument(const QString &filePath, Document::Error &error); + + ~XpsDocumentAdapter() override; + + int pageCount() const override; + Page *page(int index) const override; + QStringList saveFilter() const override; + bool save() const override; + bool saveAs(const QString &filePath) const override; + Outline outline() const override; + Properties properties() const override; + + QImage renderPage(int pageIndex, int width, int height, const QRect &slice) const; + QSizeF pageSize(int pageIndex) const; + +private: + class Handle; + + XpsDocumentAdapter(const QString &filePath, + _GXPSFile *fileHandle, + _GXPSDocument *documentHandle); + + void ensurePageCache() const; + void ensureProperties() const; + void ensureOutline() const; + + QString m_filePath; + mutable QMutex m_mutex; + mutable QVector m_pageSizes; + mutable Properties m_properties; + mutable Outline m_outline; + std::unique_ptr m_handle; +}; + +class XpsPageAdapter : public Page +{ + Q_OBJECT +public: + XpsPageAdapter(const XpsDocumentAdapter *document, int pageIndex); + ~XpsPageAdapter() override; + + QSizeF sizeF() const override; + QImage render(int width, int height, const QRect &slice = QRect()) const override; + Link getLinkAtPoint(const QPointF &point) override; + QString text(const QRectF &rect) const override; + QVector search(const QString &text, bool matchCase, bool wholeWords) const override; + QList annotations() const override; + QList words() override; + +private: + const XpsDocumentAdapter *m_document; + int m_pageIndex = -1; +}; + +} // namespace deepin_reader + +#endif // XPSDOCUMENTADAPTER_H + diff --git a/reader/document/document.pri b/reader/document/document.pri index e7d9d4fe..45a2f1be 100644 --- a/reader/document/document.pri +++ b/reader/document/document.pri @@ -5,8 +5,8 @@ HEADERS += \ # XPS支持文件(条件包含) xps_support { - HEADERS += $$PWD/XpsDocument.h - SOURCES += $$PWD/XpsDocument.cpp + HEADERS += $$PWD/XpsDocumentAdapter.h + SOURCES += $$PWD/XpsDocumentAdapter.cpp } SOURCES += \