diff --git a/batch-print/CMakeLists.txt b/batch-print/CMakeLists.txt index 0593816a2..33d919451 100644 --- a/batch-print/CMakeLists.txt +++ b/batch-print/CMakeLists.txt @@ -44,6 +44,15 @@ set(BATCHPRINT_READER_SOURCES ${CMAKE_SOURCE_DIR}/reader/document/XpsTextExtractor.cpp ) +# OFD: Model.cpp gets OFD_SUPPORT_ENABLED from the global add_compile_definitions, +# so OfdModel.cpp (implements OfdDocument::loadDocument) and rofd/cairo must be linked too. +if (OFD_SUPPORT_ENABLED) + list(APPEND BATCHPRINT_READER_SOURCES + ${CMAKE_SOURCE_DIR}/reader/document/OfdModel.cpp + ${CMAKE_SOURCE_DIR}/reader/document/Navigation.cpp + ) +endif() + add_library(batchprint-convert STATIC formatconverter.h formatconverter.cpp ${BATCHPRINT_READER_SOURCES} @@ -61,6 +70,8 @@ target_include_directories(batchprint-convert PUBLIC ${CMAKE_SOURCE_DIR}/reader/document ${PDFIUM_INCLUDE_DIRS} $<$:${XPS_DEPS_INCLUDE_DIRS}> + $<$:${ROFD_INCLUDE_DIR}> + $<$:${OFD_CAIRO_INCLUDE_DIRS}> ) target_compile_definitions(batchprint-convert PRIVATE @@ -83,6 +94,8 @@ target_link_libraries(batchprint-convert PUBLIC ${LIBJPEG_LIBRARIES} ${FREETYPE_LIBRARIES} $<$:${XPS_DEPS_LIBRARIES}> + $<$:${ROFD_FFI_LIBRARY}> + $<$:${OFD_CAIRO_LIBRARIES}> ) # DTK linking @@ -109,6 +122,10 @@ if (XPS_SUPPORT_ENABLED) target_compile_options(batchprint-convert PUBLIC ${XPS_DEPS_CFLAGS_OTHER}) endif() +if (OFD_SUPPORT_ENABLED) + target_compile_options(batchprint-convert PRIVATE ${OFD_CAIRO_CFLAGS_OTHER}) +endif() + # Executable add_executable(deepin-reader-batchprint main.cpp diff --git a/reader/browser/PageRenderThread.cpp b/reader/browser/PageRenderThread.cpp index 61ba9aa27..ba38b29c4 100644 --- a/reader/browser/PageRenderThread.cpp +++ b/reader/browser/PageRenderThread.cpp @@ -817,6 +817,11 @@ bool PageRenderThread::execNextDocPageThumbnailTask() QImage image = task.renderer->getImage(task.index, 174, 174); if (!image.isNull()) { + // 预取图片对象 bbox(夜间/深色蒙版用):在工作线程取,避免 UI 线程与渲染争文档锁; + // 与 getImage 同尺寸(174,174)请求,蒙版与缩略图输出像素一一对齐 + if (DocSheet::existSheetByUuid(task.uuid) && task.renderer->opened()) { + task.imageRects = task.renderer->getImageObjectRects(task.index, 174, 174); + } emit sigDocPageThumbnailTaskFinished(task, QPixmap::fromImage(image)); } qCDebug(appLog) << "执行缩略图任务已完成"; @@ -968,7 +973,7 @@ void PageRenderThread::onDocPageThumbnailTask(DocPageThumbnailTask task, QPixmap { // qCDebug(appLog) << "PageRenderThread::onDocPageThumbnailTask() - Starting on doc page thumbnail task"; if (DocSheet::existSheet(task.sheet)) { - task.model->handleRenderThumbnail(task.index, pixmap); + task.model->handleRenderThumbnail(task.index, pixmap, task.imageRects); } // qCDebug(appLog) << "PageRenderThread::onDocPageThumbnailTask() - On doc page thumbnail task completed"; } diff --git a/reader/browser/PageRenderThread.h b/reader/browser/PageRenderThread.h index 12b4a6df5..0b8ff46bb 100644 --- a/reader/browser/PageRenderThread.h +++ b/reader/browser/PageRenderThread.h @@ -83,6 +83,7 @@ struct DocPageThumbnailTask {//缩略图 DocSheet *sheet = nullptr; SideBarImageViewModel *model = nullptr; int index = -1; + QVector imageRects; //图片对象 bbox(夜间/深色蒙版用,与缩略图像素对齐,worker线程预取) }; struct DocOpenTask {//打开文档 diff --git a/reader/sidebar/BookMarkDelegate.cpp b/reader/sidebar/BookMarkDelegate.cpp index de692eed4..7bea597e9 100644 --- a/reader/sidebar/BookMarkDelegate.cpp +++ b/reader/sidebar/BookMarkDelegate.cpp @@ -5,6 +5,7 @@ #include "BookMarkDelegate.h" #include "SideBarImageViewModel.h" +#include "NightFilter.h" #include "Application.h" #include "ddlog.h" @@ -48,42 +49,33 @@ void BookMarkDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti clipPath.addRoundedRect(rect, borderRadius, borderRadius); painter->setClipPath(clipPath); // 深色主题下将白底缩略图反色为黑底白字(仅绘制时反色,不改缓存原图): - // HSL 亮度反转,下限钳制 37(#252525),反转后 ≥192 提亮纯白。 + // 统一走主干夜间滤镜 NightFilter(CIELAB L* 反转),图片对象区域不反色 + // (照片零负片,与主视图蒙版行为一致),含扫描页整页反色特判。 if (DTK_NAMESPACE::Gui::DGuiApplicationHelper::instance()->themeType() == DTK_NAMESPACE::Gui::DGuiApplicationHelper::DarkType) { - // 按源图 cacheKey() 缓存反色结果,避免每次重绘重复逐像素计算 + // 图片对象 bbox(与存储缩略图像素对齐,渲染线程预取) + const QVector imageRects = index.data(ImageinfoType_e::IMAGE_NIGHT_MASK).value>(); + // 按源缩略图 cacheKey() 缓存反色结果,避免每次重绘重复逐像素计算 QPixmap invertedPixmap; if (QPixmap *cached = m_darkPixmapCache.object(pixmap.cacheKey())) { invertedPixmap = *cached; } else { - QImage img = scalePix.toImage(); - if (!img.isNull()) { - if (img.format() != QImage::Format_ARGB32) - img = img.convertToFormat(QImage::Format_ARGB32); - const int w = img.width(); - const int h = img.height(); - const int kMinLightAfterInvert = 37; // #252525 - const int kMaxLightBoostThreshold = 192; // 0xC0,提亮阈值 - for (int y = 0; y < h; ++y) { - QRgb *line = reinterpret_cast(img.scanLine(y)); - for (int x = 0; x < w; ++x) { - const QRgb px = line[x]; - const int alpha = qAlpha(px); - QColor c = QColor::fromRgb(qRed(px), qGreen(px), qBlue(px)); - int hue, sat, light, dummy; - c.getHsl(&hue, &sat, &light, &dummy); - light = 255 - light; - if (light >= kMaxLightBoostThreshold) - light = 255; - light = qMax(light, kMinLightAfterInvert); - c.setHsl(hue, sat, light); - line[x] = qRgba(c.red(), c.green(), c.blue(), alpha); - } - } - invertedPixmap = QPixmap::fromImage(img); - invertedPixmap.setDevicePixelRatio(scalePix.devicePixelRatio()); + // bbox 是存储缩略图(174px)坐标,而 scalePix 是等比缩小后的图, + // 须先映射到 scalePix 坐标,否则覆盖率特判/蒙版区域全错 + QVector scaledRects; + scaledRects.reserve(imageRects.size()); + const qreal sx = pixmap.isNull() || pixmap.width() == 0 + ? 0.0 : qreal(scalePix.width()) / pixmap.width(); + const qreal sy = pixmap.isNull() || pixmap.height() == 0 + ? 0.0 : qreal(scalePix.height()) / pixmap.height(); + for (const QRectF &r : imageRects) + scaledRects.append(QRectF(r.x() * sx, r.y() * sy, + r.width() * sx, r.height() * sy)); + + invertedPixmap = QPixmap::fromImage(NightFilter::applyPage(scalePix.toImage(), scaledRects)); + invertedPixmap.setDevicePixelRatio(scalePix.devicePixelRatio()); + if (!invertedPixmap.isNull()) m_darkPixmapCache.insert(pixmap.cacheKey(), new QPixmap(invertedPixmap), - img.width() * img.height() * 4); // ARGB32 每像素固定 4 字节 - } + invertedPixmap.width() * invertedPixmap.height() * 4); // ARGB32 每像素固定 4 字节 } if (!invertedPixmap.isNull()) painter->drawPixmap(rect.x(), rect.y(), invertedPixmap); diff --git a/reader/sidebar/NotesDelegate.cpp b/reader/sidebar/NotesDelegate.cpp index c4a1fcdea..d4f0f99af 100644 --- a/reader/sidebar/NotesDelegate.cpp +++ b/reader/sidebar/NotesDelegate.cpp @@ -5,6 +5,7 @@ #include "NotesDelegate.h" #include "SideBarImageViewModel.h" +#include "NightFilter.h" #include "Utils.h" #include "Application.h" #include "ddlog.h" @@ -15,12 +16,14 @@ #include #include #include +#include NotesDelegate::NotesDelegate(QAbstractItemView *parent) : DStyledItemDelegate(parent) { // qCDebug(appLog) << "NotesDelegate::NotesDelegate() - Starting constructor"; m_parent = parent; + m_darkPixmapCache.setMaxCost(8 * 1024 * 1024); // 反色缓存预算 8MB,按像素字节数计费 // qCDebug(appLog) << "NotesDelegate::NotesDelegate() - Constructor completed"; } @@ -45,7 +48,42 @@ void NotesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, QPainterPath clipPath; clipPath.addRoundedRect(rect, borderRadius, borderRadius); painter->setClipPath(clipPath); - painter->drawPixmap(rect.x(), rect.y(), scalePix); + // 深色主题下将白底缩略图反色为黑底白字(仅绘制时反色,不改缓存原图): + // 统一走主干夜间滤镜 NightFilter(CIELAB L* 反转),图片对象区域不反色 + // (照片零负片,与主视图蒙版行为一致),含扫描页整页反色特判。 + if (DTK_NAMESPACE::Gui::DGuiApplicationHelper::instance()->themeType() == DTK_NAMESPACE::Gui::DGuiApplicationHelper::DarkType) { + // 图片对象 bbox(与存储缩略图像素对齐,渲染线程预取) + const QVector imageRects = index.data(ImageinfoType_e::IMAGE_NIGHT_MASK).value>(); + // 按源缩略图 cacheKey() 缓存反色结果,避免每次重绘重复逐像素计算 + QPixmap invertedPixmap; + if (QPixmap *cached = m_darkPixmapCache.object(pixmap.cacheKey())) { + invertedPixmap = *cached; + } else { + // bbox 是存储缩略图(174px)坐标,而 scalePix 是等比缩小后的图, + // 须先映射到 scalePix 坐标,否则覆盖率特判/蒙版区域全错 + QVector scaledRects; + scaledRects.reserve(imageRects.size()); + const qreal sx = pixmap.isNull() || pixmap.width() == 0 + ? 0.0 : qreal(scalePix.width()) / pixmap.width(); + const qreal sy = pixmap.isNull() || pixmap.height() == 0 + ? 0.0 : qreal(scalePix.height()) / pixmap.height(); + for (const QRectF &r : imageRects) + scaledRects.append(QRectF(r.x() * sx, r.y() * sy, + r.width() * sx, r.height() * sy)); + + invertedPixmap = QPixmap::fromImage(NightFilter::applyPage(scalePix.toImage(), scaledRects)); + invertedPixmap.setDevicePixelRatio(scalePix.devicePixelRatio()); + if (!invertedPixmap.isNull()) + m_darkPixmapCache.insert(pixmap.cacheKey(), new QPixmap(invertedPixmap), + invertedPixmap.width() * invertedPixmap.height() * 4); // ARGB32 每像素固定 4 字节 + } + if (!invertedPixmap.isNull()) + painter->drawPixmap(rect.x(), rect.y(), invertedPixmap); + else + painter->drawPixmap(rect.x(), rect.y(), scalePix); + } else { + painter->drawPixmap(rect.x(), rect.y(), scalePix); + } painter->restore(); } @@ -57,7 +95,12 @@ void NotesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, painter->setPen(QPen(DTK_NAMESPACE::Gui::DGuiApplicationHelper::instance()->applicationPalette().highlight().color(), 2)); painter->drawRoundedRect(rect, borderRadius, borderRadius); } else { - painter->setPen(QPen(DTK_NAMESPACE::Gui::DGuiApplicationHelper::instance()->applicationPalette().frameShadowBorder().color(), 1)); + QColor frameColor = DTK_NAMESPACE::Gui::DGuiApplicationHelper::instance()->applicationPalette().frameShadowBorder().color(); + if (DTK_NAMESPACE::Gui::DGuiApplicationHelper::instance()->themeType() == DTK_NAMESPACE::Gui::DGuiApplicationHelper::DarkType) { + frameColor = DTK_NAMESPACE::Gui::DGuiApplicationHelper::instance()->applicationPalette().windowText().color(); + frameColor.setAlphaF(0.2); + } + painter->setPen(QPen(frameColor, 1)); painter->drawRoundedRect(rect, borderRadius, borderRadius); } painter->restore(); diff --git a/reader/sidebar/NotesDelegate.h b/reader/sidebar/NotesDelegate.h index 95bd6e63d..1ce70812a 100644 --- a/reader/sidebar/NotesDelegate.h +++ b/reader/sidebar/NotesDelegate.h @@ -8,6 +8,9 @@ #include +#include +#include + DWIDGET_USE_NAMESPACE /** * @brief The NotesDelegate class @@ -39,6 +42,11 @@ class NotesDelegate : public DStyledItemDelegate private: QAbstractItemView *m_parent = nullptr; + + /** + * @brief 深色主题反色结果缓存(键:源图 cacheKey();paint() 为 const 故 mutable) + */ + mutable QCache m_darkPixmapCache; }; #endif // NOTESDELEGATE_H diff --git a/reader/sidebar/SearchResDelegate.cpp b/reader/sidebar/SearchResDelegate.cpp index 6301a7745..c5171bcef 100644 --- a/reader/sidebar/SearchResDelegate.cpp +++ b/reader/sidebar/SearchResDelegate.cpp @@ -5,6 +5,7 @@ #include "SearchResDelegate.h" #include "SideBarImageViewModel.h" +#include "NightFilter.h" #include "Utils.h" #include "Application.h" #include "ddlog.h" @@ -39,7 +40,13 @@ void SearchResDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt if (!pixmap.isNull()) { - const QPixmap &scalePix = pixmap.scaled(pageSize); + // 深色主题下搜索结果页的页面小图与缩略图侧栏一致走主干夜间滤镜 + // (CIELAB L* 反转,图片对象区域保持原色,扫描页整页反色特判), + // 浅色主题照常绘制原图;先按原始尺寸反色再缩放,蒙版坐标无需换算 + const bool darkTheme = (DTK_NAMESPACE::Gui::DGuiApplicationHelper::instance()->themeType() == DTK_NAMESPACE::Gui::DGuiApplicationHelper::DarkType); + const QVector imageRects = index.data(ImageinfoType_e::IMAGE_NIGHT_MASK).value>(); + const QPixmap &displayPixmap = darkTheme ? nightPixmap(pixmap, imageRects) : pixmap; + const QPixmap &scalePix = displayPixmap.scaled(pageSize); //clipPath pixmap painter->save(); QPainterPath clipPath; @@ -124,3 +131,19 @@ QSize SearchResDelegate::sizeHint(const QStyleOptionViewItem &option, const QMod return size; } +QPixmap SearchResDelegate::nightPixmap(const QPixmap &src, const QVector &imageRects) const +{ + if (src.isNull()) + return src; + + // 搜索结果列表滚动时同一张页面小图会被反复重绘,缓存反色结果避免逐像素重复计算; + // 页面重渲染时 cacheKey 必然变化,无需将蒙版纳入缓存键 + if (m_nightSourceCache.cacheKey() == src.cacheKey() && !m_nightPixmapCache.isNull()) + return m_nightPixmapCache; + + m_nightSourceCache = src; + m_nightPixmapCache = QPixmap::fromImage(NightFilter::applyPage(src.toImage(), imageRects)); + m_nightPixmapCache.setDevicePixelRatio(src.devicePixelRatio()); + return m_nightPixmapCache; +} + diff --git a/reader/sidebar/SearchResDelegate.h b/reader/sidebar/SearchResDelegate.h index 4b53b6890..e3503c439 100644 --- a/reader/sidebar/SearchResDelegate.h +++ b/reader/sidebar/SearchResDelegate.h @@ -7,6 +7,9 @@ #define SEARCHRESDELEGATE_H #include +#include +#include +#include DWIDGET_USE_NAMESPACE /** @@ -37,8 +40,23 @@ class SearchResDelegate : public DStyledItemDelegate */ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +private: + /** + * @brief nightPixmap + * 夜间智能滤镜/深色主题下页面小图的反色结果(带缓存): + * 走主干 NightFilter 管线(CIELAB L* 反转 + 图片对象区域跳过 + 扫描页整页反色特判), + * 与缩略图侧栏/主视图观感一致 + * @param src 原始缩略图 + * @param imageRects 图片对象 bbox(与 src 像素对齐,这些区域不反色避免照片负片) + * @return 反色后的缩略图 + */ + QPixmap nightPixmap(const QPixmap &src, const QVector &imageRects = QVector()) const; + private: QAbstractItemView *m_parent = nullptr; + + mutable QPixmap m_nightSourceCache; // 反色缓存的源缩略图 + mutable QPixmap m_nightPixmapCache; // 反色后的缩略图 }; #endif // SEARCHRESDELEGATE_H diff --git a/reader/sidebar/SideBarImageViewModel.cpp b/reader/sidebar/SideBarImageViewModel.cpp index f8ab61d49..3d64d743b 100644 --- a/reader/sidebar/SideBarImageViewModel.cpp +++ b/reader/sidebar/SideBarImageViewModel.cpp @@ -159,6 +159,9 @@ QVariant SideBarImageViewModel::data(const QModelIndex &index, int role) const } else if (role == ImageinfoType_e::IMAGE_PAGE_SIZE) { // qCDebug(appLog) << "Getting page size for index:" << nRow; return QVariant::fromValue(m_sheet->pageSizeByIndex(nRow)); + } else if (role == ImageinfoType_e::IMAGE_NIGHT_MASK) { + // 图片对象 bbox:夜间/深色反色时跳过这些区域,与主视图(BrowserPage)蒙版行为一致 + return QVariant::fromValue(m_sheet->thumbnailImageRects(nRow)); } return QVariant(); } @@ -302,11 +305,11 @@ int SideBarImageViewModel::findItemForAnno(deepin_reader::Annotation *annotation return -1; } -void SideBarImageViewModel::handleRenderThumbnail(int index, QPixmap pixmap) +void SideBarImageViewModel::handleRenderThumbnail(int index, QPixmap pixmap, const QVector &imageRects) { - qCDebug(appLog) << "Handling thumbnail render for page:" << index << "size:" << pixmap.size(); + qCDebug(appLog) << "Handling thumbnail render for page:" << index << "size:" << pixmap.size() << "imageRects:" << imageRects.size(); pixmap.setDevicePixelRatio(dApp->devicePixelRatio()); - m_sheet->setThumbnail(index, pixmap); + m_sheet->setThumbnail(index, pixmap, imageRects); m_pendingUpdatePages.insert(index); if (!m_batchUpdateTimer->isActive()) { diff --git a/reader/sidebar/SideBarImageViewModel.h b/reader/sidebar/SideBarImageViewModel.h index 8e8b877e8..f89011a09 100644 --- a/reader/sidebar/SideBarImageViewModel.h +++ b/reader/sidebar/SideBarImageViewModel.h @@ -10,11 +10,15 @@ #include #include #include +#include +#include namespace deepin_reader { class Annotation; } +Q_DECLARE_METATYPE(QVector) + typedef enum E_SideBar { SIDE_THUMBNIL = 0, SIDE_BOOKMARK, @@ -31,6 +35,7 @@ typedef enum ImageinfoType_e { IMAGE_CONTENT_TEXT = Qt::UserRole + 4, IMAGE_SEARCH_COUNT = Qt::UserRole + 5, IMAGE_PAGE_SIZE = Qt::UserRole + 6, + IMAGE_NIGHT_MASK = Qt::UserRole + 7, //图片对象 bbox 列表(夜间/深色反色时跳过图片区域,与 IMAGE_PIXMAP 像素对齐) } ImageinfoType_e; typedef struct ImagePageInfo_t { @@ -157,8 +162,9 @@ class SideBarImageViewModel : public QAbstractListModel * 处理缩略图 * @param index * @param pixmap + * @param imageRects 图片对象 bbox(与 pixmap 像素对齐,夜间/深色反色跳过图片区域用) */ - void handleRenderThumbnail(int index, QPixmap pixmap); + void handleRenderThumbnail(int index, QPixmap pixmap, const QVector &imageRects = QVector()); public slots: /** diff --git a/reader/sidebar/ThumbnailDelegate.cpp b/reader/sidebar/ThumbnailDelegate.cpp index d5b199e3f..09d5b0258 100644 --- a/reader/sidebar/ThumbnailDelegate.cpp +++ b/reader/sidebar/ThumbnailDelegate.cpp @@ -5,7 +5,6 @@ #include "ThumbnailDelegate.h" #include "SideBarImageViewModel.h" -#include "EyeProtectionManager.h" #include "NightFilter.h" #include "Utils.h" #include "Application.h" @@ -67,34 +66,20 @@ void ThumbnailDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt QPainterPath clipPath; clipPath.addRoundedRect(rect, borderRadius, borderRadius); painter->setClipPath(clipPath); - // 缩略图外观必须与主视图(BrowserPage::paint)保持一致,因此跟随护眼模式而非 - // 系统深浅主题:此前按系统深色主题反色,深色主题 + 无护眼时会出现主视图 - // 仍是白底、侧边栏缩略图却是黑底的不一致(尤其白底的 OFD/PDF 文档)。 - // 使用主干的夜间滤镜;缩略图尚无图片对象蒙版,采用其整页回退路径: - // Night → CIELAB 明度反转 + 深色压暗 - // Classic/Green → 正片叠底(Multiply)染色 - // Off → 原图 - EyeProtectionManager *epMgr = EyeProtectionManager::instance(); - const EyeProtectionManager::Mode mode = epMgr->mode(); + // 缩略图反色只跟随系统深色主题:深色主题下白底文档反转为黑底白字 + // (与书签/注释列表观感一致),浅色主题照常绘制原图。 + // 反色统一走主干夜间滤镜 NightFilter(CIELAB L* 反转,不再用旧 HSL 方案), + // 图片对象区域蒙版随缩略图由渲染线程预取(与主视图同源),照片区域不反色, + // 并含扫描页覆盖率特判(超过阈值整页反色,避免回贴/蒙版异常) + const bool darkTheme = (DTK_NAMESPACE::Gui::DGuiApplicationHelper::instance()->themeType() == DTK_NAMESPACE::Gui::DGuiApplicationHelper::DarkType); + const QVector imageRects = index.data(ImageinfoType_e::IMAGE_NIGHT_MASK).value>(); // 反色结果按未旋转的原始缩略图缓存,再叠加旋转,避免每次重绘都逐像素反色 - const QPixmap displayPixmap = (mode == EyeProtectionManager::Night) - ? nightPixmap(rawPixmap).transformed(transform) + const QPixmap displayPixmap = darkTheme + ? nightPixmap(rawPixmap, imageRects).transformed(transform) : pixmap; painter->drawPixmap(rect.x(), rect.y(), rect.width(), rect.height(), displayPixmap); - - if (mode == EyeProtectionManager::Night) { - // 与主视图一致:叠加轻微深色半透明层降低整体亮度 - QColor dark = epMgr->pageBackgroundColor(); - dark.setAlpha(60); - painter->fillRect(rect, dark); - } else if (mode != EyeProtectionManager::Off) { - // 与主视图一致:经典/绿色护眼用正片叠底染色 - painter->setCompositionMode(QPainter::CompositionMode_Multiply); - painter->fillRect(rect, epMgr->pageBackgroundColor()); - painter->setCompositionMode(QPainter::CompositionMode_SourceOver); - } painter->restore(); } @@ -128,17 +113,18 @@ QSize ThumbnailDelegate::sizeHint(const QStyleOptionViewItem &option, const QMod return DStyledItemDelegate::sizeHint(option, index); } -QPixmap ThumbnailDelegate::nightPixmap(const QPixmap &src) const +QPixmap ThumbnailDelegate::nightPixmap(const QPixmap &src, const QVector &imageRects) const { if (src.isNull()) return src; - // 滚动/选中时同一张缩略图会被反复重绘,缓存反色结果避免逐像素重复计算 + // 滚动/选中时同一张缩略图会被反复重绘,缓存反色结果避免逐像素重复计算; + // 缩略图重渲染时 cacheKey 必然变化,无需将蒙版纳入缓存键 if (m_nightSourceCache.cacheKey() == src.cacheKey() && !m_nightPixmapCache.isNull()) return m_nightPixmapCache; m_nightSourceCache = src; - m_nightPixmapCache = QPixmap::fromImage(NightFilter::applyPage(src.toImage(), {})); + m_nightPixmapCache = QPixmap::fromImage(NightFilter::applyPage(src.toImage(), imageRects)); m_nightPixmapCache.setDevicePixelRatio(src.devicePixelRatio()); return m_nightPixmapCache; } diff --git a/reader/sidebar/ThumbnailDelegate.h b/reader/sidebar/ThumbnailDelegate.h index 80f6bdd4c..573611429 100644 --- a/reader/sidebar/ThumbnailDelegate.h +++ b/reader/sidebar/ThumbnailDelegate.h @@ -50,11 +50,14 @@ class ThumbnailDelegate : public DStyledItemDelegate /** * @brief nightPixmap - * 夜间护眼模式下缩略图的智能反色结果(带缓存) + * 夜间智能滤镜/深色主题下缩略图的反色结果(带缓存): + * 走主干 NightFilter 管线(CIELAB L* 反转 + 图片对象区域跳过 + 扫描页整页反色特判), + * 与主视图观感一致 * @param src 原始缩略图 + * @param imageRects 图片对象 bbox(与 src 像素对齐,这些区域不反色避免照片负片) * @return 反色后的缩略图 */ - QPixmap nightPixmap(const QPixmap &src) const; + QPixmap nightPixmap(const QPixmap &src, const QVector &imageRects = QVector()) const; private: QAbstractItemView *m_parent = nullptr; diff --git a/reader/uiframe/DocSheet.cpp b/reader/uiframe/DocSheet.cpp index f0fd3c77b..a07111aa9 100644 --- a/reader/uiframe/DocSheet.cpp +++ b/reader/uiframe/DocSheet.cpp @@ -540,10 +540,17 @@ QPixmap DocSheet::thumbnail(int index) return m_thumbnailMap.value(index); } -void DocSheet::setThumbnail(int index, QPixmap pixmap) +QVector DocSheet::thumbnailImageRects(int index) +{ + // qCDebug(appLog) << "thumbnailImageRects"; + return m_thumbnailImageRects.value(index); +} + +void DocSheet::setThumbnail(int index, QPixmap pixmap, const QVector &imageRects) { // qCDebug(appLog) << "setThumbnail"; m_thumbnailMap[index] = pixmap; + m_thumbnailImageRects[index] = imageRects; } void DocSheet::setScaleMode(Dr::ScaleMode mode) diff --git a/reader/uiframe/DocSheet.h b/reader/uiframe/DocSheet.h index c8244acaf..4be2d37cd 100644 --- a/reader/uiframe/DocSheet.h +++ b/reader/uiframe/DocSheet.h @@ -321,12 +321,21 @@ class DocSheet : public Dtk::Widget::DSplitter */ QPixmap thumbnail(int index); + /** + * @brief thumbnailImageRects + * 获取缩略图对应的图片对象 bbox(夜间/深色蒙版用,与缩略图像素对齐;需要先设置) + * @param index + * @return 该页无图片对象时为空 + */ + QVector thumbnailImageRects(int index); + /** * @brief setThumbnail * @param index * @param pixmap + * @param imageRects 图片对象 bbox(缩略图渲染坐标系,与 pixmap 对齐),随缩略图一同缓存 */ - void setThumbnail(int index, QPixmap pixmap); + void setThumbnail(int index, QPixmap pixmap, const QVector &imageRects = QVector()); /** * @brief openMagnifier @@ -983,6 +992,7 @@ private slots: QString m_uuid; QTemporaryDir *m_tempDir = nullptr; //存放临时数据 QMap m_thumbnailMap; + QMap> m_thumbnailImageRects; //缩略图图片对象 bbox(与 m_thumbnailMap 同生命周期) bool m_documentChanged = false; bool m_bookmarkChanged = false; diff --git a/tests/browser/ut_pagerenderthread.cpp b/tests/browser/ut_pagerenderthread.cpp index 8f0294f75..c7904c299 100644 --- a/tests/browser/ut_pagerenderthread.cpp +++ b/tests/browser/ut_pagerenderthread.cpp @@ -11,6 +11,7 @@ #include "stub.h" #include +#include #include #include #include @@ -82,7 +83,7 @@ static void handleAnnotationLoaded_stub(const QList g_funcName = __FUNCTION__; } -static void handleRenderThumbnail_stub(int, QPixmap) +static void handleRenderThumbnail_stub(int, QPixmap, const QVector &) { g_funcName = __FUNCTION__; } @@ -485,5 +486,132 @@ TEST_F(TestPageRenderThread, UT_PageRenderThread_appendTask_Close) m_tester->m_closeTasks.clear(); } +/**********execNextDocPageThumbnailTask*************/ + +// DocSheet::existSheetByUuid 档:任务校验通过 +static bool existSheetByUuid_true_stub(const QString &) +{ + return true; +} + +// execNextDocPageThumbnailTask: 预取图片对象 bbox(夜间/深色蒙版用)并随任务转发给模型 +static QVector g_imageRectsResult; // getImageObjectRects 返回值(输入) +static QVector g_forwardedRects; // handleRenderThumbnail 收到的 bbox(输出) +static QVector getImageObjectRects_stub(int, int, int) +{ + return g_imageRectsResult; +} + +static bool opened_true_stub() +{ + return true; +} + +static bool opened_false_stub() +{ + return false; +} + +// 足够大的可写静态存储充当假 sheet(同 onDocOpenTask_002 做法) +static unsigned char fake_sheet_storage[8192] = {}; +// 假 model:任务经信号转发到 onDocPageThumbnailTask 时以 existSheet+model 调 handleRenderThumbnail +static unsigned char fake_model_storage[8192] = {}; + +// 空删除器:测试结束不删除 renderer,避免 ~SheetRenderer 往任务池塞关闭任务 +// (会 start 真实工作线程,与测试拆解竞态导致堆损坏) +static void thumbnailRendererNoopDeleter(SheetRenderer *) {} + +static QImage getImage_stub(int, int, int, const QRect &) +{ + QImage img(174, 174, QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::white); + return img; +} + +// 成员函数桩:首参占位 this(Itanium ABI 下成员函数≈隐式 this 的自由函数), +// 否则实参寄存器整体错位,解引用 rects 会读到垃圾内存 +static void handleRenderThumbnail_rects_stub(SideBarImageViewModel *, int, QPixmap, const QVector &rects) +{ + g_funcName = "handleRenderThumbnail_rects_stub"; + g_forwardedRects = rects; +} + +TEST_F(TestPageRenderThread, UT_PageRenderThread_execNextDocPageThumbnailTask_prefetchesImageRects) +{ + // renderStub 由 RAII 管理恢复,避免影响后续用例 + Stub s; + const QVector rects { QRectF(1, 2, 3, 4) }; + g_imageRectsResult = rects; + g_funcName.clear(); + + s.set(ADDR(DocSheet, existSheetByUuid), existSheetByUuid_true_stub); + // 任务经信号转发到 onDocPageThumbnailTask,其中以 existSheet 校验后调 handleRenderThumbnail + s.set(ADDR(DocSheet, existSheet), existSheet_true_stub); + s.set(ADDR(SheetRenderer, getImage), getImage_stub); + s.set(ADDR(SheetRenderer, opened), opened_true_stub); + s.set(ADDR(SheetRenderer, getImageObjectRects), getImageObjectRects_stub); + s.set(ADDR(SideBarImageViewModel, handleRenderThumbnail), handleRenderThumbnail_rects_stub); + + DocPageThumbnailTask task; + task.sheet = reinterpret_cast(fake_sheet_storage); // 成员调用均已被桩 + task.uuid = "ut-sheet-uuid"; // 与 uuid_stub 一致,校验通过 + // renderer 为空会提前结束任务;空删除器避免析构竞态(见 thumbnailRendererNoopDeleter) + task.renderer = QSharedPointer(renderer_stub(), &thumbnailRendererNoopDeleter); + task.model = reinterpret_cast(fake_model_storage); // 槽内 model->handleRenderThumbnail 已被桩 + task.index = 0; + m_tester->m_pageThumbnailTasks.append(task); + + EXPECT_TRUE(m_tester->execNextDocPageThumbnailTask()); + EXPECT_TRUE(m_tester->m_pageThumbnailTasks.isEmpty()); + // sigDocPageThumbnailTaskFinished 为 QueuedConnection,手动派发队列中的槽调用 + QEventLoop loop; + QMetaObject::invokeMethod(&loop, "quit", Qt::QueuedConnection); + loop.exec(); + QCoreApplication::sendPostedEvents(nullptr, QEvent::MetaCall); + // 任务转发到 handleRenderThumbnail,且预取的 bbox 原样传给模型 + EXPECT_TRUE(g_funcName == "handleRenderThumbnail_rects_stub"); + EXPECT_TRUE(g_forwardedRects == rects); + g_imageRectsResult.clear(); + g_forwardedRects.clear(); +} + +// 渲染器未打开时不预取 bbox,任务照常转发(imageRects 为空) +TEST_F(TestPageRenderThread, UT_PageRenderThread_execNextDocPageThumbnailTask_skipRectsWhenNotOpened) +{ + Stub s; + static const QVector kEmpty; + g_imageRectsResult = QVector() << QRectF(9, 9, 9, 9); // 若被误取会带入任务 + g_funcName.clear(); + + s.set(ADDR(DocSheet, existSheetByUuid), existSheetByUuid_true_stub); + // 任务经信号转发到 onDocPageThumbnailTask,其中以 existSheet 校验后调 handleRenderThumbnail + s.set(ADDR(DocSheet, existSheet), existSheet_true_stub); + s.set(ADDR(SheetRenderer, getImage), getImage_stub); + s.set(ADDR(SheetRenderer, opened), opened_false_stub); + s.set(ADDR(SheetRenderer, getImageObjectRects), getImageObjectRects_stub); + s.set(ADDR(SideBarImageViewModel, handleRenderThumbnail), handleRenderThumbnail_rects_stub); + + DocPageThumbnailTask task; + task.sheet = reinterpret_cast(fake_sheet_storage); + task.uuid = "ut-sheet-uuid"; + // renderer 为空会提前结束任务;空删除器避免析构竞态(见 thumbnailRendererNoopDeleter) + task.renderer = QSharedPointer(renderer_stub(), &thumbnailRendererNoopDeleter); + task.model = reinterpret_cast(fake_model_storage); // 槽内 model->handleRenderThumbnail 已被桩 + task.index = 0; + m_tester->m_pageThumbnailTasks.append(task); + + EXPECT_TRUE(m_tester->execNextDocPageThumbnailTask()); + // 同上:派发队列中的槽调用后再校验 + QEventLoop loop2; + QMetaObject::invokeMethod(&loop2, "quit", Qt::QueuedConnection); + loop2.exec(); + QCoreApplication::sendPostedEvents(nullptr, QEvent::MetaCall); + EXPECT_TRUE(g_funcName == "handleRenderThumbnail_rects_stub"); + // 未预取:转发给模型的 bbox 应为空 + EXPECT_TRUE(g_forwardedRects.isEmpty()); + g_imageRectsResult.clear(); + g_forwardedRects.clear(); +} + // (NullInstance test removed: modifying s_quitForever corrupts global state // and causes segfaults in subsequent DocSheet destructor tests.) diff --git a/tests/sidebar/ut_bookmarkdelegate.cpp b/tests/sidebar/ut_bookmarkdelegate.cpp index b87442b33..debdc0aba 100644 --- a/tests/sidebar/ut_bookmarkdelegate.cpp +++ b/tests/sidebar/ut_bookmarkdelegate.cpp @@ -14,6 +14,45 @@ #include #include #include +#include + +#include +#include + +DGUI_USE_NAMESPACE + +namespace { + +// 未打开文档时渲染器没有页面尺寸,桩掉以得到稳定的缩略图卡片区域 +QSizeF pageSizeByIndex_stub(DocSheet *, int) +{ + return QSizeF(210, 297); +} + +// 测试期间屏蔽 dtk 主题持久化,避免 setPaletteType 污染用户配置 +class ThemeGuard +{ +public: + explicit ThemeGuard(DGuiApplicationHelper::ColorType type) + { + DGuiApplicationHelper::setAttribute(DGuiApplicationHelper::DontSaveApplicationTheme, true); + m_previous = DGuiApplicationHelper::instance()->themeType(); + DGuiApplicationHelper::instance()->setPaletteType(type); + } + ~ThemeGuard() + { + if (m_previous != DGuiApplicationHelper::UnknownType) + DGuiApplicationHelper::instance()->setPaletteType(m_previous); + else + DGuiApplicationHelper::instance()->setPaletteType(DGuiApplicationHelper::LightType); + DGuiApplicationHelper::setAttribute(DGuiApplicationHelper::DontSaveApplicationTheme, false); + } + +private: + DGuiApplicationHelper::ColorType m_previous = DGuiApplicationHelper::UnknownType; +}; + +} // namespace class UT_BookMarkDelegate : public ::testing::Test { @@ -59,3 +98,103 @@ TEST_F(UT_BookMarkDelegate, UT_BookMarkDelegate_paint) EXPECT_TRUE(m_tester->m_parent == m_pView); delete painter; } + +namespace { + +// 绘制到离屏画布:页面 (210,297) 按 62×62 等比缩放后绘制在 +// (option.rect.x()+10, 垂直居中) 处,返回缩略图卡片内相对坐标 (rx, ry) 处像素 +QColor paintPixelAt(UT_BookMarkDelegate *fixture, const QPixmap &thumb, int rx, int ry) +{ + fixture->m_pView->getImageModel()->insertPageIndex(0); + fixture->m_sheet->setThumbnail(0, thumb); + + const QModelIndex index = fixture->m_pView->getImageModel()->index(0, 0); + if (!index.isValid()) + return QColor(); + + QStyleOptionViewItem option; + option.rect = QRect(0, 0, 200, 300); + + QImage canvas(200, 300, QImage::Format_ARGB32_Premultiplied); + canvas.fill(Qt::red); // 红底便于观察是否被绘制覆盖 + QPainter painter(&canvas); + fixture->m_tester->paint(&painter, option, index); + painter.end(); + + // 缩略图卡片:宽 44(=62*210/297)、高 62,起点 (10, 150-31) + return canvas.pixelColor(10 + rx, 150 - 31 + ry); +} + +} // namespace + +// 浅色主题:书签列表保持文档原始白底 +TEST_F(UT_BookMarkDelegate, UT_BookMarkDelegate_paintLightThemeKeepsWhitePage) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard light(DGuiApplicationHelper::LightType); + + QPixmap whiteThumb(174, 246); + whiteThumb.fill(Qt::white); + const QColor center = paintPixelAt(this, whiteThumb, 22, 31); + + EXPECT_GT(center.lightness(), 239); +} + +// 深色主题:白底反转为深色(走 NightFilter 主干滤镜) +TEST_F(UT_BookMarkDelegate, UT_BookMarkDelegate_paintDarkThemeInvertsWhitePage) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard dark(DGuiApplicationHelper::DarkType); + + QPixmap whiteThumb(174, 246); + whiteThumb.fill(Qt::white); + const QColor center = paintPixelAt(this, whiteThumb, 22, 31); + + EXPECT_LT(center.lightness(), 32); +} + +// 深色主题 + 图片对象蒙版:照片区域保持原始像素,白底反转为深色 +TEST_F(UT_BookMarkDelegate, UT_BookMarkDelegate_paintDarkThemeWithNightMaskKeepsPhotoPixels) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard dark(DGuiApplicationHelper::DarkType); + + // 左半为纯色照片块(70,70,70),右半白底;蒙版罩住照片(源缩略图 174 像素坐标) + QPixmap mixed(174, 246); + mixed.fill(Qt::white); + QPainter p(&mixed); + p.fillRect(0, 0, 87, 246, QColor(70, 70, 70)); + p.end(); + + m_pView->getImageModel()->insertPageIndex(0); + m_sheet->setThumbnail(0, mixed, QVector() << QRectF(0, 0, 87, 246)); + + const QModelIndex index = m_pView->getImageModel()->index(0, 0); + ASSERT_TRUE(index.isValid()); + + QStyleOptionViewItem option; + option.rect = QRect(0, 0, 200, 300); + + QImage canvas(200, 300, QImage::Format_ARGB32_Premultiplied); + canvas.fill(Qt::red); + QPainter painter(&canvas); + m_tester->paint(&painter, option, index); + painter.end(); + + // 蒙版区域(照片中心,避开缩放采样边界)像素保持不变 + EXPECT_EQ(canvas.pixelColor(10 + 15, 150), QColor(70, 70, 70)); + // 蒙版外白底已反转为深色 + EXPECT_LT(canvas.pixelColor(10 + 40, 150).lightness(), 32); +} diff --git a/tests/sidebar/ut_notesdelegate.cpp b/tests/sidebar/ut_notesdelegate.cpp index 329efeadb..a73c402bb 100644 --- a/tests/sidebar/ut_notesdelegate.cpp +++ b/tests/sidebar/ut_notesdelegate.cpp @@ -14,6 +14,45 @@ #include #include #include +#include + +#include +#include + +DGUI_USE_NAMESPACE + +namespace { + +// 未打开文档时渲染器没有页面尺寸,桩掉以得到稳定的缩略图卡片区域 +QSizeF pageSizeByIndex_stub(DocSheet *, int) +{ + return QSizeF(210, 297); +} + +// 测试期间屏蔽 dtk 主题持久化,避免 setPaletteType 污染用户配置 +class ThemeGuard +{ +public: + explicit ThemeGuard(DGuiApplicationHelper::ColorType type) + { + DGuiApplicationHelper::setAttribute(DGuiApplicationHelper::DontSaveApplicationTheme, true); + m_previous = DGuiApplicationHelper::instance()->themeType(); + DGuiApplicationHelper::instance()->setPaletteType(type); + } + ~ThemeGuard() + { + if (m_previous != DGuiApplicationHelper::UnknownType) + DGuiApplicationHelper::instance()->setPaletteType(m_previous); + else + DGuiApplicationHelper::instance()->setPaletteType(DGuiApplicationHelper::LightType); + DGuiApplicationHelper::setAttribute(DGuiApplicationHelper::DontSaveApplicationTheme, false); + } + +private: + DGuiApplicationHelper::ColorType m_previous = DGuiApplicationHelper::UnknownType; +}; + +} // namespace class UT_NotesDelegate : public ::testing::Test { @@ -60,6 +99,106 @@ TEST_F(UT_NotesDelegate, UT_NotesDelegate_paint) delete painter; } +namespace { + +// 绘制到离屏画布:页面 (210,297) 按 62×62 等比缩放后绘制在 +// (option.rect.x()+10, 垂直居中) 处,返回缩略图卡片内相对坐标 (rx, ry) 处像素 +QColor paintPixelAt(UT_NotesDelegate *fixture, const QPixmap &thumb, int rx, int ry) +{ + fixture->m_pView->getImageModel()->insertPageIndex(0); + fixture->m_sheet->setThumbnail(0, thumb); + + const QModelIndex index = fixture->m_pView->getImageModel()->index(0, 0); + if (!index.isValid()) + return QColor(); + + QStyleOptionViewItem option; + option.rect = QRect(0, 0, 200, 300); + + QImage canvas(200, 300, QImage::Format_ARGB32_Premultiplied); + canvas.fill(Qt::red); // 红底便于观察是否被绘制覆盖 + QPainter painter(&canvas); + fixture->m_tester->paint(&painter, option, index); + painter.end(); + + // 缩略图卡片:宽 44(=62*210/297)、高 62,起点 (10, 150-31) + return canvas.pixelColor(10 + rx, 150 - 31 + ry); +} + +} // namespace + +// 浅色主题:注释列表保持文档原始白底 +TEST_F(UT_NotesDelegate, UT_NotesDelegate_paintLightThemeKeepsWhitePage) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard light(DGuiApplicationHelper::LightType); + + QPixmap whiteThumb(174, 246); + whiteThumb.fill(Qt::white); + const QColor center = paintPixelAt(this, whiteThumb, 22, 31); + + EXPECT_GT(center.lightness(), 239); +} + +// 深色主题:白底反转为深色(走 NightFilter 主干滤镜) +TEST_F(UT_NotesDelegate, UT_NotesDelegate_paintDarkThemeInvertsWhitePage) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard dark(DGuiApplicationHelper::DarkType); + + QPixmap whiteThumb(174, 246); + whiteThumb.fill(Qt::white); + const QColor center = paintPixelAt(this, whiteThumb, 22, 31); + + EXPECT_LT(center.lightness(), 32); +} + +// 深色主题 + 图片对象蒙版:照片区域保持原始像素,白底反转为深色 +TEST_F(UT_NotesDelegate, UT_NotesDelegate_paintDarkThemeWithNightMaskKeepsPhotoPixels) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard dark(DGuiApplicationHelper::DarkType); + + // 左半为纯色照片块(70,70,70),右半白底;蒙版罩住照片(源缩略图 174 像素坐标) + QPixmap mixed(174, 246); + mixed.fill(Qt::white); + QPainter p(&mixed); + p.fillRect(0, 0, 87, 246, QColor(70, 70, 70)); + p.end(); + + m_pView->getImageModel()->insertPageIndex(0); + m_sheet->setThumbnail(0, mixed, QVector() << QRectF(0, 0, 87, 246)); + + const QModelIndex index = m_pView->getImageModel()->index(0, 0); + ASSERT_TRUE(index.isValid()); + + QStyleOptionViewItem option; + option.rect = QRect(0, 0, 200, 300); + + QImage canvas(200, 300, QImage::Format_ARGB32_Premultiplied); + canvas.fill(Qt::red); + QPainter painter(&canvas); + m_tester->paint(&painter, option, index); + painter.end(); + + // 蒙版区域(照片中心,避开缩放采样边界)像素保持不变 + EXPECT_EQ(canvas.pixelColor(10 + 15, 150), QColor(70, 70, 70)); + // 蒙版外白底已反转为深色 + EXPECT_LT(canvas.pixelColor(10 + 40, 150).lightness(), 32); +} + TEST_F(UT_NotesDelegate, UT_NotesDelegate_sizeHint) { m_pView->getImageModel()->insertPageIndex(1); diff --git a/tests/sidebar/ut_searchresdelegate.cpp b/tests/sidebar/ut_searchresdelegate.cpp index 45b5142c5..0b1e710b3 100644 --- a/tests/sidebar/ut_searchresdelegate.cpp +++ b/tests/sidebar/ut_searchresdelegate.cpp @@ -1,12 +1,12 @@ -// Copyright (C) 2019-2026 ~ 2020 UnionTech Software Technology Co.,Ltd. -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2019 ~ 2026 Uniontech Software Technology Co.,Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later #include "SearchResDelegate.h" -#include "DocSheet.h" #include "SideBarImageListview.h" #include "SideBarImageViewModel.h" +#include "DocSheet.h" #include "stub.h" @@ -14,6 +14,45 @@ #include #include #include +#include + +#include +#include + +DGUI_USE_NAMESPACE + +namespace { + +// 未打开文档时渲染器没有页面尺寸,桩掉以得到稳定的搜索结果卡片区域 +QSizeF pageSizeByIndex_stub(DocSheet *, int) +{ + return QSizeF(210, 297); +} + +// 测试期间屏蔽 dtk 主题持久化,避免 setPaletteType 污染用户配置 +class ThemeGuard +{ +public: + explicit ThemeGuard(DGuiApplicationHelper::ColorType type) + { + DGuiApplicationHelper::setAttribute(DGuiApplicationHelper::DontSaveApplicationTheme, true); + m_previous = DGuiApplicationHelper::instance()->themeType(); + DGuiApplicationHelper::instance()->setPaletteType(type); + } + ~ThemeGuard() + { + if (m_previous != DGuiApplicationHelper::UnknownType) + DGuiApplicationHelper::instance()->setPaletteType(m_previous); + else + DGuiApplicationHelper::instance()->setPaletteType(DGuiApplicationHelper::LightType); + DGuiApplicationHelper::setAttribute(DGuiApplicationHelper::DontSaveApplicationTheme, false); + } + +private: + DGuiApplicationHelper::ColorType m_previous = DGuiApplicationHelper::UnknownType; +}; + +} // namespace class UT_SearchResDelegate : public ::testing::Test { @@ -27,6 +66,7 @@ class UT_SearchResDelegate : public ::testing::Test strPath += "/files/1.pdf"; m_sheet = new DocSheet(Dr::PDF, strPath, nullptr); m_pView = new SideBarImageListView(m_sheet); + m_pView->setListType(E_SideBar::SIDE_SEARCH); m_tester = new SearchResDelegate(m_pView); m_pView->setItemDelegate(m_tester); m_tester->disconnect(); @@ -60,6 +100,107 @@ TEST_F(UT_SearchResDelegate, UT_SearchResDelegate_paint) delete painter; } +namespace { + +// 绘制到离屏画布:页面 (210,297) 按 62×62 等比缩放后绘制在 +// (option.rect.x()+10, 垂直居中) 处,返回卡片内相对坐标 (rx, ry) 处像素 +QColor paintPixelAt(UT_SearchResDelegate *fixture, const QPixmap &thumb, int rx, int ry) +{ + fixture->m_pView->getImageModel()->insertPageIndex(0); + fixture->m_sheet->setThumbnail(0, thumb); + + const QModelIndex index = fixture->m_pView->getImageModel()->index(0, 0); + if (!index.isValid()) + return QColor(); + + QStyleOptionViewItem option; + option.rect = QRect(0, 0, 200, 300); + + QImage canvas(200, 300, QImage::Format_ARGB32_Premultiplied); + canvas.fill(Qt::red); // 红底便于观察是否被绘制覆盖 + QPainter painter(&canvas); + fixture->m_tester->paint(&painter, option, index); + painter.end(); + + // 搜索结果卡片:宽 43(=62*210/297)、高 62,起点 (10, 150-31) + return canvas.pixelColor(10 + rx, 150 - 31 + ry); +} + +} // namespace + +// 浅色主题:搜索结果页小图保持文档原始白底 +TEST_F(UT_SearchResDelegate, UT_SearchResDelegate_paintLightThemeKeepsWhitePage) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard light(DGuiApplicationHelper::LightType); + + QPixmap whiteThumb(174, 246); + whiteThumb.fill(Qt::white); + const QColor center = paintPixelAt(this, whiteThumb, 20, 31); + + EXPECT_GT(center.lightness(), 239); +} + +// 深色主题:触发搜索后结果页小图同样反转为深色(走 NightFilter 主干滤镜), +// 不能再显示原始白底 +TEST_F(UT_SearchResDelegate, UT_SearchResDelegate_paintDarkThemeInvertsWhitePage) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard dark(DGuiApplicationHelper::DarkType); + + QPixmap whiteThumb(174, 246); + whiteThumb.fill(Qt::white); + const QColor center = paintPixelAt(this, whiteThumb, 20, 31); + + EXPECT_LT(center.lightness(), 32); +} + +// 深色主题 + 图片对象蒙版:搜索结果页小图中照片区域保持原始像素,白底反转为深色 +TEST_F(UT_SearchResDelegate, UT_SearchResDelegate_paintDarkThemeWithNightMaskKeepsPhotoPixels) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard dark(DGuiApplicationHelper::DarkType); + + // 左半为纯色照片块(70,70,70),右半白底;蒙版罩住照片(源缩略图 174 像素坐标) + QPixmap mixed(174, 246); + mixed.fill(Qt::white); + QPainter p(&mixed); + p.fillRect(0, 0, 87, 246, QColor(70, 70, 70)); + p.end(); + + m_pView->getImageModel()->insertPageIndex(0); + m_sheet->setThumbnail(0, mixed, QVector() << QRectF(0, 0, 87, 246)); + + const QModelIndex index = m_pView->getImageModel()->index(0, 0); + ASSERT_TRUE(index.isValid()); + + QStyleOptionViewItem option; + option.rect = QRect(0, 0, 200, 300); + + QImage canvas(200, 300, QImage::Format_ARGB32_Premultiplied); + canvas.fill(Qt::red); + QPainter painter(&canvas); + m_tester->paint(&painter, option, index); + painter.end(); + + // 蒙版区域(照片中心,避开缩放采样边界)像素保持不变 + EXPECT_EQ(canvas.pixelColor(10 + 10, 150), QColor(70, 70, 70)); + // 蒙版外白底已反转为深色 + EXPECT_LT(canvas.pixelColor(10 + 35, 150).lightness(), 32); +} + TEST_F(UT_SearchResDelegate, UT_SearchResDelegate_sizeHint) { m_pView->getImageModel()->insertPageIndex(1); diff --git a/tests/sidebar/ut_sidebarimageviewmodel.cpp b/tests/sidebar/ut_sidebarimageviewmodel.cpp index 20b38cee0..1ce5ac713 100644 --- a/tests/sidebar/ut_sidebarimageviewmodel.cpp +++ b/tests/sidebar/ut_sidebarimageviewmodel.cpp @@ -195,6 +195,38 @@ TEST_F(TestSideBarImageViewModel, testhandleRenderThumbnail) m_tester->handleRenderThumbnail(0, QPixmap()); } +// handleRenderThumbnail 第三参(图片对象 bbox)需存入 DocSheet,供 data(IMAGE_NIGHT_MASK) 读取 +TEST_F(TestSideBarImageViewModel, testhandleRenderThumbnailStoresImageRects) +{ + const QVector rects { QRectF(1, 2, 3, 4), QRectF(5, 6, 7, 8) }; + QPixmap thumb(174, 174); + thumb.fill(Qt::white); + + m_tester->handleRenderThumbnail(0, thumb, rects); + + EXPECT_TRUE(m_sheet->thumbnailImageRects(0) == rects); + + // data(IMAGE_NIGHT_MASK) 返回同一份 bbox,供代理反色时跳过图片区域 + m_tester->insertPageIndex(0); + const QModelIndex index = m_tester->index(0, 0); + ASSERT_TRUE(index.isValid()); + const QVector got = + index.data(ImageinfoType_e::IMAGE_NIGHT_MASK).value>(); + EXPECT_TRUE(got == rects); +} + +// 未设置蒙版时 IMAGE_NIGHT_MASK 返回空列表(整页反色,不跳过图片区域) +TEST_F(TestSideBarImageViewModel, testImageNightMaskDefaultsEmpty) +{ + m_tester->insertPageIndex(0); + const QModelIndex index = m_tester->index(0, 0); + ASSERT_TRUE(index.isValid()); + const QVector got = + index.data(ImageinfoType_e::IMAGE_NIGHT_MASK).value>(); + EXPECT_TRUE(got.isEmpty()); + EXPECT_TRUE(m_sheet->thumbnailImageRects(0).isEmpty()); +} + TEST_F(TestSideBarImageViewModel, testonBatchUpdateTimer) { // Trigger onBatchUpdateTimer directly diff --git a/tests/sidebar/ut_thumbnaildelegate.cpp b/tests/sidebar/ut_thumbnaildelegate.cpp index 119fe170e..c8da55ee0 100644 --- a/tests/sidebar/ut_thumbnaildelegate.cpp +++ b/tests/sidebar/ut_thumbnaildelegate.cpp @@ -15,6 +15,12 @@ #include #include #include +#include + +#include +#include + +DGUI_USE_NAMESPACE namespace { @@ -24,6 +30,29 @@ QSizeF pageSizeByIndex_stub(DocSheet *, int) return QSizeF(210, 297); } +// 测试期间屏蔽 dtk 主题持久化,避免 setPaletteType 污染用户配置 +class ThemeGuard +{ +public: + explicit ThemeGuard(DGuiApplicationHelper::ColorType type) + { + DGuiApplicationHelper::setAttribute(DGuiApplicationHelper::DontSaveApplicationTheme, true); + m_previous = DGuiApplicationHelper::instance()->themeType(); + DGuiApplicationHelper::instance()->setPaletteType(type); + } + ~ThemeGuard() + { + if (m_previous != DGuiApplicationHelper::UnknownType) + DGuiApplicationHelper::instance()->setPaletteType(m_previous); + else + DGuiApplicationHelper::instance()->setPaletteType(DGuiApplicationHelper::LightType); + DGuiApplicationHelper::setAttribute(DGuiApplicationHelper::DontSaveApplicationTheme, false); + } + +private: + DGuiApplicationHelper::ColorType m_previous = DGuiApplicationHelper::UnknownType; +}; + } // namespace class UT_ThumbnailDelegate : public ::testing::Test @@ -96,19 +125,107 @@ TEST_F(UT_ThumbnailDelegate, UT_ThumbnailDelegate_sizeHint) EXPECT_FALSE(size.isEmpty()); } -// 缩略图外观跟随护眼模式(而非系统深浅主题),与主视图保持一致: -// 无护眼时保持文档原始白底,夜间护眼时反转为深色底 -TEST_F(UT_ThumbnailDelegate, UT_ThumbnailDelegate_paintFollowsEyeProtectionMode) +namespace { + +// 绘制到离屏画布,返回画面中心的像素(缩略图卡片正中) +QColor paintCenterPixel(UT_ThumbnailDelegate *fixture, const QPixmap &thumb) +{ + fixture->m_pView->getImageModel()->insertPageIndex(0); + fixture->m_sheet->setThumbnail(0, thumb); + + const QModelIndex index = fixture->m_pView->getImageModel()->index(0, 0); + if (!index.isValid()) + return QColor(); + + QStyleOptionViewItem option; + option.rect = QRect(0, 0, 240, 300); + + QImage canvas(240, 300, QImage::Format_ARGB32_Premultiplied); + canvas.fill(Qt::red); // 红底便于观察是否被绘制覆盖 + QPainter painter(&canvas); + fixture->m_tester->paint(&painter, option, index); + painter.end(); + return canvas.pixelColor(120, 150); +} + +} // namespace + +// 缩略图外观只跟随系统深浅主题(不再跟随护眼模式): +// 浅色主题保持文档原始白底,深色主题反转为深色底 +TEST_F(UT_ThumbnailDelegate, UT_ThumbnailDelegate_paintLightThemeKeepsWhitePage) { Stub s; typedef QSizeF(*fptr)(DocSheet *, int); fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); s.set(pageSizeFunc, pageSizeByIndex_stub); - m_pView->getImageModel()->insertPageIndex(0); + ThemeGuard light(DGuiApplicationHelper::LightType); + + QPixmap whiteThumb(174, 246); + whiteThumb.fill(Qt::white); + const QColor center = paintCenterPixel(this, whiteThumb); + + EXPECT_GT(center.lightness(), 239); +} + +// 深色主题下白底反转为深色(与 BookMark/Notes 列表观感一致) +TEST_F(UT_ThumbnailDelegate, UT_ThumbnailDelegate_paintDarkThemeInvertsWhitePage) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard dark(DGuiApplicationHelper::DarkType); + + QPixmap whiteThumb(174, 246); + whiteThumb.fill(Qt::white); + const QColor center = paintCenterPixel(this, whiteThumb); + + EXPECT_LT(center.lightness(), 32); +} + +// 护眼模式不再影响缩略图:夜间护眼开启时浅色主题下仍绘制原始白底 +TEST_F(UT_ThumbnailDelegate, UT_ThumbnailDelegate_paintIgnoresEyeProtectionMode) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard light(DGuiApplicationHelper::LightType); + + const EyeProtectionManager::Mode previousMode = EyeProtectionManager::instance()->mode(); + EyeProtectionManager::instance()->setMode(EyeProtectionManager::Night); + QPixmap whiteThumb(174, 246); whiteThumb.fill(Qt::white); - m_sheet->setThumbnail(0, whiteThumb); + const QColor center = paintCenterPixel(this, whiteThumb); + + EyeProtectionManager::instance()->setMode(previousMode); + + EXPECT_GT(center.lightness(), 239); +} + +// 深色主题 + 图片对象蒙版:蒙版区域(照片)保持原始像素,非蒙版区域(白底)反转为深色 +TEST_F(UT_ThumbnailDelegate, UT_ThumbnailDelegate_paintDarkThemeWithNightMaskKeepsPhotoPixels) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard dark(DGuiApplicationHelper::DarkType); + + // 左半为纯色照片块(70,70,70),右半白底;蒙版仅罩住照片,覆盖率 < 70% 不触发扫描页整页反色 + QPixmap mixed(174, 246); + mixed.fill(Qt::white); + QPainter p(&mixed); + p.fillRect(0, 0, 87, 246, QColor(70, 70, 70)); + p.end(); + + m_pView->getImageModel()->insertPageIndex(0); + m_sheet->setThumbnail(0, mixed, QVector() << QRectF(0, 0, 87, 246)); const QModelIndex index = m_pView->getImageModel()->index(0, 0); ASSERT_TRUE(index.isValid()); @@ -117,21 +234,46 @@ TEST_F(UT_ThumbnailDelegate, UT_ThumbnailDelegate_paintFollowsEyeProtectionMode) option.rect = QRect(0, 0, 240, 300); QImage canvas(240, 300, QImage::Format_ARGB32_Premultiplied); - - const auto previousMode = EyeProtectionManager::instance()->mode(); - EyeProtectionManager::instance()->setMode(EyeProtectionManager::Off); canvas.fill(Qt::red); - QPainter offPainter(&canvas); - m_tester->paint(&offPainter, option, index); - offPainter.end(); - EXPECT_GT(canvas.pixelColor(120, 150).lightness(), 239); + QPainter painter(&canvas); + m_tester->paint(&painter, option, index); + painter.end(); - EyeProtectionManager::instance()->setMode(EyeProtectionManager::Night); + // 蒙版区域像素保持不变(imageDimFactor=1 时 dimPixelImage 不修改像素值) + EXPECT_EQ(canvas.pixelColor(60, 150), QColor(70, 70, 70)); + // 蒙版外白底已反转为深色 + EXPECT_LT(canvas.pixelColor(180, 150).lightness(), 32); +} + +// 深色主题 + 扫描页(图片 bbox 覆盖率 > 70%):整页反色,蒙版不生效 +TEST_F(UT_ThumbnailDelegate, UT_ThumbnailDelegate_paintDarkThemeScannedPageFullInvert) +{ + Stub s; + typedef QSizeF(*fptr)(DocSheet *, int); + fptr pageSizeFunc = (fptr)(&DocSheet::pageSizeByIndex); + s.set(pageSizeFunc, pageSizeByIndex_stub); + + ThemeGuard dark(DGuiApplicationHelper::DarkType); + + // 整页照片(米黄纸面色):bbox 覆盖率 100%,按扫描页整页反色,深色像素反转后提亮为白 + QPixmap scanned(174, 246); + scanned.fill(QColor(60, 60, 60)); + + m_pView->getImageModel()->insertPageIndex(0); + m_sheet->setThumbnail(0, scanned, QVector() << QRectF(0, 0, 174, 246)); + + const QModelIndex index = m_pView->getImageModel()->index(0, 0); + ASSERT_TRUE(index.isValid()); + + QStyleOptionViewItem option; + option.rect = QRect(0, 0, 240, 300); + + QImage canvas(240, 300, QImage::Format_ARGB32_Premultiplied); canvas.fill(Qt::red); - QPainter nightPainter(&canvas); - m_tester->paint(&nightPainter, option, index); - nightPainter.end(); - EXPECT_LT(canvas.pixelColor(120, 150).lightness(), 32); + QPainter painter(&canvas); + m_tester->paint(&painter, option, index); + painter.end(); - EyeProtectionManager::instance()->setMode(previousMode); + // 中心点:若蒙版生效则保持 (60,60,60);整页反色后亮度提升,应明显偏亮 + EXPECT_GT(canvas.pixelColor(120, 150).lightness(), 128); } diff --git a/tests/uiframe/ut_docsheet.cpp b/tests/uiframe/ut_docsheet.cpp index 9049bd468..5a9370a78 100644 --- a/tests/uiframe/ut_docsheet.cpp +++ b/tests/uiframe/ut_docsheet.cpp @@ -728,6 +728,27 @@ TEST_F(TestDocSheet, UT_DocSheet_setThumbnail_001) EXPECT_TRUE(m_tester->m_thumbnailMap.size() == 1); } +// setThumbnail 带 bbox:thumbnailImageRects 返回同一份蒙版 +TEST_F(TestDocSheet, UT_DocSheet_thumbnailImageRects_001) +{ + EXPECT_TRUE(m_tester->thumbnailImageRects(1).isEmpty()); + + const QVector rects { QRectF(0, 0, 10, 20), QRectF(1, 1, 2, 2) }; + m_tester->setThumbnail(1, QPixmap(10, 20), rects); + EXPECT_TRUE(m_tester->thumbnailImageRects(1) == rects); +} + +// 不带 bbox 重设缩略图时,旧蒙版需被清空(避免残留旧页面蒙版) +TEST_F(TestDocSheet, UT_DocSheet_setThumbnailClearsStaleImageRects) +{ + const QVector rects { QRectF(0, 0, 10, 20) }; + m_tester->setThumbnail(2, QPixmap(10, 20), rects); + EXPECT_FALSE(m_tester->thumbnailImageRects(2).isEmpty()); + + m_tester->setThumbnail(2, QPixmap(10, 20)); + EXPECT_TRUE(m_tester->thumbnailImageRects(2).isEmpty()); +} + TEST_F(TestDocSheet, UT_DocSheet_setScaleMode_001) { m_tester->m_operation.scaleMode = Dr::FitToPageWorHMode;