mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-10 14:14:18 +02:00
Merge pull request #1416 from bhangart/feature/drop-images-on-folio
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m40s
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m40s
Drop picture files on a folio to add them
This commit is contained in:
@@ -216,6 +216,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/diagramposition.h
|
||||
${QET_DIR}/sources/diagramview.cpp
|
||||
${QET_DIR}/sources/diagramview.h
|
||||
${QET_DIR}/sources/imagedrop.cpp
|
||||
${QET_DIR}/sources/imagedrop.h
|
||||
${QET_DIR}/sources/elementdialog.cpp
|
||||
${QET_DIR}/sources/elementdialog.h
|
||||
${QET_DIR}/sources/elementprovider.cpp
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
#include "diagrameventaddimage.h"
|
||||
#include "../imagedrop.h"
|
||||
|
||||
#include "../qetapp.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
@@ -341,10 +342,11 @@ void DiagramEventAddImage::openDialog()
|
||||
|
||||
if (fileName.isEmpty()) return;
|
||||
|
||||
QImage image(fileName);
|
||||
QString error;
|
||||
const QImage image = ImageDrop::load(fileName, &error);
|
||||
if(image.isNull())
|
||||
{
|
||||
QMessageBox::critical(m_diagram->views().isEmpty()? nullptr : m_diagram->views().first(), QObject::tr("Error"), QObject::tr("Unable to load the image."));
|
||||
QMessageBox::critical(m_diagram->views().isEmpty()? nullptr : m_diagram->views().first(), QObject::tr("Error"), QObject::tr("Unable to load the image.") + "\n" + error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+100
-3
@@ -30,6 +30,7 @@
|
||||
#include "qetdiagrameditor.h"
|
||||
#include "qetgraphicsitem/conductor.h"
|
||||
#include "qetgraphicsitem/conductortextitem.h"
|
||||
#include "qetgraphicsitem/diagramimageitem.h"
|
||||
#include "qetgraphicsitem/independenttextitem.h"
|
||||
#include "qeticons.h"
|
||||
#include "qetpalette.h"
|
||||
@@ -42,6 +43,7 @@
|
||||
#include "diagram.h"
|
||||
#include "foliogrid.h"
|
||||
#include "diagramcontexttoolbar.h"
|
||||
#include "imagedrop.h"
|
||||
#include "diagramgestureoverlay.h"
|
||||
#include "gesturesettings.h"
|
||||
#include "shortcutbarsettings.h"
|
||||
@@ -55,6 +57,7 @@
|
||||
#include "qetversion.h"
|
||||
#include <QApplication>
|
||||
#include <QDropEvent>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
@@ -222,7 +225,14 @@ DiagramView::~DiagramView()
|
||||
@param e le QDragEnterEvent correspondant au drag'n drop tente
|
||||
*/
|
||||
void DiagramView::dragEnterEvent(QDragEnterEvent *e) {
|
||||
if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
|
||||
// Picture files from the file manager are checked first: a file drag
|
||||
// also carries its path as text, which would otherwise become a text.
|
||||
// Any other file (a .qet project...) is left to the main window.
|
||||
if (!ImageDrop::imageFiles(e -> mimeData()).isEmpty()) {
|
||||
e -> acceptProposedAction();
|
||||
} else if (ImageDrop::hasOnlyOtherUrls(e -> mimeData())) {
|
||||
e -> ignore();
|
||||
} else if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
|
||||
e -> acceptProposedAction();
|
||||
} else if (e -> mimeData() -> hasFormat("application/x-qet-titleblock-uri")) {
|
||||
e -> acceptProposedAction();
|
||||
@@ -238,7 +248,9 @@ void DiagramView::dragEnterEvent(QDragEnterEvent *e) {
|
||||
@param e le QDragMoveEvent correspondant au drag'n drop tente
|
||||
*/
|
||||
void DiagramView::dragMoveEvent(QDragMoveEvent *e) {
|
||||
if (e -> mimeData() -> hasFormat("text/plain")) e -> acceptProposedAction();
|
||||
if (!ImageDrop::imageFiles(e -> mimeData()).isEmpty()) e -> acceptProposedAction();
|
||||
else if (ImageDrop::hasOnlyOtherUrls(e -> mimeData())) e -> ignore();
|
||||
else if (e -> mimeData() -> hasFormat("text/plain")) e -> acceptProposedAction();
|
||||
else e-> ignore();
|
||||
}
|
||||
|
||||
@@ -248,7 +260,11 @@ void DiagramView::dragMoveEvent(QDragMoveEvent *e) {
|
||||
*/
|
||||
void DiagramView::dropEvent(QDropEvent *e) {
|
||||
|
||||
if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
|
||||
if (!ImageDrop::imageFiles(e -> mimeData()).isEmpty()) {
|
||||
handleImageFilesDrop(e);
|
||||
} else if (ImageDrop::hasOnlyOtherUrls(e -> mimeData())) {
|
||||
e -> ignore();
|
||||
} else if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
|
||||
handleElementDrop(e);
|
||||
} else if (e -> mimeData() -> hasFormat("application/x-qet-titleblock-uri")) {
|
||||
handleTitleBlockDrop(e);
|
||||
@@ -411,6 +427,87 @@ void DiagramView::handleTextDrop(QDropEvent *e) {
|
||||
iti, m_diagram, mapToScene(e->position().toPoint())));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramView::handleImageFilesDrop
|
||||
Add the picture files dropped from the file manager. A single one is
|
||||
centred on the drop point and kept inside the frame; a picture too
|
||||
large for the folio is scaled down to leave a free margin of
|
||||
ImageDrop::frameMargin to the frame on every side. Several pictures
|
||||
are spread side by side in a grid over that same area
|
||||
(ImageDrop::gridLayout). 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
|
||||
*/
|
||||
void DiagramView::handleImageFilesDrop(QDropEvent *e)
|
||||
{
|
||||
if (m_diagram -> isReadOnly()) return;
|
||||
e -> acceptProposedAction();
|
||||
|
||||
const QStringList files = ImageDrop::imageFiles(e -> mimeData());
|
||||
const QPointF drop_pos = mapToScene(e -> position().toPoint());
|
||||
const QRectF frame = m_diagram -> border_and_titleblock.insideBorderRect();
|
||||
|
||||
QStringList refused;
|
||||
QList<DiagramImageItem *> items;
|
||||
for (const QString &file : files)
|
||||
{
|
||||
QString error;
|
||||
const QImage image = ImageDrop::load(file, &error);
|
||||
if (image.isNull())
|
||||
refused << QStringLiteral("%1 : %2").arg(QFileInfo(file).fileName(), error);
|
||||
else
|
||||
items << new DiagramImageItem(QPixmap::fromImage(image));
|
||||
}
|
||||
|
||||
// Where each picture goes, as it will look on the folio: one picture
|
||||
// is centred on the drop point and fitted to the frame, several are
|
||||
// spread over the frame in a grid.
|
||||
QList<QRectF> targets;
|
||||
if (items.size() == 1)
|
||||
{
|
||||
const QSizeF size = items.first() -> mapRectToScene(items.first() -> boundingRect()).size();
|
||||
const qreal scale = ImageDrop::fitScale(size, frame);
|
||||
QRectF r(QPointF(), size * scale);
|
||||
r.moveCenter(drop_pos);
|
||||
targets << ImageDrop::keepInside(r, scale < 1.0 ? ImageDrop::innerFrame(frame) : frame);
|
||||
}
|
||||
else
|
||||
{
|
||||
QList<QSizeF> sizes;
|
||||
for (DiagramImageItem *item : items)
|
||||
sizes << item -> mapRectToScene(item -> boundingRect()).size();
|
||||
targets = ImageDrop::gridLayout(sizes, ImageDrop::innerFrame(frame));
|
||||
}
|
||||
|
||||
auto *undo = new QUndoCommand();
|
||||
const int placed = int(items.size());
|
||||
for (int i = 0 ; i < placed ; ++i)
|
||||
{
|
||||
DiagramImageItem *item = items.at(i);
|
||||
const QRectF natural = item -> mapRectToScene(item -> boundingRect());
|
||||
const qreal scale = natural.width() > 0 ? targets.at(i).width() / natural.width() : 1.0;
|
||||
if (scale < 1.0) {
|
||||
item -> setScaleFactorX(scale);
|
||||
item -> setScaleFactorY(scale);
|
||||
}
|
||||
const QPointF offset = item -> mapRectToScene(item -> boundingRect()).topLeft() - item -> pos();
|
||||
new AddGraphicsObjectCommand(item, m_diagram, targets.at(i).topLeft() - offset, undo);
|
||||
}
|
||||
|
||||
if (placed) {
|
||||
undo -> setText(placed == 1 ? tr("Add an image")
|
||||
: tr("Add %n image(s)", nullptr, placed));
|
||||
m_diagram -> undoStack().push(undo);
|
||||
} else {
|
||||
delete undo;
|
||||
}
|
||||
|
||||
if (!refused.isEmpty())
|
||||
QMessageBox::warning(this, tr("Images not added"),
|
||||
tr("These files could not be added:") + "\n\n"
|
||||
+ refused.join("\n"));
|
||||
}
|
||||
|
||||
/**
|
||||
Set the Diagram in visualisation mode
|
||||
*/
|
||||
|
||||
@@ -145,6 +145,7 @@ class DiagramView : public PaletteGraphicsView
|
||||
void updateFolioReportMenu();
|
||||
void handleTitleBlockDrop(QDropEvent *);
|
||||
void handleTextDrop(QDropEvent *);
|
||||
void handleImageFilesDrop(QDropEvent *);
|
||||
void scrollOnMovement(QKeyEvent *);
|
||||
bool gestureEvent(QGestureEvent *event);
|
||||
QRectF viewedSceneRect() const;
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "imagedrop.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFileInfo>
|
||||
#include <QImageReader>
|
||||
#include <QMimeData>
|
||||
#include <QUrl>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool isImageFile(const QUrl &url)
|
||||
{
|
||||
if (!url.isLocalFile())
|
||||
return false;
|
||||
const QString suffix = QFileInfo(url.toLocalFile()).suffix().toLower();
|
||||
return ImageDrop::supportedSuffixes().contains(suffix);
|
||||
}
|
||||
|
||||
QString tr(const char *text)
|
||||
{
|
||||
return QCoreApplication::translate("ImageDrop", text);
|
||||
}
|
||||
}
|
||||
|
||||
QStringList ImageDrop::supportedSuffixes()
|
||||
{
|
||||
return {QStringLiteral("png"), QStringLiteral("jpg"), QStringLiteral("jpeg"),
|
||||
QStringLiteral("bmp"), QStringLiteral("svg")};
|
||||
}
|
||||
|
||||
QStringList ImageDrop::imageFiles(const QMimeData *mime)
|
||||
{
|
||||
QStringList files;
|
||||
if (!mime || !mime->hasUrls())
|
||||
return files;
|
||||
for (const QUrl &url : mime->urls())
|
||||
if (isImageFile(url))
|
||||
files << url.toLocalFile();
|
||||
return files;
|
||||
}
|
||||
|
||||
bool ImageDrop::hasOnlyOtherUrls(const QMimeData *mime)
|
||||
{
|
||||
return mime && mime->hasUrls() && imageFiles(mime).isEmpty();
|
||||
}
|
||||
|
||||
QImage ImageDrop::load(const QString &path, QString *error)
|
||||
{
|
||||
auto fail = [error](const QString &message) {
|
||||
if (error)
|
||||
*error = message;
|
||||
return QImage();
|
||||
};
|
||||
|
||||
const QFileInfo info(path);
|
||||
if (!info.isFile())
|
||||
return fail(tr("not a file"));
|
||||
if (info.size() > maxFileBytes)
|
||||
return fail(tr("the file is larger than 10 MB"));
|
||||
|
||||
QImageReader reader(path);
|
||||
const QSize size = reader.size();
|
||||
if (size.isValid() && qint64(size.width()) * size.height() > maxPixels)
|
||||
return fail(tr("the image has too many pixels"));
|
||||
|
||||
const QImage image = reader.read();
|
||||
if (image.isNull())
|
||||
return fail(tr("unable to read the image"));
|
||||
return image;
|
||||
}
|
||||
|
||||
QRectF ImageDrop::innerFrame(const QRectF &frame)
|
||||
{
|
||||
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(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;
|
||||
}
|
||||
|
||||
QList<QRectF> ImageDrop::gridLayout(const QList<QSizeF> &sizes, const QRectF &area)
|
||||
{
|
||||
QList<QRectF> rects;
|
||||
const int count = int(sizes.size());
|
||||
if (count == 0)
|
||||
return rects;
|
||||
const int columns = int(std::ceil(std::sqrt(qreal(count))));
|
||||
const int rows = (count + columns - 1) / columns;
|
||||
const QSizeF cell(area.width() / columns, area.height() / rows);
|
||||
const QSizeF room = cell * (1.0 - gridGap);
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
const QSizeF size = sizes.at(i);
|
||||
qreal scale = 1.0;
|
||||
if (!size.isEmpty() && !room.isEmpty())
|
||||
scale = std::min({1.0, room.width() / size.width(), room.height() / size.height()});
|
||||
QRectF r(QPointF(), size * scale);
|
||||
r.moveCenter(area.topLeft() + QPointF(cell.width() * (i % columns + 0.5),
|
||||
cell.height() * (i / columns + 0.5)));
|
||||
rects << r;
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef IMAGEDROP_H
|
||||
#define IMAGEDROP_H
|
||||
|
||||
#include <QImage>
|
||||
#include <QPointF>
|
||||
#include <QRectF>
|
||||
#include <QSizeF>
|
||||
#include <QList>
|
||||
#include <QStringList>
|
||||
|
||||
class QMimeData;
|
||||
|
||||
/**
|
||||
Picture files dropped on a folio: which files qualify, how they are
|
||||
loaded, and where they are placed. Kept free of any QElectroTech class
|
||||
so that it can be tested on its own.
|
||||
*/
|
||||
namespace ImageDrop
|
||||
{
|
||||
/// Pictures are embedded in the project, so larger files are refused.
|
||||
constexpr qint64 maxFileBytes = 10LL * 1024 * 1024;
|
||||
/// Refuses decompression bombs before any pixel is allocated.
|
||||
constexpr qint64 maxPixels = 64LL * 1024 * 1024;
|
||||
/// Space left between the pictures of one drop laid out in a grid, as
|
||||
/// a share of a grid cell.
|
||||
constexpr qreal gridGap = 0.05;
|
||||
/// 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();
|
||||
|
||||
/// The local picture files among the URLs of @a mime, in drop order.
|
||||
QStringList imageFiles(const QMimeData *mime);
|
||||
|
||||
/// True when @a mime carries URLs and none of them is a picture file:
|
||||
/// the drop is meant for someone else (a .qet project, an element).
|
||||
bool hasOnlyOtherUrls(const QMimeData *mime);
|
||||
|
||||
/**
|
||||
Load the picture at @a path, checking that it is a regular file of
|
||||
at most maxFileBytes and maxPixels. On failure the image is null
|
||||
and @a error, if given, says why.
|
||||
*/
|
||||
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 innerFrame(@a frame),
|
||||
keeping its proportions; 1.0 when it already fits. Pictures are
|
||||
never enlarged.
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
Lay out pictures of @a sizes, dropped together, side by side in a
|
||||
grid filling @a area: as many columns as needed for a roughly
|
||||
square grid, every picture shrunk -- never enlarged -- into its
|
||||
cell, proportions kept, and centred in it.
|
||||
@return where each picture goes, in the order of @a sizes
|
||||
*/
|
||||
QList<QRectF> gridLayout(const QList<QSizeF> &sizes, const QRectF &area);
|
||||
}
|
||||
|
||||
#endif // IMAGEDROP_H
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "../diagramcontent.h"
|
||||
#include "../diagramview.h"
|
||||
#include "../factory/elementfactory.h"
|
||||
#include "../imagedrop.h"
|
||||
#include "../factory/qetgraphicstablefactory.h"
|
||||
#include "../factory/ui/addtabledialog.h"
|
||||
#include "../qet.h"
|
||||
@@ -3437,20 +3438,12 @@ int QetScriptApi::addImage(int folioIndex, const QString &filePath, double x, do
|
||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
|
||||
|
||||
const QFileInfo info(filePath);
|
||||
if (!info.isFile()) {
|
||||
log(QStringLiteral("qet.addImage: '%1' is not a file").arg(filePath));
|
||||
return -1;
|
||||
}
|
||||
constexpr qint64 max_bytes = 10LL * 1024 * 1024;
|
||||
if (info.size() > max_bytes) {
|
||||
log(QStringLiteral("qet.addImage: '%1' is %2 bytes; images are embedded in the project, "
|
||||
"so files over 10 MB are refused").arg(filePath).arg(info.size()));
|
||||
return -1;
|
||||
}
|
||||
const QImage image(filePath);
|
||||
// Same checks as a picture dropped on a folio: a regular file of at
|
||||
// most 10 MB (it is embedded in the project) and a bounded pixel count.
|
||||
QString error;
|
||||
const QImage image = ImageDrop::load(filePath, &error);
|
||||
if (image.isNull()) {
|
||||
log(QStringLiteral("qet.addImage: '%1' could not be read as an image").arg(filePath));
|
||||
log(QStringLiteral("qet.addImage: '%1': %2").arg(filePath, error));
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1105,6 +1105,17 @@ add_test(NAME tst_borderpropertiesxml COMMAND tst_borderpropertiesxml)
|
||||
target_include_directories(tst_borderpropertiesxml PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_borderpropertiesxml PRIVATE Qt::Test Qt::Xml)
|
||||
|
||||
# Picture files dropped on a folio: which files are taken, the size and
|
||||
# pixel limits before a picture is embedded, fitting into the view and the
|
||||
# cascade of several pictures.
|
||||
add_executable(
|
||||
tst_imagedrop
|
||||
tst_imagedrop.cpp
|
||||
${QET_DIR}/sources/imagedrop.cpp)
|
||||
add_test(NAME tst_imagedrop COMMAND tst_imagedrop)
|
||||
target_include_directories(tst_imagedrop PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_imagedrop PRIVATE Qt::Test Qt::Gui)
|
||||
|
||||
# Qt 6.4.2 writes the attributes of an element in an order that changes from
|
||||
# one process to the next, so a project saved twice by two runs of the binary
|
||||
# differs in bytes only (seen on projects without any numbering, too). A fixed
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "imagedrop.h"
|
||||
|
||||
#include <QtEndian>
|
||||
#include <QtTest>
|
||||
|
||||
#include <QImage>
|
||||
#include <QMimeData>
|
||||
#include <QTemporaryDir>
|
||||
#include <QUrl>
|
||||
|
||||
/**
|
||||
Picture files dropped on a folio: which files are taken, the limits a
|
||||
file must meet before it is embedded in the project, and where the
|
||||
pictures of one drop land.
|
||||
*/
|
||||
class tst_imagedrop : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
QString path(const QString &name) const { return m_dir.filePath(name); }
|
||||
|
||||
static QMimeData *urls(const QStringList &paths)
|
||||
{
|
||||
auto *mime = new QMimeData();
|
||||
QList<QUrl> list;
|
||||
for (const QString &p : paths)
|
||||
list << (p.startsWith("http") ? QUrl(p) : QUrl::fromLocalFile(p));
|
||||
mime->setUrls(list);
|
||||
return mime;
|
||||
}
|
||||
|
||||
// A BMP header announcing width x height pixels, without the pixels.
|
||||
static QByteArray bmpHeader(qint32 width, qint32 height)
|
||||
{
|
||||
QByteArray b(54, '\0');
|
||||
b[0] = 'B'; b[1] = 'M';
|
||||
qToLittleEndian<quint32>(54, b.data() + 2);
|
||||
qToLittleEndian<quint32>(54, b.data() + 10);
|
||||
qToLittleEndian<quint32>(40, b.data() + 14);
|
||||
qToLittleEndian<qint32>(width, b.data() + 18);
|
||||
qToLittleEndian<qint32>(height, b.data() + 22);
|
||||
qToLittleEndian<quint16>(1, b.data() + 26);
|
||||
qToLittleEndian<quint16>(24, b.data() + 28);
|
||||
return b;
|
||||
}
|
||||
|
||||
static void write(const QString &p, const QByteArray &data)
|
||||
{
|
||||
QFile f(p);
|
||||
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||
f.write(data);
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QImage img(40, 20, QImage::Format_ARGB32);
|
||||
img.fill(Qt::red);
|
||||
QVERIFY(img.save(path("a.png")));
|
||||
QVERIFY(img.save(path("b.JPG"), "JPG"));
|
||||
write(path("plan.qet"), "<project/>");
|
||||
write(path("fake.png"), "this is not a picture");
|
||||
write(path("bomb.bmp"), bmpHeader(10000, 10000));
|
||||
QFile big(path("big.png"));
|
||||
QVERIFY(big.open(QIODevice::WriteOnly));
|
||||
QVERIFY(big.resize(ImageDrop::maxFileBytes + 1));
|
||||
}
|
||||
|
||||
// Only local files with a picture suffix are taken, in drop order and
|
||||
// whatever the case of the suffix.
|
||||
void picturesAreTakenInOrder()
|
||||
{
|
||||
std::unique_ptr<QMimeData> mime(urls({path("b.JPG"), path("plan.qet"), path("a.png"),
|
||||
"https://example.org/remote.png"}));
|
||||
QCOMPARE(ImageDrop::imageFiles(mime.get()), QStringList({path("b.JPG"), path("a.png")}));
|
||||
QVERIFY(!ImageDrop::hasOnlyOtherUrls(mime.get()));
|
||||
}
|
||||
|
||||
// A drop holding only a project is left to the main window, which
|
||||
// opens it; plain text is not a URL drop at all.
|
||||
void otherFilesAreLeftAlone()
|
||||
{
|
||||
std::unique_ptr<QMimeData> project(urls({path("plan.qet")}));
|
||||
QVERIFY(ImageDrop::imageFiles(project.get()).isEmpty());
|
||||
QVERIFY(ImageDrop::hasOnlyOtherUrls(project.get()));
|
||||
|
||||
QMimeData text;
|
||||
text.setText("K1");
|
||||
QVERIFY(!ImageDrop::hasOnlyOtherUrls(&text));
|
||||
QVERIFY(ImageDrop::imageFiles(&text).isEmpty());
|
||||
QVERIFY(ImageDrop::imageFiles(nullptr).isEmpty());
|
||||
}
|
||||
|
||||
void validPictureLoads()
|
||||
{
|
||||
QString error;
|
||||
const QImage image = ImageDrop::load(path("a.png"), &error);
|
||||
QCOMPARE(image.size(), QSize(40, 20));
|
||||
QVERIFY(error.isEmpty());
|
||||
}
|
||||
|
||||
void unusableFilesAreRefusedWithAReason_data()
|
||||
{
|
||||
QTest::addColumn<QString>("file");
|
||||
QTest::addColumn<QString>("reason");
|
||||
QTest::newRow("missing") << path("missing.png") << "file";
|
||||
QTest::newRow("directory") << m_dir.path() << "file";
|
||||
QTest::newRow("not a picture") << path("fake.png") << "read";
|
||||
QTest::newRow("over 10 MB") << path("big.png") << "10 MB";
|
||||
// refused on its header, before the 300 MB of pixels are allocated
|
||||
QTest::newRow("too many pixels") << path("bomb.bmp") << "pixels";
|
||||
}
|
||||
|
||||
void unusableFilesAreRefusedWithAReason()
|
||||
{
|
||||
QFETCH(QString, file);
|
||||
QFETCH(QString, reason);
|
||||
QString error;
|
||||
QVERIFY(ImageDrop::load(file, &error).isNull());
|
||||
QVERIFY2(error.contains(reason), qPrintable(error));
|
||||
}
|
||||
|
||||
// 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<qreal>("scale");
|
||||
// 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 largePicturesLeaveAMarginToTheFrame()
|
||||
{
|
||||
QFETCH(QSizeF, size);
|
||||
QFETCH(qreal, 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);
|
||||
}
|
||||
|
||||
// Several pictures dropped together are spread side by side: a
|
||||
// roughly square grid over the area, each shrunk into its cell.
|
||||
void severalPicturesAreSpreadInAGrid_data()
|
||||
{
|
||||
QTest::addColumn<int>("count");
|
||||
QTest::addColumn<int>("columns");
|
||||
QTest::addColumn<int>("rows");
|
||||
QTest::newRow("2") << 2 << 2 << 1;
|
||||
QTest::newRow("3") << 3 << 2 << 2;
|
||||
QTest::newRow("4") << 4 << 2 << 2;
|
||||
QTest::newRow("5") << 5 << 3 << 2;
|
||||
QTest::newRow("9") << 9 << 3 << 3;
|
||||
}
|
||||
|
||||
void severalPicturesAreSpreadInAGrid()
|
||||
{
|
||||
QFETCH(int, count);
|
||||
QFETCH(int, columns);
|
||||
QFETCH(int, rows);
|
||||
const QRectF area(100, 50, 600, 480);
|
||||
const QList<QSizeF> sizes(count, QSizeF(3024, 4032));
|
||||
const QList<QRectF> rects = ImageDrop::gridLayout(sizes, area);
|
||||
QCOMPARE(rects.size(), count);
|
||||
|
||||
const QSizeF cell(area.width() / columns, area.height() / rows);
|
||||
for (int i = 0 ; i < count ; ++i)
|
||||
{
|
||||
const QRectF r = rects.at(i);
|
||||
// inside the area, proportions kept
|
||||
QVERIFY(area.contains(r));
|
||||
QVERIFY(qAbs(r.width() / r.height() - 3024.0 / 4032.0) < 1e-6);
|
||||
// centred in its own cell, row by row
|
||||
const QPointF centre = area.topLeft() + QPointF(cell.width() * (i % columns + 0.5),
|
||||
cell.height() * (i / columns + 0.5));
|
||||
QVERIFY(qAbs(r.center().x() - centre.x()) < 1e-6);
|
||||
QVERIFY(qAbs(r.center().y() - centre.y()) < 1e-6);
|
||||
// and overlapping no other
|
||||
for (int j = i + 1 ; j < count ; ++j)
|
||||
QVERIFY2(!r.intersects(rects.at(j)), qPrintable(QString("%1 and %2").arg(i).arg(j)));
|
||||
}
|
||||
}
|
||||
|
||||
void smallPicturesAreNotEnlargedInTheGrid()
|
||||
{
|
||||
const QList<QRectF> rects = ImageDrop::gridLayout({QSizeF(40, 20), QSizeF(30, 30)},
|
||||
QRectF(0, 0, 600, 480));
|
||||
QCOMPARE(rects.at(0).size(), QSizeF(40, 20));
|
||||
QCOMPARE(rects.at(1).size(), QSizeF(30, 30));
|
||||
QCOMPARE(rects.at(0).center(), QPointF(150, 240));
|
||||
QCOMPARE(rects.at(1).center(), QPointF(450, 240));
|
||||
}
|
||||
|
||||
void emptyDropHasNoLayout()
|
||||
{
|
||||
QVERIFY(ImageDrop::gridLayout({}, QRectF(0, 0, 600, 480)).isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_imagedrop)
|
||||
#include "tst_imagedrop.moc"
|
||||
Reference in New Issue
Block a user