From e1ea833ad67be60f19cf83a204cd4a7468582c24 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 8 Feb 2021 10:01:15 +0300 Subject: [PATCH] Added ability to crop images in photo editor. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/editor/editor_crop.cpp | 300 ++++++++++++++++++ Telegram/SourceFiles/editor/editor_crop.h | 95 ++++++ Telegram/SourceFiles/editor/photo_editor.cpp | 1 + .../editor/photo_editor_common.cpp | 6 +- .../SourceFiles/editor/photo_editor_common.h | 3 +- .../editor/photo_editor_content.cpp | 33 +- .../SourceFiles/editor/photo_editor_content.h | 6 + 8 files changed, 438 insertions(+), 8 deletions(-) create mode 100644 Telegram/SourceFiles/editor/editor_crop.cpp create mode 100644 Telegram/SourceFiles/editor/editor_crop.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index c28db4566..83fac6e3d 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -508,6 +508,8 @@ PRIVATE dialogs/dialogs_search_from_controllers.h dialogs/dialogs_widget.cpp dialogs/dialogs_widget.h + editor/editor_crop.cpp + editor/editor_crop.h editor/photo_editor.cpp editor/photo_editor.h editor/photo_editor_common.cpp diff --git a/Telegram/SourceFiles/editor/editor_crop.cpp b/Telegram/SourceFiles/editor/editor_crop.cpp new file mode 100644 index 000000000..9dcf19432 --- /dev/null +++ b/Telegram/SourceFiles/editor/editor_crop.cpp @@ -0,0 +1,300 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "editor/editor_crop.h" + +#include "styles/style_boxes.h" + +namespace Editor { +namespace { + +constexpr auto kETL = Qt::TopEdge | Qt::LeftEdge; +constexpr auto kETR = Qt::TopEdge | Qt::RightEdge; +constexpr auto kEBL = Qt::BottomEdge | Qt::LeftEdge; +constexpr auto kEBR = Qt::BottomEdge | Qt::RightEdge; +constexpr auto kEAll = Qt::TopEdge + | Qt::LeftEdge + | Qt::BottomEdge + | Qt::RightEdge; + +std::tuple RectEdges(const QRect &r) { + return { r.left(), r.top(), r.left() + r.width(), r.top() + r.height() }; +} + +QPoint PointOfEdge(Qt::Edges e, const QRect &r) { + switch(e) { + case kETL: return QPoint(r.x(), r.y()); + case kETR: return QPoint(r.x() + r.width(), r.y()); + case kEBL: return QPoint(r.x(), r.y() + r.height()); + case kEBR: return QPoint(r.x() + r.width(), r.y() + r.height()); + default: return QPoint(); + } +} + +} // namespace + +Crop::Crop( + not_null parent, + const PhotoModifications &modifications, + const QSize &imageSize) +: RpWidget(parent) +, _pointSize(st::cropPointSize) +, _pointSizeH(_pointSize / 2.) +, _innerMargins(QMarginsF(_pointSizeH, _pointSizeH, _pointSizeH, _pointSizeH) + .toMargins()) +, _offset(_innerMargins.left(), _innerMargins.top()) +, _edgePointMargins(_pointSizeH, _pointSizeH, -_pointSizeH, -_pointSizeH) { + + _angle = modifications.angle; + _flipped = modifications.flipped; + _cropRect = modifications.crop; + if (_cropRect.isValid()) { + const auto inner = QRect(QPoint(), imageSize); + _innerRect = QRect( + QPoint(), + QMatrix().rotate(-_angle).mapRect(inner).size()); + } + + setMouseTracking(true); + + paintRequest( + ) | rpl::start_with_next([=] { + Painter p(this); + + p.fillPath(_painterPath, st::photoCropFadeBg); + + paintPoints(p); + + }, lifetime()); + +} + +void Crop::applyTransform(QRect geometry, int angle, bool flipped) { + if (geometry.isEmpty()) { + return; + } + setGeometry(geometry); + + const auto nowInner = QRect(QPoint(), geometry.size()) - _innerMargins; + const auto wasInner = _innerRect.isEmpty() ? nowInner : _innerRect; + const auto nowInnerF = QRectF(QPointF(), QSizeF(nowInner.size())); + const auto wasInnerF = QRectF(QPointF(), QSizeF(wasInner.size())); + + _innerRect = nowInner; + + if (_cropRect.isEmpty()) { + setCropRect(_innerRect.translated(-_offset)); + } + + const auto angleTo = (angle - _angle) * (flipped ? -1 : 1); + const auto flippedChanges = (_flipped != flipped); + + const auto nowInnerCenter = nowInnerF.center(); + const auto nowInnerRotated = QMatrix() + .translate(nowInnerCenter.x(), nowInnerCenter.y()) + .rotate(-angleTo) + .translate(-nowInnerCenter.x(), -nowInnerCenter.y()) + .mapRect(nowInnerF); + + const auto nowCropRect = resizedCropRect(wasInnerF, nowInnerRotated) + .translated(nowInnerRotated.topLeft()); + + const auto nowInnerRotatedCenter = nowInnerRotated.center(); + + setCropRect(QMatrix() + .translate(nowInnerRotatedCenter.x(), nowInnerRotatedCenter.y()) + .rotate(angleTo) + .scale(flippedChanges ? -1 : 1, 1) + .translate(-nowInnerRotatedCenter.x(), -nowInnerRotatedCenter.y()) + .mapRect(nowCropRect) + .toRect()); + + { + // Check boundaries. + const auto p = _cropRectPaint.center(); + computeDownState(p); + performMove(p); + clearDownState(); + } + + _flipped = flipped; + _angle = angle; +} + +void Crop::paintPoints(Painter &p) { + p.save(); + p.setPen(Qt::NoPen); + p.setBrush(st::photoCropPointFg); + for (const auto &r : ranges::views::values(_edges)) { + p.drawRect(r); + } + p.restore(); +} + +void Crop::setCropRect(QRect &&rect) { + _cropRect = std::move(rect); + _cropRectPaint = _cropRect.translated(_offset); + updateEdges(); + + _painterPath.clear(); + _painterPath.addRect(_innerRect); + _painterPath.addRect(_cropRectPaint); +} + +void Crop::setCropRectPaint(QRect &&rect) { + rect.translate(-_offset); + setCropRect(std::move(rect)); +} + +void Crop::updateEdges() { + const auto &s = _pointSize; + const auto &m = _edgePointMargins; + const auto &r = _cropRectPaint; + for (const auto &e : { kETL, kETR, kEBL, kEBR }) { + _edges[e] = QRectF(PointOfEdge(e, r), QSize(s, s)) + m; + } +} + +Qt::Edges Crop::mouseState(const QPoint &p) { + for (const auto &[e, r] : _edges) { + if (r.contains(p)) { + return e; + } + } + if (_cropRectPaint.contains(p)) { + return kEAll; + } + return Qt::Edges(); +} + +void Crop::mousePressEvent(QMouseEvent *e) { + computeDownState(e->pos()); +} + +void Crop::mouseReleaseEvent(QMouseEvent *e) { + clearDownState(); +} + +void Crop::computeDownState(const QPoint &p) { + const auto edge = mouseState(p); + const auto &inner = _innerRect; + const auto &crop = _cropRectPaint; + const auto [iLeft, iTop, iRight, iBottom] = RectEdges(inner); + const auto [cLeft, cTop, cRight, cBottom] = RectEdges(crop); + _down = InfoAtDown{ + .rect = crop, + .edge = edge, + .point = (p - PointOfEdge(edge, crop)), + .borders = InfoAtDown::Borders{ + .left = iLeft - cLeft, + .right = iRight - cRight, + .top = iTop - cTop, + .bottom = iBottom - cBottom, + } + }; +} + +void Crop::clearDownState() { + _down = InfoAtDown(); +} + +void Crop::performCrop(const QPoint &pos) { + const auto &crop = _down.rect; + const auto &pressedEdge = _down.edge; + const auto hasLeft = (pressedEdge & Qt::LeftEdge); + const auto hasTop = (pressedEdge & Qt::TopEdge); + const auto hasRight = (pressedEdge & Qt::RightEdge); + const auto hasBottom = (pressedEdge & Qt::BottomEdge); + const auto diff = [&] { + const auto diff = pos - PointOfEdge(pressedEdge, crop) - _down.point; + const auto hFactor = hasLeft ? 1 : -1; + const auto vFactor = hasTop ? 1 : -1; + const auto &borders = _down.borders; + + const auto hMin = hFactor * crop.width() - hFactor * st::cropMinSize; + const auto vMin = vFactor * crop.height() - vFactor * st::cropMinSize; + + const auto x = std::clamp( + diff.x(), + hasLeft ? borders.left : hMin, + hasLeft ? hMin : borders.right); + const auto y = std::clamp( + diff.y(), + hasTop ? borders.top : vMin, + hasTop ? vMin : borders.bottom); + if (_keepAspectRatio) { + const auto minDiff = std::min(std::abs(x), std::abs(y)); + return QPoint(minDiff * hFactor, minDiff * vFactor); + } + return QPoint(x, y); + }(); + setCropRectPaint(crop - QMargins( + hasLeft ? diff.x() : 0, + hasTop ? diff.y() : 0, + hasRight ? -diff.x() : 0, + hasBottom ? -diff.y() : 0)); +} + +void Crop::performMove(const QPoint &pos) { + const auto &inner = _down.rect; + const auto &b = _down.borders; + const auto diffX = std::clamp(pos.x() - _down.point.x(), b.left, b.right); + const auto diffY = std::clamp(pos.y() - _down.point.y(), b.top, b.bottom); + setCropRectPaint(inner.translated(diffX, diffY)); +} + +void Crop::mouseMoveEvent(QMouseEvent *e) { + const auto pos = e->pos(); + const auto pressedEdge = _down.edge; + + if (pressedEdge) { + if (pressedEdge == kEAll) { + performMove(pos); + } else if (pressedEdge) { + performCrop(pos); + } + update(); + } + + const auto edge = pressedEdge ? pressedEdge : mouseState(pos); + + const auto cursor = ((edge == kETL) || (edge == kEBR)) + ? style::cur_sizefdiag + : ((edge == kETR) || (edge == kEBL)) + ? style::cur_sizebdiag + : (edge == kEAll) + ? style::cur_sizeall + : style::cur_default; + setCursor(cursor); +} + +QRect Crop::innerRect() const { + return _innerRect; +} + +style::margins Crop::cropMargins() const { + return _innerMargins; +} + +QRect Crop::saveCropRect(const QRect &from, const QRect &to) { + return resizedCropRect(QRectF(from), QRectF(to)).toRect(); +} + +QRectF Crop::resizedCropRect(const QRectF &from, const QRectF &to) { + const auto ratioW = to.width() / float64(from.width()); + const auto ratioH = to.height() / float64(from.height()); + const auto &min = float64(st::cropMinSize); + const auto &r = _cropRect; + + return QRectF( + r.x() * ratioW, + r.y() * ratioH, + std::max(r.width() * ratioW, min), + std::max(r.height() * ratioH, min)); +} + +} // namespace Editor diff --git a/Telegram/SourceFiles/editor/editor_crop.h b/Telegram/SourceFiles/editor/editor_crop.h new file mode 100644 index 000000000..d4e19719f --- /dev/null +++ b/Telegram/SourceFiles/editor/editor_crop.h @@ -0,0 +1,95 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/rp_widget.h" + +#include "base/flat_map.h" +#include "editor/photo_editor_common.h" + +namespace Editor { + +// Crop control. +class Crop final : public Ui::RpWidget { +public: + Crop( + not_null parent, + const PhotoModifications &modifications, + const QSize &imageSize); + + void applyTransform(QRect geometry, int angle, bool flipped); + [[nodiscard]] QRect innerRect() const; + [[nodiscard]] QRect saveCropRect( + const QRect &from, + const QRect &to); + [[nodiscard]] style::margins cropMargins() const; + +protected: + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + +private: + struct InfoAtDown { + QRect rect; + Qt::Edges edge = 0; + QPoint point; + + struct Borders { + int left = 0; + int right = 0; + int top = 0; + int bottom = 0; + } borders; + }; + + [[nodiscard]] QRectF resizedCropRect( + const QRectF &from, + const QRectF &to); + + void paintPoints(Painter &p); + + void updateEdges(); + [[nodiscard]] QPoint pointOfEdge(Qt::Edges e) const; + void setCropRect(QRect &&rect); + void setCropRectPaint(QRect &&rect); + void rotate(bool clockwise = true); + + void computeDownState(const QPoint &p); + void clearDownState(); + [[nodiscard]] Qt::Edges mouseState(const QPoint &p); + void performCrop(const QPoint &pos); + void performMove(const QPoint &pos); + + const int _pointSize; + const float _pointSizeH; + const style::margins _innerMargins; + const QPoint _offset; + const QMarginsF _edgePointMargins; + + base::flat_map _edges; + + // Is translated with the inner indentation. + QRect _cropRectPaint; + // Is not. + QRect _cropRect; + + QRect _innerRect; + + QPainterPath _painterPath; + + InfoAtDown _down; + + int _angle = 0; + bool _flipped = false; + + bool _keepAspectRatio = false; + +}; + +} // namespace Editor diff --git a/Telegram/SourceFiles/editor/photo_editor.cpp b/Telegram/SourceFiles/editor/photo_editor.cpp index f1685f75d..f83fef724 100644 --- a/Telegram/SourceFiles/editor/photo_editor.cpp +++ b/Telegram/SourceFiles/editor/photo_editor.cpp @@ -52,6 +52,7 @@ PhotoEditor::PhotoEditor( } void PhotoEditor::save() { + _modifications.crop = _content->cropRect(); _done.fire_copy(_modifications); } diff --git a/Telegram/SourceFiles/editor/photo_editor_common.cpp b/Telegram/SourceFiles/editor/photo_editor_common.cpp index 67813ecd2..b06d2aadc 100644 --- a/Telegram/SourceFiles/editor/photo_editor_common.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_common.cpp @@ -20,7 +20,11 @@ QImage ImageModified(QImage image, const PhotoModifications &mods) { if (mods.angle) { transform.rotate(mods.angle); } - return image.transformed(transform); + auto newImage = image.transformed(transform); + if (mods.crop.isValid()) { + newImage = newImage.copy(mods.crop); + } + return newImage; } } // namespace Editor diff --git a/Telegram/SourceFiles/editor/photo_editor_common.h b/Telegram/SourceFiles/editor/photo_editor_common.h index 245f3259a..517ca24a2 100644 --- a/Telegram/SourceFiles/editor/photo_editor_common.h +++ b/Telegram/SourceFiles/editor/photo_editor_common.h @@ -12,9 +12,10 @@ namespace Editor { struct PhotoModifications { int angle = 0; bool flipped = false; + QRect crop; [[nodiscard]] bool empty() const { - return !angle && !flipped; + return !angle && !flipped && !crop.isValid(); } [[nodiscard]] explicit operator bool() const { return !empty(); diff --git a/Telegram/SourceFiles/editor/photo_editor_content.cpp b/Telegram/SourceFiles/editor/photo_editor_content.cpp index 54ca3a780..96460c5ad 100644 --- a/Telegram/SourceFiles/editor/photo_editor_content.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_content.cpp @@ -7,15 +7,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "editor/photo_editor_content.h" +#include "editor/editor_crop.h" #include "media/view/media_view_pip.h" namespace Editor { +using Media::View::FlipSizeByRotation; +using Media::View::RotatedRect; + PhotoEditorContent::PhotoEditorContent( not_null parent, std::shared_ptr photo, PhotoModifications modifications) : RpWidget(parent) +, _crop(base::make_unique_q(this, modifications, photo->size())) +, _photo(photo) , _modifications(modifications) { rpl::combine( @@ -23,14 +29,20 @@ PhotoEditorContent::PhotoEditorContent( sizeValue() ) | rpl::start_with_next([=]( const PhotoModifications &mods, const QSize &size) { - const auto rotatedSize = - Media::View::FlipSizeByRotation(size, mods.angle); + if (size.isEmpty()) { + return; + } const auto imageSize = [&] { - const auto originalSize = photo->size() / cIntRetinaFactor(); - if ((originalSize.width() > rotatedSize.width()) - || (originalSize.height() > rotatedSize.height())) { + const auto rotatedSize = + FlipSizeByRotation(size, mods.angle); + const auto m = _crop->cropMargins(); + const auto sizeForCrop = rotatedSize + - QSize(m.left() + m.right(), m.top() + m.bottom()); + const auto originalSize = photo->size(); + if ((originalSize.width() > sizeForCrop.width()) + || (originalSize.height() > sizeForCrop.height())) { return originalSize.scaled( - rotatedSize, + sizeForCrop, Qt::KeepAspectRatio); } return originalSize; @@ -45,6 +57,11 @@ PhotoEditorContent::PhotoEditorContent( _imageMatrix.scale(-1, 1); } _imageMatrix.rotate(mods.angle); + + _crop->applyTransform( + _imageMatrix.mapRect(_imageRect) + _crop->cropMargins(), + mods.angle, + mods.flipped); }, lifetime()); paintRequest( @@ -65,4 +82,8 @@ void PhotoEditorContent::applyModifications( update(); } +QRect PhotoEditorContent::cropRect() const { + return _crop->saveCropRect(_imageRect, _photo->rect()); +} + } // namespace Editor diff --git a/Telegram/SourceFiles/editor/photo_editor_content.h b/Telegram/SourceFiles/editor/photo_editor_content.h index bd87ba868..7ccd3acf9 100644 --- a/Telegram/SourceFiles/editor/photo_editor_content.h +++ b/Telegram/SourceFiles/editor/photo_editor_content.h @@ -13,6 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Editor { +class Crop; + class PhotoEditorContent final : public Ui::RpWidget { public: PhotoEditorContent( @@ -21,9 +23,13 @@ public: PhotoModifications modifications); void applyModifications(PhotoModifications modifications); + [[nodiscard]] QRect cropRect() const; private: + const base::unique_qptr _crop; + const std::shared_ptr _photo; + rpl::variable _modifications; QRect _imageRect;