Fit dropped pictures into the folio frame, with a margin

A dropped picture too large for the folio is now scaled to leave 20 %
of the drawing area free on every side, instead of being fitted to
the visible part of the view, and every dropped picture is kept inside
the frame -- a scaled-down one also off that margin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Beat Hangartner
2026-10-04 19:24:38 +02:00
parent 034013ff67
commit c76bc5eb4e
4 changed files with 96 additions and 27 deletions
+13 -9
View File
@@ -430,9 +430,10 @@ void DiagramView::handleTextDrop(QDropEvent *e) {
/**
@brief DiagramView::handleImageFilesDrop
Add the picture files dropped from the file manager. The first one is
centred on the drop point and the others are cascaded from it; a
picture larger than the visible part of the folio is scaled down to
fit it. One undo step removes them all. Files that cannot be used are
centred on the drop point and the others are cascaded from it. A
picture too large for the folio is scaled down to leave a free margin
of ImageDrop::frameMargin to the frame on every side, and every
picture is kept inside the frame. One undo step removes them all. Files that cannot be used are
listed once, after the others have been placed.
@param e the QDropEvent describing the current drag'n drop
*/
@@ -443,7 +444,7 @@ void DiagramView::handleImageFilesDrop(QDropEvent *e)
const QStringList files = ImageDrop::imageFiles(e -> mimeData());
const QPointF drop_pos = mapToScene(e -> position().toPoint());
const QSizeF available = viewedSceneRect().size();
const QRectF frame = m_diagram -> border_and_titleblock.insideBorderRect();
auto *undo = new QUndoCommand();
QStringList refused;
@@ -458,15 +459,18 @@ void DiagramView::handleImageFilesDrop(QDropEvent *e)
}
auto *item = new DiagramImageItem(QPixmap::fromImage(image));
const qreal scale = ImageDrop::fitScale(item -> boundingRect().size(), available);
const qreal scale = ImageDrop::fitScale(item -> boundingRect().size(), frame);
if (scale < 1.0) {
item -> setScaleFactorX(scale);
item -> setScaleFactorY(scale);
}
// Centre the picture, as transformed, on its drop point.
const QPointF centre = item -> mapRectToScene(item -> boundingRect()).center() - item -> pos();
new AddGraphicsObjectCommand(item, m_diagram,
drop_pos + ImageDrop::cascadeOffset(placed) - centre, undo);
// Centre the picture, as transformed, on its drop point, then keep
// it inside the frame -- a scaled-down one also off the margin.
QRectF rect = item -> mapRectToScene(item -> boundingRect());
const QPointF offset = rect.topLeft() - item -> pos();
rect.moveCenter(drop_pos + ImageDrop::cascadeOffset(placed));
rect = ImageDrop::keepInside(rect, scale < 1.0 ? ImageDrop::innerFrame(frame) : frame);
new AddGraphicsObjectCommand(item, m_diagram, rect.topLeft() - offset, undo);
++placed;
}
+26 -4
View File
@@ -88,15 +88,37 @@ QImage ImageDrop::load(const QString &path, QString *error)
return image;
}
qreal ImageDrop::fitScale(const QSizeF &size, const QSizeF &available)
QRectF ImageDrop::innerFrame(const QRectF &frame)
{
if (size.isEmpty() || available.isEmpty())
return frame.adjusted(frame.width() * frameMargin, frame.height() * frameMargin,
-frame.width() * frameMargin, -frame.height() * frameMargin);
}
qreal ImageDrop::fitScale(const QSizeF &size, const QRectF &frame)
{
const QRectF inner = innerFrame(frame);
if (size.isEmpty() || inner.isEmpty())
return 1.0;
const qreal scale = std::min(available.width() * fitMargin / size.width(),
available.height() * fitMargin / size.height());
const qreal scale = std::min(inner.width() / size.width(),
inner.height() / size.height());
return std::min(1.0, scale);
}
QRectF ImageDrop::keepInside(const QRectF &picture, const QRectF &area)
{
if (area.isEmpty())
return picture;
auto axis = [](qreal start, qreal length, qreal area_start, qreal area_length) {
if (length >= area_length)
return area_start + (area_length - length) / 2;
return std::clamp(start, area_start, area_start + area_length - length);
};
QRectF result = picture;
result.moveLeft(axis(picture.left(), picture.width(), area.left(), area.width()));
result.moveTop(axis(picture.top(), picture.height(), area.top(), area.height()));
return result;
}
QPointF ImageDrop::cascadeOffset(int index)
{
return QPointF(cascadeStep * index, cascadeStep * index);
+15 -4
View File
@@ -20,6 +20,7 @@
#include <QImage>
#include <QPointF>
#include <QRectF>
#include <QSizeF>
#include <QStringList>
@@ -38,8 +39,9 @@ namespace ImageDrop
constexpr qint64 maxPixels = 64LL * 1024 * 1024;
/// Offset between the pictures of one drop, in scene units.
constexpr qreal cascadeStep = 20.0;
/// Share of the visible area a large dropped picture is fitted into.
constexpr qreal fitMargin = 0.9;
/// Free margin left between a scaled-down picture and the folio frame,
/// as a share of the frame on each side.
constexpr qreal frameMargin = 0.2;
/// The suffixes the "add image" file dialog offers, lower case.
QStringList supportedSuffixes();
@@ -58,12 +60,21 @@ namespace ImageDrop
*/
QImage load(const QString &path, QString *error = nullptr);
/// @a frame shrunk by frameMargin on every side.
QRectF innerFrame(const QRectF &frame);
/**
Scale that fits a picture of @a size into fitMargin of @a available,
Scale that fits a picture of @a size into innerFrame(@a frame),
keeping its proportions; 1.0 when it already fits. Pictures are
never enlarged.
*/
qreal fitScale(const QSizeF &size, const QSizeF &available);
qreal fitScale(const QSizeF &size, const QRectF &frame);
/**
@a picture moved by the least amount that puts it inside @a area;
centred on @a area along an axis where it is larger.
*/
QRectF keepInside(const QRectF &picture, const QRectF &area);
/// Offset of the @a index-th picture of one drop from the first one.
QPointF cascadeOffset(int index);
+42 -10
View File
@@ -140,24 +140,56 @@ private slots:
QVERIFY2(error.contains(reason), qPrintable(error));
}
void largePicturesAreFittedIntoTheView_data()
// A picture too large for the folio is scaled down until it leaves 20 %
// of the frame free on every side, i.e. into 60 % of its width and
// height; a picture that already fits keeps its size.
void largePicturesLeaveAMarginToTheFrame_data()
{
QTest::addColumn<QSizeF>("size");
QTest::addColumn<QSizeF>("available");
QTest::addColumn<qreal>("scale");
QTest::newRow("small, kept") << QSizeF(100, 50) << QSizeF(1000, 800) << 1.0;
QTest::newRow("wide") << QSizeF(4000, 1000) << QSizeF(1000, 800) << 0.225;
QTest::newRow("tall") << QSizeF(500, 3200) << QSizeF(1000, 800) << 0.225;
QTest::newRow("exactly the margin") << QSizeF(900, 720) << QSizeF(1000, 800) << 1.0;
QTest::newRow("empty view") << QSizeF(4000, 1000) << QSizeF() << 1.0;
// frame: 1000 x 800, so the inner frame is 600 x 480
QTest::newRow("small, kept") << QSizeF(100, 50) << 1.0;
QTest::newRow("exactly the inner frame") << QSizeF(600, 480) << 1.0;
QTest::newRow("wide") << QSizeF(3000, 1000) << 0.2;
QTest::newRow("tall photo") << QSizeF(3024, 4032) << 480.0 / 4032;
}
void largePicturesAreFittedIntoTheView()
void largePicturesLeaveAMarginToTheFrame()
{
QFETCH(QSizeF, size);
QFETCH(QSizeF, available);
QFETCH(qreal, scale);
QCOMPARE(ImageDrop::fitScale(size, available), scale);
const QRectF frame(50, 30, 1000, 800);
QCOMPARE(ImageDrop::innerFrame(frame), QRectF(250, 190, 600, 480));
QCOMPARE(ImageDrop::fitScale(size, frame), scale);
const QSizeF scaled = size * ImageDrop::fitScale(size, frame);
QVERIFY(scaled.width() <= 600.0001 && scaled.height() <= 480.0001);
}
void noFrameMeansNoScaling()
{
QCOMPARE(ImageDrop::fitScale(QSizeF(4000, 3000), QRectF()), 1.0);
}
// A picture dropped near an edge is moved back inside, by the least
// amount; one larger than the area is centred on it.
void picturesAreKeptInsideTheArea_data()
{
QTest::addColumn<QRectF>("picture");
QTest::addColumn<QRectF>("expected");
const QRectF inside(300, 300, 100, 50);
QTest::newRow("already inside") << inside << inside;
QTest::newRow("over the top edge") << QRectF(300, -20, 100, 50) << QRectF(300, 0, 100, 50);
QTest::newRow("past the right and bottom") << QRectF(980, 790, 100, 50)
<< QRectF(900, 750, 100, 50);
QTest::newRow("wider than the area") << QRectF(-50, 100, 1200, 50)
<< QRectF(-100, 100, 1200, 50);
}
void picturesAreKeptInsideTheArea()
{
QFETCH(QRectF, picture);
QFETCH(QRectF, expected);
QCOMPARE(ImageDrop::keepInside(picture, QRectF(0, 0, 1000, 800)), expected);
}
void picturesOfOneDropCascade()