This commit is contained in:
2026-05-14 13:30:06 +08:00
parent 974946cee4
commit e43171521d
91 changed files with 10485 additions and 3254 deletions
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include <QtGlobal>
#include <QImageReader>
#include <QRect>
#include <QSize>
#include <algorithm>
#include <cmath>
namespace core {
namespace image_decode {
/// 提高 Qt6 解码像素上限,避免大图被 reader 直接拒绝(单位 MB)
inline void prepareLargeImageReader() {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QImageReader::setAllocationLimit(4096);
#endif
}
/// 裁剪对话框等「仅预览」场景:像素上限(libvips 缩略与 Qt 回退共用)
[[maybe_unused]] constexpr qint64 kPreviewMaxPixels = 16LL * 1000 * 1000;
/// 编辑器 / 帧图替换:缩略像素上限(libvips 与 Qt 回退共用)
[[maybe_unused]] constexpr qint64 kWorkspaceMaxPixels = 48LL * 1000 * 1000;
/// 在 read() 前调用:若像素数超过 maxPixels,则 setScaledSize 按比例缩小(不解码全图)
inline void downscaleReaderIfExceeds(QImageReader& reader, qint64 maxPixels) {
const QSize sz = reader.size();
if (!sz.isValid() || sz.width() <= 0 || sz.height() <= 0) {
return;
}
const qint64 pixels = qint64(sz.width()) * qint64(sz.height());
if (pixels <= maxPixels) {
return;
}
const double s = std::sqrt(double(maxPixels) / double(pixels));
const int nw = std::max(1, int(std::lround(sz.width() * s)));
const int nh = std::max(1, int(std::lround(sz.height() * s)));
reader.setScaledSize(QSize(nw, nh));
}
/// 将 rect 从 fromSize 坐标系线性映射到 toSize(例如预览图上的选区 → 原图逻辑像素)
inline QRect mapRectBetweenSizes(const QRect& rect, const QSize& fromSize, const QSize& toSize) {
if (!rect.isValid() || fromSize.width() <= 0 || fromSize.height() <= 0 || toSize.width() <= 0 ||
toSize.height() <= 0) {
return {};
}
const qreal sx = qreal(toSize.width()) / qreal(fromSize.width());
const qreal sy = qreal(toSize.height()) / qreal(fromSize.height());
const int x0 = int(std::floor(qreal(rect.left()) * sx));
const int y0 = int(std::floor(qreal(rect.top()) * sy));
const int x1 = int(std::ceil(qreal(rect.right() + 1) * sx));
const int y1 = int(std::ceil(qreal(rect.bottom() + 1) * sy));
QRect out(x0, y0, x1 - x0, y1 - y0);
return out.normalized().intersected(QRect(0, 0, toSize.width(), toSize.height()));
}
} // namespace image_decode
} // namespace core
+99
View File
@@ -0,0 +1,99 @@
#include "image/ImageFileLoader.h"
#include "image/ImageDecodeConfig.h"
#include "large_image/VipsBackend.h"
#include <QImageReader>
namespace core {
namespace image_file {
bool probeImagePixelSize(const QString& absolutePath, QSize* outSize) {
if (!outSize || absolutePath.isEmpty()) {
return false;
}
if (VipsBackend::isAvailable() && VipsBackend::probeSize(absolutePath, outSize)) {
return outSize->isValid() && outSize->width() > 0 && outSize->height() > 0;
}
core::image_decode::prepareLargeImageReader();
QImageReader reader(absolutePath);
reader.setAutoTransform(true);
*outSize = reader.size();
return outSize->isValid() && outSize->width() > 0 && outSize->height() > 0;
}
QImage loadImageLimited(const QString& absolutePath, qint64 maxPixelBudget) {
if (absolutePath.isEmpty()) {
return {};
}
QImage viaVips;
if (VipsBackend::isAvailable() &&
VipsBackend::loadThumbnailQImage(absolutePath, maxPixelBudget, &viaVips) && !viaVips.isNull()) {
return viaVips;
}
core::image_decode::prepareLargeImageReader();
QImageReader reader(absolutePath);
reader.setAutoTransform(true);
core::image_decode::downscaleReaderIfExceeds(reader, maxPixelBudget);
return reader.read();
}
bool loadRegionToQImage(const QString& absolutePath, const QRect& rectLogical, int maxOutputWidth, int maxOutputHeight,
QImage* out) {
if (!out || absolutePath.isEmpty() || maxOutputWidth < 32 || maxOutputHeight < 32) {
return false;
}
out->detach();
*out = QImage();
if (VipsBackend::isAvailable() &&
VipsBackend::loadRegionToQImage(absolutePath, rectLogical, maxOutputWidth, maxOutputHeight, out) &&
!out->isNull()) {
return true;
}
QSize logical;
if (!probeImagePixelSize(absolutePath, &logical)) {
return false;
}
const QRect r = rectLogical.intersected(QRect(0, 0, logical.width(), logical.height()));
if (r.isEmpty()) {
return false;
}
QImage full = loadImageLimited(absolutePath, core::image_decode::kWorkspaceMaxPixels);
if (full.isNull()) {
return false;
}
const QRect r2 = core::image_decode::mapRectBetweenSizes(r, logical, full.size());
QImage piece = full.copy(r2);
if (piece.isNull()) {
return false;
}
// 与 vips 路径保持一致:输出永不超过 maxOutput*,避免后续绘制阶段二次缩放导致明显“发糊”。
if (piece.width() > maxOutputWidth || piece.height() > maxOutputHeight) {
piece = piece.scaled(QSize(maxOutputWidth, maxOutputHeight), Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
*out = piece;
return !out->isNull();
}
bool loadRegionToQImageExact(const QString& absolutePath, const QRect& rectLogical, QImage* out) {
if (!out || absolutePath.isEmpty()) {
return false;
}
out->detach();
*out = QImage();
QSize logical;
if (!probeImagePixelSize(absolutePath, &logical)) {
return false;
}
const QRect r = rectLogical.intersected(QRect(0, 0, logical.width(), logical.height()));
if (r.isEmpty()) {
return false;
}
// 1:1 输出:maxOutput 设为 region 本身大小,避免 vips/Qt 管线自动缩放导致坐标不一致。
const int w = std::max(1, r.width());
const int h = std::max(1, r.height());
return loadRegionToQImage(absolutePath, r, w, h, out);
}
} // namespace image_file
} // namespace core
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <QImage>
#include <QRect>
#include <QString>
#include <QSize>
#include <QtGlobal>
namespace core {
namespace image_file {
/// 仅读元数据(宽高),不解码像素;优先 libvips,否则 Qt QImageReader::size。
[[nodiscard]] bool probeImagePixelSize(const QString& absolutePath, QSize* outSize);
/// 统一大图解码管线:libvips(可用)按像素预算缩小后再解码为 QImage,否则 Qt。
[[nodiscard]] QImage loadImageLimited(const QString& absolutePath, qint64 maxPixelBudget);
/// 仅解码源图中矩形区域(逻辑像素坐标);超过 maxOutput* 则缩放该区域。
/// libvips 可用时走随机访问裁剪;否则整图降采样回退后再拷贝矩形。
[[nodiscard]] bool loadRegionToQImage(const QString& absolutePath, const QRect& rectLogical, int maxOutputWidth,
int maxOutputHeight, QImage* out);
/// 仅解码源图中矩形区域(逻辑像素坐标),并保证输出为 1:1 像素(不缩放)。
/// 用于抠图/像素对齐等需要严格坐标一致的场景。
[[nodiscard]] bool loadRegionToQImageExact(const QString& absolutePath, const QRect& rectLogical, QImage* out);
} // namespace image_file
} // namespace core