#include "avatarcrop.h" #include "avatarcache.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include "debug_config.h" } namespace { class CropView : public QWidget { public: QImage image; QSize originalSize; explicit CropView(QWidget* parent) : QWidget(parent) { setMinimumSize(320, 320); setCursor(Qt::OpenHandCursor); } QRectF frame() const { const double side = qMin(width(), height()) * 0.8; return {(width()-side)/2, (height()-side)/2, side, side}; } double scale() const { return qMax(frame().width()/originalSize.width(), frame().height()/originalSize.height()) * zoom; } void clamp() { if (image.isNull()) return; const double s = scale(), mx = (originalSize.width()*s-frame().width())/2, my = (originalSize.height()*s-frame().height())/2; offset = {qBound(-mx, offset.x(), mx), qBound(-my, offset.y(), my)}; } QRectF selection() const { const double s = scale(); const QPointF top = QPointF(width()/2.0,height()/2.0) + offset - QPointF(originalSize.width()*s/2, originalSize.height()*s/2); const QRectF f = frame(); return {(f.x()-top.x())/s/originalSize.width(), (f.y()-top.y())/s/originalSize.height(), f.width()/s/originalSize.width(), f.height()/s/originalSize.height()}; } protected: void paintEvent(QPaintEvent*) override { QPainter p(this); p.fillRect(rect(), QColor(20,20,22)); if (image.isNull()) return; clamp(); const double s = scale(); const QSizeF size(originalSize.width()*s, originalSize.height()*s); p.setRenderHint(QPainter::SmoothPixmapTransform); p.setRenderHint(QPainter::Antialiasing); p.drawImage(QRectF(QPointF(width()/2.0,height()/2.0)+offset-QPointF(size.width()/2,size.height()/2), size), image); QPainterPath mask; mask.addRect(rect()); mask.addEllipse(frame()); p.fillPath(mask, QColor(0,0,0,150)); p.setPen(QPen(Qt::white, 2)); p.drawEllipse(frame()); } void wheelEvent(QWheelEvent* e) override { if (image.isNull()) return; const double previous = zoom; zoom = qBound(1.0, zoom * (e->angleDelta().y()>0 ? 1.15 : 1/1.15), 16.0); offset *= zoom/previous; clamp(); update(); e->accept(); } void mousePressEvent(QMouseEvent* e) override { if (e->button()==Qt::LeftButton) { last=e->pos(); dragging=true; } } void mouseMoveEvent(QMouseEvent* e) override { if (dragging) { offset+=e->pos()-last; last=e->pos(); clamp(); update(); } } void mouseReleaseEvent(QMouseEvent*) override { dragging=false; } private: double zoom = 1; QPointF offset; QPoint last; bool dragging = false; }; } bool selectAvatarCrop(const QString& source, QRectF& crop, QWidget* parent) { QDialog dialog(parent); dialog.setWindowTitle(QCoreApplication::translate("ChatGui", "Crop avatar")); dialog.resize(620,650); auto* layout = new QVBoxLayout(&dialog); auto* view = new CropView(&dialog); layout->addWidget(view, 1); view->setObjectName("avatarCropView"); auto* status = new QLabel(QCoreApplication::translate("ChatGui", "Drag photo and use the wheel to fit the circle"), &dialog); layout->addWidget(status); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); buttons->button(QDialogButtonBox::Ok)->setText(QCoreApplication::translate("ChatGui", "Upload avatar")); buttons->button(QDialogButtonBox::Ok)->setObjectName("avatarCropUpload"); buttons->button(QDialogButtonBox::Ok)->setEnabled(false); layout->addWidget(buttons); QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); QThreadPool pool; pool.setMaxThreadCount(1); pool.start(QRunnable::create([source, &dialog, view, status, buttons] { QImageReader reader(source); reader.setAutoTransform(true); const QSize size = reader.size(); QImage image; QSize orientedSize=size; if (reader.transformation() & QImageIOHandler::TransformationRotate90) orientedSize.transpose(); if (QFileInfo(source).size() <= AvatarCache::OriginalMaxBytes && size.isValid() && qint64(size.width())*size.height() <= 16*1024*1024 && (reader.format()=="jpeg" || reader.format()=="png" || reader.format()=="webp")) { reader.setScaledSize(size.scaled(1600,1600,Qt::KeepAspectRatio)); image = reader.read(); } QMetaObject::invokeMethod(&dialog, [view, image, orientedSize, status, buttons] { if (image.isNull()) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "avatar: editor cannot decode selected source"); status->setText(QCoreApplication::translate("ChatGui", "Cannot load avatar image")); return; } view->image=image; view->originalSize=orientedSize; view->update(); buttons->button(QDialogButtonBox::Ok)->setEnabled(true); DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "avatar: editor loaded original=%dx%d preview=%dx%d mask=circle", orientedSize.width(), orientedSize.height(), image.width(), image.height()); }, Qt::QueuedConnection); })); const int result=dialog.exec(); pool.waitForDone(); if (result!=QDialog::Accepted) { DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "avatar: crop cancelled"); return false; } crop=view->selection(); DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "avatar: crop selected x=%.5f y=%.5f w=%.5f h=%.5f", crop.x(), crop.y(), crop.width(), crop.height()); return true; }