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
+271
View File
@@ -0,0 +1,271 @@
// 必须在任何 Qt 头(会定义宏 signals)之前包含 vips/glib
#ifdef LT_HAVE_VIPS
# include <vips/vips.h>
#endif
#include "large_image/VipsBackend.h"
#include <QByteArray>
#include <QFile>
#include <QFileInfo>
#include <algorithm>
#include <cmath>
#include <cstdlib>
namespace core {
#ifdef LT_HAVE_VIPS
namespace {
bool gReady = false;
QByteArray encodedPath(const QString& path) {
return QFile::encodeName(path);
}
bool saveVipsImageToPngFile(VipsImage* img, const QString& destPath) {
if (!img) {
return false;
}
const QByteArray dest = encodedPath(destPath);
if (vips_pngsave(img, dest.constData(), "compression", 3, nullptr) != 0) {
return false;
}
return true;
}
} // namespace
bool VipsBackend::init(const char* argv0) {
if (gReady) {
return true;
}
if (vips_init(argv0) != 0) {
return false;
}
gReady = true;
return true;
}
void VipsBackend::shutdown() {
if (!gReady) {
return;
}
vips_shutdown();
gReady = false;
}
bool VipsBackend::isAvailable() {
return gReady;
}
bool VipsBackend::probeSize(const QString& path, QSize* out) {
if (!gReady || !out) {
return false;
}
const QByteArray enc = encodedPath(path);
if (enc.isEmpty()) {
return false;
}
VipsImage* in = vips_image_new_from_file(enc.constData(), "access", VIPS_ACCESS_SEQUENTIAL, nullptr);
if (!in) {
return false;
}
const int w = vips_image_get_width(in);
const int h = vips_image_get_height(in);
g_object_unref(in);
if (w <= 0 || h <= 0) {
return false;
}
*out = QSize(w, h);
return true;
}
bool VipsBackend::loadThumbnailQImage(const QString& path, qint64 maxPixels, QImage* out) {
if (!gReady || !out || maxPixels < 1024) {
return false;
}
const QByteArray enc = encodedPath(path);
if (enc.isEmpty()) {
return false;
}
QSize sz;
if (!probeSize(path, &sz)) {
return false;
}
const qint64 pixels = qint64(sz.width()) * qint64(sz.height());
if (pixels <= 0) {
return false;
}
VipsImage* thumb = nullptr;
if (pixels <= maxPixels) {
thumb = vips_image_new_from_file(enc.constData(), "access", VIPS_ACCESS_SEQUENTIAL, nullptr);
if (!thumb) {
return false;
}
} else {
const int targetW =
std::max(1, int(std::floor(std::sqrt(double(maxPixels) * double(sz.width()) / double(sz.height())))));
if (vips_thumbnail(enc.constData(), &thumb, targetW, nullptr) != 0) {
return false;
}
}
void* buf = nullptr;
size_t len = 0;
if (vips_pngsave_buffer(thumb, &buf, &len, "compression", 3, nullptr) != 0) {
g_object_unref(thumb);
return false;
}
g_object_unref(thumb);
const bool ok = out->loadFromData(static_cast<const uchar*>(buf), static_cast<int>(len), "PNG");
g_free(buf);
return ok && !out->isNull();
}
bool VipsBackend::loadRegionToQImage(const QString& sourcePath, const QRect& rectInSource, int maxOutputWidth,
int maxOutputHeight, QImage* out) {
if (!gReady || !out || maxOutputWidth < 32 || maxOutputHeight < 32) {
return false;
}
out->detach();
*out = QImage();
const QByteArray src = encodedPath(sourcePath);
if (src.isEmpty()) {
return false;
}
VipsImage* in = vips_image_new_from_file(src.constData(), "access", VIPS_ACCESS_RANDOM, nullptr);
if (!in) {
return false;
}
const int iw = vips_image_get_width(in);
const int ih = vips_image_get_height(in);
const QRect bounds(0, 0, iw, ih);
QRect r = rectInSource.normalized().intersected(bounds);
if (r.width() <= 0 || r.height() <= 0) {
g_object_unref(in);
return false;
}
VipsImage* cropped = nullptr;
if (vips_crop(in, &cropped, r.left(), r.top(), r.width(), r.height(), nullptr) != 0) {
g_object_unref(in);
return false;
}
g_object_unref(in);
const int cw = r.width();
const int ch = r.height();
VipsImage* toEncode = cropped;
// 视口解码:输出尺寸不应超过 maxOutput*,否则后续绘制阶段再缩小会引入明显的线性滤波“发糊”。
// 这里始终把区域降采样到不超过 maxOutput*(保持宽高比),让 vips 在解码阶段完成高质量缩放。
double scale = 1.0;
if (cw > maxOutputWidth || ch > maxOutputHeight) {
const double sx = static_cast<double>(maxOutputWidth) / static_cast<double>(cw);
const double sy = static_cast<double>(maxOutputHeight) / static_cast<double>(ch);
scale = std::min({sx, sy, 1.0});
}
if (scale < 0.9999) {
VipsImage* resized = nullptr;
if (vips_resize(cropped, &resized, scale, "kernel", VIPS_KERNEL_LANCZOS3, nullptr) != 0) {
g_object_unref(cropped);
return false;
}
g_object_unref(cropped);
toEncode = resized;
}
void* buf = nullptr;
size_t len = 0;
if (vips_pngsave_buffer(toEncode, &buf, &len, "compression", 3, nullptr) != 0) {
g_object_unref(toEncode);
return false;
}
g_object_unref(toEncode);
const bool ok = out->loadFromData(static_cast<const uchar*>(buf), static_cast<int>(len), "PNG");
g_free(buf);
return ok && !out->isNull();
}
bool VipsBackend::saveRectToPng(const QString& sourcePath, const QRect& rectInSource, const QString& destPngPath) {
if (!gReady) {
return false;
}
const QByteArray src = encodedPath(sourcePath);
if (src.isEmpty()) {
return false;
}
VipsImage* in = vips_image_new_from_file(src.constData(), "access", VIPS_ACCESS_RANDOM, nullptr);
if (!in) {
return false;
}
const int iw = vips_image_get_width(in);
const int ih = vips_image_get_height(in);
const QRect bounds(0, 0, iw, ih);
QRect r = rectInSource.normalized().intersected(bounds);
if (r.width() <= 0 || r.height() <= 0) {
g_object_unref(in);
return false;
}
VipsImage* toSave = in;
VipsImage* cropped = nullptr;
if (r != bounds) {
if (vips_crop(in, &cropped, r.left(), r.top(), r.width(), r.height(), nullptr) != 0) {
g_object_unref(in);
return false;
}
toSave = cropped;
}
const bool ok = saveVipsImageToPngFile(toSave, destPngPath);
if (cropped) {
g_object_unref(cropped);
}
g_object_unref(in);
return ok;
}
#else // !LT_HAVE_VIPS
bool VipsBackend::init(const char*) {
return false;
}
void VipsBackend::shutdown() {}
bool VipsBackend::isAvailable() {
return false;
}
bool VipsBackend::probeSize(const QString&, QSize*) {
return false;
}
bool VipsBackend::loadThumbnailQImage(const QString&, qint64, QImage*) {
return false;
}
bool VipsBackend::saveRectToPng(const QString&, const QRect&, const QString&) {
return false;
}
bool VipsBackend::loadRegionToQImage(const QString&, const QRect&, int, int, QImage*) {
return false;
}
#endif // LT_HAVE_VIPS
} // namespace core
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <QtGlobal>
#include <QImage>
#include <QRect>
#include <QString>
namespace core {
/// 基于 libvips 的大图 I/O:随机访问裁剪、流式写 PNG、缩略预览,避免整图进内存。
/// 未编译 libvips 时 API 返回 false,由调用方回退到 Qt。
class VipsBackend {
public:
/// 须在 QApplication 创建后调用一次,argv0 建议为 argv[0]。
static bool init(const char* argv0);
static void shutdown();
static bool isAvailable();
/// 打开文件读取宽高(轻量,不解码全图)
static bool probeSize(const QString& path, QSize* out);
/// 生成预览图(总像素约不超过 maxPixels),用于裁剪对话框 / 画布
static bool loadThumbnailQImage(const QString& path, qint64 maxPixels, QImage* out);
/// 将原图中矩形区域(像素坐标,含边界)无损写入 PNG;区域为整图时直接流式转存。
/// 适合 GB 级 PNG,不构建整幅 QImage。
static bool saveRectToPng(const QString& sourcePath, const QRect& rectInSource, const QString& destPngPath);
/// 裁剪原图矩形区域;若超过 maxOutput* 则按比例缩小(不降采样整图,只处理该区域)。
static bool loadRegionToQImage(const QString& sourcePath, const QRect& rectInSource, int maxOutputWidth,
int maxOutputHeight, QImage* out);
private:
VipsBackend() = default;
};
} // namespace core