567 lines
18 KiB
C++
567 lines
18 KiB
C++
#include "dialogs/ImageCropDialog.h"
|
|
|
|
#include "core/image/ImageDecodeConfig.h"
|
|
#include "core/image/ImageFileLoader.h"
|
|
|
|
#include <QBoxLayout>
|
|
#include <QCursor>
|
|
#include <QDialogButtonBox>
|
|
#include <QLabel>
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
#include <QPushButton>
|
|
#include <QResizeEvent>
|
|
#include <QWheelEvent>
|
|
#include <QtMath>
|
|
|
|
#include <algorithm>
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
# define CROP_LOCAL_POS_M(E) (E)->position()
|
|
# define CROP_LOCAL_POS_W(E) (E)->position()
|
|
#else
|
|
# define CROP_LOCAL_POS_M(E) (E)->localPos()
|
|
# define CROP_LOCAL_POS_W(E) (E)->posF()
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
constexpr int kMinCropSidePx = 2;
|
|
/// 相对「适应窗口」倍率的乘子;范围极大以便实质任意缩放(仍避免数值溢出)
|
|
constexpr qreal kZoomMulMin = 1e-12;
|
|
constexpr qreal kZoomMulMax = 1e12;
|
|
constexpr qreal kHandleViewPx = 7.0;
|
|
|
|
enum class HitPart : quint8 {
|
|
None,
|
|
Inside,
|
|
N,
|
|
Ne,
|
|
E,
|
|
Se,
|
|
S,
|
|
Sw,
|
|
W,
|
|
Nw,
|
|
/// 平移视图(选区外左键拖动)
|
|
PanView
|
|
};
|
|
|
|
QRect clampRectToBounds(QRect r, const QRect& bounds) {
|
|
r = r.normalized();
|
|
if (r.width() < kMinCropSidePx || r.height() < kMinCropSidePx) {
|
|
return {};
|
|
}
|
|
if (r.left() < bounds.left()) {
|
|
r.moveLeft(bounds.left());
|
|
}
|
|
if (r.top() < bounds.top()) {
|
|
r.moveTop(bounds.top());
|
|
}
|
|
if (r.right() > bounds.right()) {
|
|
r.moveRight(bounds.right());
|
|
}
|
|
if (r.bottom() > bounds.bottom()) {
|
|
r.moveBottom(bounds.bottom());
|
|
}
|
|
if (r.width() < kMinCropSidePx || r.height() < kMinCropSidePx) {
|
|
return {};
|
|
}
|
|
return r;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class ImageCropDialog::CropView final : public QWidget {
|
|
public:
|
|
explicit CropView(QWidget* parent = nullptr)
|
|
: QWidget(parent) {
|
|
setMouseTracking(true);
|
|
setFocusPolicy(Qt::WheelFocus);
|
|
setMinimumSize(480, 320);
|
|
}
|
|
|
|
void setImage(const QImage& img) {
|
|
m_image = img;
|
|
m_zoom = 1.0;
|
|
m_pan = QPointF();
|
|
m_selImage = m_image.isNull() ? QRect() : QRect(QPoint(0, 0), m_image.size());
|
|
m_dragPart = HitPart::None;
|
|
updateGeometry();
|
|
update();
|
|
}
|
|
|
|
bool hasSelection() const {
|
|
return !m_image.isNull() && m_selImage.width() >= kMinCropSidePx && m_selImage.height() >= kMinCropSidePx;
|
|
}
|
|
|
|
QRect selectionInImagePixels() const {
|
|
if (m_image.isNull() || !hasSelection()) {
|
|
return {};
|
|
}
|
|
return m_selImage.normalized().intersected(QRect(0, 0, m_image.width(), m_image.height()));
|
|
}
|
|
|
|
void resetSelection() {
|
|
if (!m_image.isNull()) {
|
|
m_selImage = QRect(QPoint(0, 0), m_image.size());
|
|
} else {
|
|
m_selImage = {};
|
|
}
|
|
update();
|
|
}
|
|
|
|
protected:
|
|
void resizeEvent(QResizeEvent* e) override {
|
|
QWidget::resizeEvent(e);
|
|
update();
|
|
}
|
|
|
|
void paintEvent(QPaintEvent*) override {
|
|
QPainter p(this);
|
|
p.fillRect(rect(), palette().window());
|
|
|
|
if (m_image.isNull()) {
|
|
p.setPen(palette().text().color());
|
|
p.drawText(rect(), Qt::AlignCenter, QStringLiteral("无法加载图片"));
|
|
return;
|
|
}
|
|
|
|
qreal eff = 0;
|
|
QPointF tl;
|
|
computeLayout(eff, tl);
|
|
|
|
const QTransform imgToView = imageToViewTransform(eff, tl);
|
|
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
|
p.setTransform(imgToView);
|
|
p.drawImage(QPoint(0, 0), m_image);
|
|
p.resetTransform();
|
|
|
|
const QRectF selView = imgToView.mapRect(QRectF(m_selImage));
|
|
const QRect sel = selView.toAlignedRect().intersected(rect());
|
|
|
|
const QColor shade(0, 0, 0, 120);
|
|
p.fillRect(QRect(0, 0, width(), sel.top()), shade);
|
|
p.fillRect(QRect(0, sel.bottom(), width(), height() - sel.bottom()), shade);
|
|
p.fillRect(QRect(0, sel.top(), sel.left(), sel.height()), shade);
|
|
p.fillRect(QRect(sel.right(), sel.top(), width() - sel.right(), sel.height()), shade);
|
|
|
|
p.setPen(QPen(QColor(255, 255, 255, 230), 2));
|
|
p.setBrush(Qt::NoBrush);
|
|
p.drawRect(sel);
|
|
|
|
const qreal hsz = kHandleViewPx;
|
|
const QPointF c[4] = {
|
|
selView.topLeft(),
|
|
selView.topRight(),
|
|
selView.bottomRight(),
|
|
selView.bottomLeft(),
|
|
};
|
|
p.setPen(QPen(QColor(255, 255, 255, 240), 1));
|
|
p.setBrush(QColor(255, 255, 255, 220));
|
|
for (const QPointF& pt : c) {
|
|
p.drawRect(QRectF(pt.x() - hsz * 0.5, pt.y() - hsz * 0.5, hsz, hsz));
|
|
}
|
|
}
|
|
|
|
void wheelEvent(QWheelEvent* e) override {
|
|
if (m_image.isNull()) {
|
|
e->ignore();
|
|
return;
|
|
}
|
|
qreal effOld = 0;
|
|
QPointF tlOld;
|
|
computeLayout(effOld, tlOld);
|
|
|
|
const QPointF cursor = CROP_LOCAL_POS_W(e);
|
|
const QPointF imgPt((cursor.x() - tlOld.x()) / effOld, (cursor.y() - tlOld.y()) / effOld);
|
|
|
|
const qreal steps = e->angleDelta().y() / 120.0;
|
|
const qreal factor = std::pow(1.1, steps);
|
|
const qreal newZoom = std::clamp(m_zoom * factor, kZoomMulMin, kZoomMulMax);
|
|
if (qFuzzyCompare(newZoom, m_zoom)) {
|
|
e->accept();
|
|
return;
|
|
}
|
|
|
|
m_zoom = newZoom;
|
|
|
|
qreal effNew = 0;
|
|
QPointF tlIgnored;
|
|
computeLayout(effNew, tlIgnored);
|
|
|
|
const QPointF tlNew(cursor.x() - imgPt.x() * effNew, cursor.y() - imgPt.y() * effNew);
|
|
const qreal iw = m_image.width();
|
|
const qreal ih = m_image.height();
|
|
const qreal vw = width();
|
|
const qreal vh = height();
|
|
const QPointF centerOff((vw - iw * effNew) / 2.0, (vh - ih * effNew) / 2.0);
|
|
m_pan = tlNew - centerOff;
|
|
|
|
e->accept();
|
|
update();
|
|
}
|
|
|
|
void mousePressEvent(QMouseEvent* e) override {
|
|
if (m_image.isNull() || e->button() != Qt::LeftButton) {
|
|
return;
|
|
}
|
|
qreal eff = 0;
|
|
QPointF tl;
|
|
computeLayout(eff, tl);
|
|
const QPointF viewPos = CROP_LOCAL_POS_M(e);
|
|
const QPointF img = viewToImage(viewPos, eff, tl);
|
|
|
|
m_dragPart = hitTest(img, eff);
|
|
if (m_dragPart == HitPart::None) {
|
|
m_dragPart = HitPart::PanView;
|
|
}
|
|
if (m_dragPart == HitPart::PanView) {
|
|
m_pressView = viewPos;
|
|
m_panAtPress = m_pan;
|
|
setCursor(Qt::ClosedHandCursor);
|
|
return;
|
|
}
|
|
m_pressImg = img;
|
|
m_startSel = m_selImage;
|
|
updateCursorForHover(viewPos, eff, tl);
|
|
}
|
|
|
|
void mouseMoveEvent(QMouseEvent* e) override {
|
|
if (m_image.isNull()) {
|
|
return;
|
|
}
|
|
qreal eff = 0;
|
|
QPointF tl;
|
|
computeLayout(eff, tl);
|
|
const QPointF viewPos = CROP_LOCAL_POS_M(e);
|
|
|
|
if ((e->buttons() & Qt::LeftButton) && m_dragPart == HitPart::PanView) {
|
|
m_pan = m_panAtPress + (viewPos - m_pressView);
|
|
update();
|
|
return;
|
|
}
|
|
|
|
if ((e->buttons() & Qt::LeftButton) && m_dragPart != HitPart::None && m_dragPart != HitPart::PanView) {
|
|
const QPointF img = viewToImage(viewPos, eff, tl);
|
|
applyDrag(m_dragPart, m_pressImg, img, m_startSel);
|
|
update();
|
|
return;
|
|
}
|
|
|
|
updateCursorForHover(viewPos, eff, tl);
|
|
}
|
|
|
|
void mouseReleaseEvent(QMouseEvent* e) override {
|
|
if (e->button() == Qt::LeftButton) {
|
|
m_dragPart = HitPart::None;
|
|
unsetCursor();
|
|
}
|
|
QWidget::mouseReleaseEvent(e);
|
|
}
|
|
|
|
private:
|
|
void computeLayout(qreal& outEffScale, QPointF& outTopLeft) const {
|
|
const qreal vw = width();
|
|
const qreal vh = height();
|
|
const qreal iw = m_image.width();
|
|
const qreal ih = m_image.height();
|
|
if (iw <= 0 || ih <= 0 || vw <= 1 || vh <= 1) {
|
|
outEffScale = 1.0;
|
|
outTopLeft = QPointF();
|
|
return;
|
|
}
|
|
const qreal fit = std::min(vw / iw, vh / ih);
|
|
const qreal eff = fit * m_zoom;
|
|
outEffScale = eff;
|
|
outTopLeft = m_pan + QPointF((vw - iw * eff) / 2.0, (vh - ih * eff) / 2.0);
|
|
}
|
|
|
|
QTransform imageToViewTransform(qreal eff, const QPointF& tl) const {
|
|
QTransform t;
|
|
t.translate(tl.x(), tl.y());
|
|
t.scale(eff, eff);
|
|
return t;
|
|
}
|
|
|
|
QPointF viewToImage(const QPointF& viewPt, qreal eff, const QPointF& tl) const {
|
|
return QPointF((viewPt.x() - tl.x()) / eff, (viewPt.y() - tl.y()) / eff);
|
|
}
|
|
|
|
qreal marginImage(qreal eff) const {
|
|
return std::max<qreal>(kMinCropSidePx, kHandleViewPx / std::max(eff, 1e-6));
|
|
}
|
|
|
|
HitPart hitTest(const QPointF& img, qreal eff) const {
|
|
const qreal iw = m_image.width();
|
|
const qreal ih = m_image.height();
|
|
const QRectF boundsF(0, 0, iw, ih);
|
|
if (!boundsF.contains(img)) {
|
|
return HitPart::PanView;
|
|
}
|
|
const qreal m = marginImage(eff);
|
|
const QRect r = m_selImage.normalized();
|
|
|
|
const bool onN = std::abs(img.y() - r.top()) <= m && img.x() >= r.left() - m && img.x() <= r.right() + m;
|
|
const bool onS = std::abs(img.y() - r.bottom()) <= m && img.x() >= r.left() - m && img.x() <= r.right() + m;
|
|
const bool onW = std::abs(img.x() - r.left()) <= m && img.y() >= r.top() - m && img.y() <= r.bottom() + m;
|
|
const bool onE = std::abs(img.x() - r.right()) <= m && img.y() >= r.top() - m && img.y() <= r.bottom() + m;
|
|
|
|
const bool cnw = std::abs(img.x() - r.left()) <= m && std::abs(img.y() - r.top()) <= m;
|
|
const bool cne = std::abs(img.x() - r.right()) <= m && std::abs(img.y() - r.top()) <= m;
|
|
const bool cse = std::abs(img.x() - r.right()) <= m && std::abs(img.y() - r.bottom()) <= m;
|
|
const bool csw = std::abs(img.x() - r.left()) <= m && std::abs(img.y() - r.bottom()) <= m;
|
|
|
|
if (cnw) {
|
|
return HitPart::Nw;
|
|
}
|
|
if (cne) {
|
|
return HitPart::Ne;
|
|
}
|
|
if (cse) {
|
|
return HitPart::Se;
|
|
}
|
|
if (csw) {
|
|
return HitPart::Sw;
|
|
}
|
|
if (onN && !onW && !onE) {
|
|
return HitPart::N;
|
|
}
|
|
if (onS && !onW && !onE) {
|
|
return HitPart::S;
|
|
}
|
|
if (onW && !onN && !onS) {
|
|
return HitPart::W;
|
|
}
|
|
if (onE && !onN && !onS) {
|
|
return HitPart::E;
|
|
}
|
|
const QRect fullImage(0, 0, m_image.width(), m_image.height());
|
|
if (r.contains(img.toPoint())) {
|
|
// 选区为整图时没有「可平移的选区」,框内左键用于平移视图
|
|
if (r.normalized() == fullImage.normalized()) {
|
|
return HitPart::PanView;
|
|
}
|
|
return HitPart::Inside;
|
|
}
|
|
return HitPart::PanView;
|
|
}
|
|
|
|
void updateCursorForHover(const QPointF& viewPt, qreal eff, const QPointF& tl) {
|
|
if (m_dragPart != HitPart::None) {
|
|
return;
|
|
}
|
|
const QPointF img = viewToImage(viewPt, eff, tl);
|
|
const HitPart h = hitTest(img, eff);
|
|
switch (h) {
|
|
case HitPart::PanView:
|
|
setCursor(Qt::OpenHandCursor);
|
|
break;
|
|
case HitPart::Inside:
|
|
setCursor(Qt::SizeAllCursor);
|
|
break;
|
|
case HitPart::N:
|
|
setCursor(Qt::SizeVerCursor);
|
|
break;
|
|
case HitPart::S:
|
|
setCursor(Qt::SizeVerCursor);
|
|
break;
|
|
case HitPart::W:
|
|
setCursor(Qt::SizeHorCursor);
|
|
break;
|
|
case HitPart::E:
|
|
setCursor(Qt::SizeHorCursor);
|
|
break;
|
|
case HitPart::Nw:
|
|
case HitPart::Se:
|
|
setCursor(Qt::SizeFDiagCursor);
|
|
break;
|
|
case HitPart::Ne:
|
|
case HitPart::Sw:
|
|
setCursor(Qt::SizeBDiagCursor);
|
|
break;
|
|
default:
|
|
unsetCursor();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void applyDrag(HitPart part, const QPointF& pressImg, const QPointF& curImg, const QRect& start) {
|
|
const QRect bounds(0, 0, m_image.width(), m_image.height());
|
|
QRect r = start.normalized();
|
|
const int L = r.left();
|
|
const int T = r.top();
|
|
const int R = r.right();
|
|
const int B = r.bottom();
|
|
|
|
auto clampX = [&](int x) { return std::clamp(x, bounds.left(), bounds.right()); };
|
|
auto clampY = [&](int y) { return std::clamp(y, bounds.top(), bounds.bottom()); };
|
|
|
|
switch (part) {
|
|
case HitPart::Inside: {
|
|
const QPoint delta = (curImg - pressImg).toPoint();
|
|
r.translate(delta);
|
|
r = clampRectToBounds(r, bounds);
|
|
break;
|
|
}
|
|
case HitPart::N: {
|
|
int nt = clampY(int(std::round(curImg.y())));
|
|
nt = std::min(nt, B - kMinCropSidePx);
|
|
r = QRect(L, nt, R - L + 1, B - nt + 1);
|
|
break;
|
|
}
|
|
case HitPart::S: {
|
|
int nb = clampY(int(std::round(curImg.y())));
|
|
nb = std::max(nb, T + kMinCropSidePx);
|
|
r = QRect(L, T, R - L + 1, nb - T + 1);
|
|
break;
|
|
}
|
|
case HitPart::W: {
|
|
int nl = clampX(int(std::round(curImg.x())));
|
|
nl = std::min(nl, R - kMinCropSidePx);
|
|
r = QRect(nl, T, R - nl + 1, B - T + 1);
|
|
break;
|
|
}
|
|
case HitPart::E: {
|
|
int nr = clampX(int(std::round(curImg.x())));
|
|
nr = std::max(nr, L + kMinCropSidePx);
|
|
r = QRect(L, T, nr - L + 1, B - T + 1);
|
|
break;
|
|
}
|
|
case HitPart::Nw: {
|
|
int nl = clampX(int(std::round(curImg.x())));
|
|
int nt = clampY(int(std::round(curImg.y())));
|
|
nl = std::min(nl, R - kMinCropSidePx);
|
|
nt = std::min(nt, B - kMinCropSidePx);
|
|
r = QRect(QPoint(nl, nt), QPoint(R, B)).normalized();
|
|
break;
|
|
}
|
|
case HitPart::Ne: {
|
|
int nr = clampX(int(std::round(curImg.x())));
|
|
int nt = clampY(int(std::round(curImg.y())));
|
|
nr = std::max(nr, L + kMinCropSidePx);
|
|
nt = std::min(nt, B - kMinCropSidePx);
|
|
r = QRect(QPoint(L, nt), QPoint(nr, B)).normalized();
|
|
break;
|
|
}
|
|
case HitPart::Se: {
|
|
int nr = clampX(int(std::round(curImg.x())));
|
|
int nb = clampY(int(std::round(curImg.y())));
|
|
nr = std::max(nr, L + kMinCropSidePx);
|
|
nb = std::max(nb, T + kMinCropSidePx);
|
|
r = QRect(QPoint(L, T), QPoint(nr, nb)).normalized();
|
|
break;
|
|
}
|
|
case HitPart::Sw: {
|
|
int nl = clampX(int(std::round(curImg.x())));
|
|
int nb = clampY(int(std::round(curImg.y())));
|
|
nl = std::min(nl, R - kMinCropSidePx);
|
|
nb = std::max(nb, T + kMinCropSidePx);
|
|
r = QRect(QPoint(nl, T), QPoint(R, nb)).normalized();
|
|
break;
|
|
}
|
|
case HitPart::PanView:
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
m_selImage = clampRectToBounds(r, bounds);
|
|
if (m_selImage.isEmpty()) {
|
|
m_selImage = start;
|
|
}
|
|
}
|
|
|
|
private:
|
|
QImage m_image;
|
|
QRect m_selImage;
|
|
double m_zoom = 1.0;
|
|
QPointF m_pan;
|
|
|
|
HitPart m_dragPart = HitPart::None;
|
|
QPointF m_pressImg;
|
|
QRect m_startSel;
|
|
QPointF m_pressView;
|
|
QPointF m_panAtPress;
|
|
};
|
|
|
|
ImageCropDialog::ImageCropDialog(const QString& imagePath, QWidget* parent)
|
|
: QDialog(parent),
|
|
m_imagePath(imagePath) {
|
|
setWindowTitle(QStringLiteral("裁剪图片"));
|
|
setModal(true);
|
|
resize(900, 600);
|
|
loadImageOrClose();
|
|
rebuildUi();
|
|
}
|
|
|
|
void ImageCropDialog::loadImageOrClose() {
|
|
if (!core::image_file::probeImagePixelSize(m_imagePath, &m_sourceLogicalSize)) {
|
|
m_sourceLogicalSize = QSize();
|
|
}
|
|
m_image =
|
|
core::image_file::loadImageLimited(m_imagePath, core::image_decode::kPreviewMaxPixels);
|
|
if (m_image.isNull()) {
|
|
reject();
|
|
return;
|
|
}
|
|
if (!m_sourceLogicalSize.isValid() || m_sourceLogicalSize.width() <= 0 ||
|
|
m_sourceLogicalSize.height() <= 0) {
|
|
m_sourceLogicalSize = m_image.size();
|
|
}
|
|
}
|
|
|
|
void ImageCropDialog::rebuildUi() {
|
|
auto* root = new QVBoxLayout(this);
|
|
|
|
auto* hint = new QLabel(
|
|
QStringLiteral(
|
|
"默认已框选整张图。滚轮可连续缩放;拖拽边或角调整裁剪,框内拖拽移动选区;选区外或留白处左键拖动平移视图。「重置」恢复整图选区。"
|
|
" 若已链接 libvips,超大图用流式解码与预览;否则回退 Qt。确定裁剪后坐标按原图逻辑像素对齐。"),
|
|
this);
|
|
hint->setWordWrap(true);
|
|
root->addWidget(hint);
|
|
|
|
m_view = new CropView(this);
|
|
m_view->setImage(m_image);
|
|
root->addWidget(m_view, 1);
|
|
|
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
|
m_okButton = buttons->button(QDialogButtonBox::Ok);
|
|
auto* resetBtn = new QPushButton(QStringLiteral("重置"), this);
|
|
buttons->addButton(resetBtn, QDialogButtonBox::ActionRole);
|
|
|
|
connect(resetBtn, &QPushButton::clicked, this, &ImageCropDialog::onReset);
|
|
connect(buttons, &QDialogButtonBox::accepted, this, &ImageCropDialog::onOk);
|
|
connect(buttons, &QDialogButtonBox::rejected, this, &ImageCropDialog::reject);
|
|
root->addWidget(buttons);
|
|
}
|
|
|
|
bool ImageCropDialog::hasValidSelection() const {
|
|
return m_view && m_view->hasSelection();
|
|
}
|
|
|
|
QRect ImageCropDialog::selectedRectInImagePixels() const {
|
|
if (!m_view) {
|
|
return {};
|
|
}
|
|
const QRect inPreview = m_view->selectionInImagePixels();
|
|
if (!inPreview.isValid() || m_image.isNull()) {
|
|
return {};
|
|
}
|
|
if (m_sourceLogicalSize.isValid() && m_sourceLogicalSize != m_image.size()) {
|
|
return core::image_decode::mapRectBetweenSizes(inPreview, m_image.size(), m_sourceLogicalSize);
|
|
}
|
|
return inPreview;
|
|
}
|
|
|
|
void ImageCropDialog::onReset() {
|
|
if (m_view) {
|
|
m_view->resetSelection();
|
|
}
|
|
}
|
|
|
|
void ImageCropDialog::onOk() {
|
|
accept();
|
|
}
|