Drop picture files on a folio to add them

Picture files (png, jpg, bmp, svg) dragged from the file manager onto a
folio are added there: the first centred on the drop point, the others
cascaded from it, and one undo step removes them all. A picture larger
than the visible part of the folio is scaled down to fit it. Files that
cannot be used are listed once after the others have been placed, and a
drop holding only other files (a .qet project) still reaches the main
window, which opens it.

The checks a picture must pass before it is embedded in the project
move into ImageDrop::load and are now shared by the drop, the add image
dialog and the script API: a regular file of at most 10 MB, and at most
64 megapixels, read from the header before any pixel is allocated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Beat Hangartner
2026-10-04 19:01:05 +02:00
parent e89c69719a
commit 034013ff67
9 changed files with 447 additions and 18 deletions
@@ -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;
}
+76 -3
View File
@@ -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,63 @@ void DiagramView::handleTextDrop(QDropEvent *e) {
iti, m_diagram, mapToScene(e->position().toPoint())));
}
/**
@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
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 QSizeF available = viewedSceneRect().size();
auto *undo = new QUndoCommand();
QStringList refused;
int placed = 0;
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);
continue;
}
auto *item = new DiagramImageItem(QPixmap::fromImage(image));
const qreal scale = ImageDrop::fitScale(item -> boundingRect().size(), available);
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);
++placed;
}
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
*/
+1
View File
@@ -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;
+103
View File
@@ -0,0 +1,103 @@
/*
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>
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;
}
qreal ImageDrop::fitScale(const QSizeF &size, const QSizeF &available)
{
if (size.isEmpty() || available.isEmpty())
return 1.0;
const qreal scale = std::min(available.width() * fitMargin / size.width(),
available.height() * fitMargin / size.height());
return std::min(1.0, scale);
}
QPointF ImageDrop::cascadeOffset(int index)
{
return QPointF(cascadeStep * index, cascadeStep * index);
}
+72
View File
@@ -0,0 +1,72 @@
/*
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 <QSizeF>
#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;
/// 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;
/// 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);
/**
Scale that fits a picture of @a size into fitMargin of @a available,
keeping its proportions; 1.0 when it already fits. Pictures are
never enlarged.
*/
qreal fitScale(const QSizeF &size, const QSizeF &available);
/// Offset of the @a index-th picture of one drop from the first one.
QPointF cascadeOffset(int index);
}
#endif // IMAGEDROP_H
+6 -13
View File
@@ -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;
}