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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion 3rdparty/deepin-pdfium/include/dpdfpage.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later

Expand Down Expand Up @@ -50,6 +50,15 @@ class DPdfPage : public QObject
*/
QImage image(int width, int height, QRect slice = QRect());

/**
* @brief 获取页面上图片对象的包围盒(夜间模式对象蒙版用,实验)
* 坐标与 image(width, height) 的整页渲染输出像素一一对齐(已处理页面自身旋转)
* @param width (in pixel) 目标渲染宽
* @param height (in pixel) 目标渲染高
* @return 图片对象包围盒列表(像素坐标,Qt 坐标系);无图片对象返回空
*/
QVector<QRectF> imageObjectRects(int width, int height);

/**
* @brief 字符数
* @return
Expand Down
60 changes: 60 additions & 0 deletions 3rdparty/deepin-pdfium/src/dpdfpage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,66 @@ int DPdfPage::countChars()
return FPDFText_CountChars(d_func()->m_textPage);
}

QVector<QRectF> DPdfPage::imageObjectRects(int width, int height)
{
QVector<QRectF> rects;

if (nullptr == d_func()->m_doc || width <= 0 || height <= 0)
return rects;

DPdfMutexLocker locker("DPdfPage::imageObjectRects index = " + QString::number(index()));

// 目标像素尺寸对应的缩放(与 image(width,height) 整页渲染对齐)
const QSizeF viewPx = d_func()->sizeF();

if (viewPx.width() <= 0 || viewPx.height() <= 0)
return rects;

// 对象列表需要解析内容(与 image() 一致用 FPDF_LoadPage);
// 取完后立即关闭,不缓存句柄,避免与渲染线程的页面句柄生命周期冲突
FPDF_PAGE page = FPDF_LoadPage(d_func()->m_doc, d_func()->m_index);

if (nullptr == page)
return rects;

CPDF_Page *pPage = CPDFPageFromFPDFPage(page);
const int rotation = pPage ? pPage->GetPageRotation() : 0;

const qreal sx = width / viewPx.width();
const qreal sy = height / viewPx.height();

const int count = FPDFPage_CountObjects(page);

for (int i = 0; i < count; ++i) {
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, i);

if (nullptr == obj || FPDF_PAGEOBJ_IMAGE != FPDFPageObj_GetType(obj))
continue;

float left = 0, bottom = 0, right = 0, top = 0;

if (!FPDFPageObj_GetBounds(obj, &left, &bottom, &right, &top))
continue;

// 坐标链:PDF 用户空间(y 向上)→ 旋转后视图坐标 → dpi 像素 → 目标像素
FS_RECTF fs;
fs.left = left;
fs.top = top;
fs.right = right;
fs.bottom = bottom;

QRectF viewPt = d_func()->transRect(rotation, fs);
QRectF px = d_func()->transPointToPixel(viewPt);

QRectF out(px.x() * sx, px.y() * sy, px.width() * sx, px.height() * sy);
rects.append(out.normalized());
}

FPDF_ClosePage(page);

return rects;
}

QVector<QRectF> DPdfPage::textRects(int start, int charCount)
{
d_func()->loadTextPage();
Expand Down
179 changes: 135 additions & 44 deletions reader/browser/BrowserPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
#include "BrowserAnnotation.h"
#include "Application.h"
#include "Utils.h"
#include "Global.h"

Check warning on line 13 in reader/browser/BrowserPage.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "Global.h" not found.
#include "SheetRenderer.h"

Check warning on line 14 in reader/browser/BrowserPage.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "SheetRenderer.h" not found.
#include "EyeProtectionManager.h"

Check warning on line 15 in reader/browser/BrowserPage.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "EyeProtectionManager.h" not found.
#include "NightFilter.h"
#include <QtConcurrent>

Check warning on line 17 in reader/browser/BrowserPage.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtConcurrent> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include "ddlog.h"

Check warning on line 18 in reader/browser/BrowserPage.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "ddlog.h" not found.

#include <DGuiApplicationHelper>

Check warning on line 20 in reader/browser/BrowserPage.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <DGuiApplicationHelper> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QPainter>
#include <QStyleOptionGraphicsItem>
#include <QGraphicsSceneMouseEvent>
Expand Down Expand Up @@ -48,6 +50,10 @@
// qCDebug(appLog) << "BrowserPage destroyed, index:" << m_index;
PageRenderThread::clearImageTasks(m_sheet, this);

// 断开并销毁夜间异步任务 watcher:后台滤镜任务持有的都是副本,安全丢弃
delete m_nightWatcher;
m_nightWatcher = nullptr;

qDeleteAll(m_annotations);

qDeleteAll(m_annotationItems);
Expand Down Expand Up @@ -154,16 +160,57 @@
EyeProtectionManager *epMgr = EyeProtectionManager::instance();

if (epMgr->mode() == EyeProtectionManager::Night) {
// 夜间模式:智能反色(HSL 亮度反转)
// 仅反转 Lightness 通道,保留 Hue/Saturation,避免图片/链接色相偏移 180°
// 白底黑字 → 黑底白字(文字/背景正确反色)
// 彩色图片/链接 → 仅变暗,色相保持
// 夜间模式:智能滤镜(对象蒙版 + CIELAB L* 反转)由后台线程异步生成,
// paint 只负责消费缓存结果,永不阻塞(旧实现同步逐像素转换是卡顿主因)
if (m_nightDirty || m_nightPixmap.isNull()) {
m_nightPixmap = applyNightMode(m_renderPixmap);
m_nightPixmap.setDevicePixelRatio(dApp->devicePixelRatio());
m_nightDirty = false;
if (!m_nightJobRunning)
startNightJob(); // 后台生成,完成后 finished 回调重绘

if (!m_nightPixmap.isNull()
&& qFuzzyCompare(m_nightPixmap.width() / m_nightPixmap.devicePixelRatio(),
boundingRect().width())
&& qFuzzyCompare(m_nightPixmap.height() / m_nightPixmap.devicePixelRatio(),
boundingRect().height())) {
// 夜间图与当前尺寸一致(如仅注释/文字层重绘):直接沿用,无任何过渡
painter->drawPixmap(0, 0, m_nightPixmap);
} else {
// 尺寸变化(缩放/旋转/首帧):用最新日间渲染即时合成近似夜间观感,
// 与日间模式同拍零延迟;精确夜间图异步完成后自动替换
// (旧实现拉伸旧夜间图,缩放时图片区域明显滞后一拍)
painter->drawPixmap(0, 0, m_renderPixmap);

// 1) 整页快速反相:栅格化 Difference 合成,毫秒级;文字/背景观感
// 接近精确反转,彩色图片暂偏色,由下一步回贴原图修正
painter->save();
painter->setCompositionMode(QPainter::CompositionMode_Difference);
painter->fillRect(boundingRect(), Qt::white);
painter->restore();

// 2) 图片对象区回贴原图:照片零负片,缩放后 bbox 随渲染尺寸等比换算
if (!m_imageRects.isEmpty() && m_rectsPixmapWidth > 0
&& m_rectsPixmapHeight > 0) {
const qreal kx = boundingRect().width() / m_rectsPixmapWidth;
const qreal ky = boundingRect().height() / m_rectsPixmapHeight;
painter->save();
painter->setClipRect(boundingRect());
// drawPixmap 源矩形不感知 DPR,需按设备像素换算
const qreal dpr = dApp->devicePixelRatio();
for (const QRectF &r : m_imageRects) {
// 外扩 1 逻辑像素吸收边缘抗锯齿带,与 NightFilter 蒙版膨胀同理
const QRectF target = QRectF(r.x() * kx - 1, r.y() * ky - 1,
r.width() * kx + 2, r.height() * ky + 2)
& boundingRect();
if (!target.isEmpty())
painter->drawPixmap(target, m_renderPixmap,
QRectF(target.x() * dpr, target.y() * dpr,
target.width() * dpr, target.height() * dpr));
}
painter->restore();
}
}
} else {
painter->drawPixmap(0, 0, m_nightPixmap);
}
painter->drawPixmap(0, 0, m_nightPixmap);
// 叠加轻微深色半透明层降低整体亮度,保持文字清晰可读
QColor dark = epMgr->pageBackgroundColor();
dark.setAlpha(60);
Expand Down Expand Up @@ -420,6 +467,11 @@

m_nightDirty = true; // 渲染结果已更新,夜间模式反色缓存失效

// 夜间模式下渲染完成即后台生成夜间图,不等 paint 触发,缩短占位层显示时间
if (EyeProtectionManager::instance()->mode() == EyeProtectionManager::Night
&& !m_nightJobRunning)
startNightJob();

update();
// qCDebug(appLog) << "BrowserPage::handleRenderFinished() - Handle render finished completed";
}
Expand Down Expand Up @@ -1280,44 +1332,83 @@
return isBig;
}

QPixmap BrowserPage::applyNightMode(const QPixmap &src)
void BrowserPage::setImageObjectRects(const QVector<QRectF> &rects, int pixmapWidth, int pixmapHeight)
{
m_imageRects = rects;
m_rectsPixmapWidth = pixmapWidth;
m_rectsPixmapHeight = pixmapHeight;
m_rectsFetched = true;
}

void BrowserPage::startNightJob()
{
if (m_renderPixmap.isNull())
return;

// QPixmap -> QImage 转换必须在 GUI 线程完成,副本交给工作线程逐像素处理;
// 失败(如超大页内存不足)直接返回,不消费脏标记,后续渲染/paint 会重试
QImage src = m_renderPixmap.toImage();
if (src.format() != QImage::Format_ARGB32 && src.format() != QImage::Format_RGB32)
src = src.convertToFormat(QImage::Format_ARGB32);
if (src.isNull())
return src;

QImage img = src.toImage();
if (img.isNull())
return src;

// 统一转非预乘 ARGB32,避免对 ARGB32_Premultiplied 写入非预乘值导致半透明像素错误
if (img.format() != QImage::Format_ARGB32)
img = img.convertToFormat(QImage::Format_ARGB32);

const int w = img.width();
const int h = img.height();

for (int y = 0; y < h; ++y) {
QRgb *line = reinterpret_cast<QRgb *>(img.scanLine(y));
for (int x = 0; x < w; ++x) {
QRgb px = line[x];
const int alpha = qAlpha(px);

// HSL 亮度反转:保留色相(H)和饱和度(S),仅反转亮度(L)
// 白(255) → 黑,黑(0) → 白(255),彩色仅变暗、色相不偏移
// 两端收敛:下限钳制 30(#1E1E1E),反转后 ≥192 提亮纯白(原图 L≤63 深色文字)
const int kMinLightAfterInvert = 30; // #1E1E1E 的 HSL 亮度值
const int kMaxLightBoostThreshold = 192; // 0xC0,提亮阈值
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);
}
return;

// 消费脏标记:本任务对应"当前" m_renderPixmap;任务在途期间若有新渲染
// 结果写入(handleRenderFinished),会重新置脏,届时过期结果将被丢弃
m_nightDirty = false;
m_nightJobRunning = true;

// 蒙版 bbox 通常由渲染线程随整页渲染预取并带回;未命中时(如打开文档后
// 才切入夜间模式的存量页)在 UI 线程同步补取一次,页对象列表有缓存,代价低
QVector<QRectF> rects = m_imageRects;
if (!m_rectsFetched && m_sheet && m_sheet->renderer() && m_sheet->renderer()->opened()) {
rects = m_sheet->renderer()->getImageObjectRects(itemIndex(), src.width(), src.height());
m_rectsPixmapWidth = src.width();
m_rectsPixmapHeight = src.height();
}

// rects 是预取时渲染尺寸下的物理像素坐标;缩放/旋转后当前渲染尺寸不同,
// 必须等比换算到新尺寸,否则蒙版错位——实际照片区被反相,旧位置留下
// 原色残影(缩放残影的根因)
if (!rects.isEmpty() && m_rectsPixmapWidth > 0 && m_rectsPixmapHeight > 0
&& (m_rectsPixmapWidth != src.width() || m_rectsPixmapHeight != src.height())) {
const qreal kx = qreal(src.width()) / m_rectsPixmapWidth;
const qreal ky = qreal(src.height()) / m_rectsPixmapHeight;
for (QRectF &r : rects)
r = QRectF(r.x() * kx, r.y() * ky, r.width() * kx, r.height() * ky);
}

EyeProtectionManager *epMgr = EyeProtectionManager::instance();

NightFilter::Options opt;
opt.imagePolicy = static_cast<NightFilter::ImagePolicy>(epMgr->nightImagePolicy());
opt.imageDimFactor = epMgr->nightImageDimFactor();

if (!m_nightWatcher) {
m_nightWatcher = new QFutureWatcher<QImage>(); // 无 parent:BrowserPage 非 QObject,在析构中手动删除
// 以 watcher 自身为接收者:BrowserPage 析构时 delete watcher 即断开连接,
// 后台任务仍在跑但结果被丢弃,不会回测已释放的 this
QObject::connect(m_nightWatcher, &QFutureWatcher<QImage>::finished,
m_nightWatcher, [this]() { onNightImageReady(); });
}

return QPixmap::fromImage(img);
m_nightWatcher->setFuture(QtConcurrent::run([src, rects, opt]() {
// 纯函数 + 私有副本,无锁无线程安全风险;含蒙版构建/扫描页特判
return NightFilter::applyPage(src, rects, opt);
}));
}

void BrowserPage::onNightImageReady()
{
m_nightJobRunning = false;

if (m_nightDirty) {
// 任务运行期间渲染结果又更新了:丢弃过期结果,重绘将重新发起
update();
return;
}

m_nightPixmap = QPixmap::fromImage(m_nightWatcher->result());
m_nightPixmap.setDevicePixelRatio(dApp->devicePixelRatio());
update();
}
31 changes: 24 additions & 7 deletions reader/browser/BrowserPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@

#include <QGraphicsItem>
#include <QSet>
#include <QMutex>

Check warning on line 14 in reader/browser/BrowserPage.h

View workflow job for this annotation

GitHub Actions / cppcheck

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

Check warning on line 15 in reader/browser/BrowserPage.h

View workflow job for this annotation

GitHub Actions / cppcheck

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

Check warning on line 16 in reader/browser/BrowserPage.h

View workflow job for this annotation

GitHub Actions / cppcheck

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

Check warning on line 17 in reader/browser/BrowserPage.h

View workflow job for this annotation

GitHub Actions / cppcheck

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

using namespace deepin_reader;

Expand Down Expand Up @@ -389,14 +390,23 @@
bool isBigDoc();

/**
* @brief applyNightMode 夜间模式智能反色
* 基于 HSL 亮度反转:仅反转 Lightness 通道,保留 Hue 和 Saturation
* 效果:白底黑字 → 黑底白字(文字/背景正确反色)
* 彩色图片/链接 → 仅变暗,色相不发生 180° 偏移
* @param src 源 pixmap
* @return 反色后的 pixmap
* @brief setImageObjectRects 接收渲染线程预取的图片对象 bbox
* 随整页渲染任务带回(物理像素坐标),夜间滤镜用它构建对象蒙版,
* 避免 UI 线程访问 PDFium 文档锁
* @param rects bbox 列表(物理像素,与 pixmapWidth/Height 的渲染输出对齐)
* @param pixmapWidth/Height rects 对应的渲染输出尺寸;
* 后续缩放后渲染尺寸变化时,paint 过渡帧按比例换算 bbox
*/
QPixmap applyNightMode(const QPixmap &src);
void setImageObjectRects(const QVector<QRectF> &rects, int pixmapWidth, int pixmapHeight);

/**
* @brief startNightJob 在后台线程生成夜间图(QtConcurrent)
* paint 不再同步执行逐像素滤镜,杜绝卡顿;期间绘制旧图/占位层
*/
void startNightJob();

/** 后台夜间滤镜完成回调:回填 m_nightPixmap 并重绘 */
void onNightImageReady();

protected:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
Expand Down Expand Up @@ -448,6 +458,13 @@

QPixmap m_nightPixmap; // 夜间模式智能反色缓存
bool m_nightDirty = true; // 缓存失效标记,m_renderPixmap 变化时置 true

QVector<QRectF> m_imageRects; // 图片对象 bbox(物理像素,渲染线程预取)
int m_rectsPixmapWidth = 0; // m_imageRects 对应的渲染输出宽(物理像素,过渡帧换算用)
int m_rectsPixmapHeight = 0; // m_imageRects 对应的渲染输出高
bool m_rectsFetched = false; // m_imageRects 是否已随整页渲染更新
bool m_nightJobRunning = false; // 后台夜间滤镜任务在途标记
QFutureWatcher<QImage> *m_nightWatcher = nullptr; // 异步任务完成通知
};

#endif // BrowserPage_H
Loading
Loading