mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-18 03:54:14 +02:00
6751fc6f19
Each picture of a folio was decoded in turn while its item was built, a quarter of a second for a 12 megapixel photo. Diagram::fromXml() now reads every picture of the folio first, decodes them all at once on a pool of its own (one thread per core, at most one per picture), then builds the items in file order in the GUI thread as before. The decoding (base64 text, then the picture file) moves out of DiagramImageItem::fromXml() into decodePicture(), which touches only its argument. QImage::fromData() then QPixmap::fromImage() is what QPixmap::loadFromData() does, so the pixels are the same. On a 263 MB project with 22 photos on one folio: --info 21.3 s -> 12.5 s, the folio's content 10.6 s -> 3.2 s, opening its .qetz 15.6 s -> 9.3 s. Peak memory +220 MB (1.81 -> 2.03 GB), the pictures' text being converted by several threads at once. A resave is byte-identical to master's, and so is a PNG export. tst_folioimageload: six pictures come back with their own pixels and positions, and an <image> without a picture does not stop the others. It fails when the decoded pictures are handed to the wrong items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
319 lines
14 KiB
C++
319 lines
14 KiB
C++
/*
|
|
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 DIAGRAM_IMAGE_ITEM_H
|
|
#define DIAGRAM_IMAGE_ITEM_H
|
|
|
|
#include "qetgraphicsitem.h"
|
|
#include "shapetransform.h"
|
|
#include "../ui/imagetransparentcolordialog.h"
|
|
|
|
#include <QColor>
|
|
#include <QFont>
|
|
#include <QImage>
|
|
#include <QList>
|
|
#include <QUuid>
|
|
#include <QStyleOptionGraphicsItem>
|
|
#include <QVector>
|
|
|
|
class QDomElement;
|
|
class QDomDocument;
|
|
class QGraphicsSceneContextMenuEvent;
|
|
class QetGraphicsHandlerItem;
|
|
|
|
/**
|
|
This class represents a selectable, movable and editable image on a
|
|
diagram.
|
|
@see QGraphicsItem::GraphicsItemFlags
|
|
*/
|
|
class DiagramImageItem : public QetGraphicsItem {
|
|
Q_OBJECT
|
|
Q_PROPERTY(QPixmap pixmap READ pixmap WRITE setPixmap NOTIFY pixmapChanged)
|
|
Q_PROPERTY(qreal scaleFactorX READ scaleFactorX WRITE setScaleFactorX NOTIFY transformChanged)
|
|
Q_PROPERTY(qreal scaleFactorY READ scaleFactorY WRITE setScaleFactorY NOTIFY transformChanged)
|
|
Q_PROPERTY(qreal rotationAngle READ rotationAngle WRITE setRotationAngle NOTIFY transformChanged)
|
|
Q_PROPERTY(qreal skewX READ skewX WRITE setSkewX NOTIFY transformChanged)
|
|
Q_PROPERTY(qreal skewY READ skewY WRITE setSkewY NOTIFY transformChanged)
|
|
Q_PROPERTY(QPointF pivot READ pivot WRITE setPivot NOTIFY transformChanged)
|
|
Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
|
|
// The picture's source -- original, crop rectangle, transparent
|
|
// colours -- as one value. The displayed pixmap is computed from it,
|
|
// so every edit of it (crop, colour key, mirror, replace) is one
|
|
// undo step on this property alone.
|
|
Q_PROPERTY(QVariant imageSource READ imageSourceVariant WRITE setImageSourceVariant)
|
|
Q_PROPERTY(bool adaptToDarkTheme READ adaptToDarkTheme WRITE setAdaptToDarkTheme NOTIFY adaptToDarkThemeChanged)
|
|
// A second, deliberately non-compensating property on the SAME
|
|
// underlying value -- setPivot() (above) intentionally adjusts
|
|
// pos() to keep the image visually in place, which is exactly
|
|
// wrong for crop()'s own undo chain: crop() already computes the
|
|
// correct final pos() itself (accounting for the crop, not just
|
|
// the pivot move) and pushes it as its own separate "pos" command,
|
|
// so a chained "pivot" step going through the compensating setter
|
|
// would silently overwrite that already-correct pos() a second
|
|
// time. rawPivot exists solely for that one caller.
|
|
Q_PROPERTY(QPointF rawPivot READ pivot WRITE setPivotRaw NOTIFY transformChanged)
|
|
// Whether the pivot was placed by hand: a hand-placed pivot is saved
|
|
// and kept through a resize. Changes in the same undo step as the
|
|
// pivot itself, so that Ctrl+Z restores both.
|
|
Q_PROPERTY(bool pivotIsCustom READ pivotIsCustom WRITE setPivotIsCustom)
|
|
|
|
// constructors, destructor
|
|
public:
|
|
DiagramImageItem(QetGraphicsItem * = nullptr);
|
|
DiagramImageItem(const QPixmap &pixmap, QetGraphicsItem * = nullptr);
|
|
~DiagramImageItem() override;
|
|
|
|
struct ImageSource
|
|
{
|
|
QPixmap base;
|
|
QRect crop;
|
|
QList<ImageTransparentColorDialog::PickedColor> colors;
|
|
};
|
|
ImageSource imageSource() const { return {m_base_pixmap, m_crop_rect, m_transparent_colors}; }
|
|
void setImageSource(const ImageSource &source);
|
|
QVariant imageSourceVariant() const;
|
|
void setImageSourceVariant(const QVariant &source);
|
|
QRect cropRect() const { return m_crop_rect; }
|
|
bool applyCrop(const QRect &cropRect);
|
|
bool isCropping() const { return m_handleMode == HandleMode::Crop; }
|
|
void enterCropMode();
|
|
void finishCropMode(bool apply);
|
|
void resetCrop();
|
|
|
|
// attributes
|
|
public:
|
|
enum { Type = UserType + 1007 };
|
|
|
|
// A deliberately smaller, image-specific set than QetShapeItem's own
|
|
// HandleMode/HandleRole: images have no path/polygon/arc concepts at
|
|
// all, so reusing those enums directly would pull in a great deal of
|
|
// irrelevant baggage for no benefit. RotateSkew's math (rotate,
|
|
// skew, and their shared scaleAndShearOffset()/scaleOnlyOffset()
|
|
// helpers) mirrors QetShapeItem's own identical formulas -- adapted
|
|
// to this class's own, simpler 4-corner/4-edge/1-pivot handle set,
|
|
// not shared code, since the two classes' broader handle roles
|
|
// differ too much (no path/polygon/arc concepts here) for sharing
|
|
// the dispatcher itself to be worth an indirect abstraction.
|
|
enum class HandleMode { Size, RotateSkew, Crop };
|
|
enum class HandleRole { Resize, Rotate, SkewEdge, Pivot, CropEdge };
|
|
|
|
// methods
|
|
public:
|
|
/**
|
|
Enable the use of qgraphicsitem_cast to safely cast a QGraphicsItem into a
|
|
DiagramImageItem
|
|
@return the QGraphicsItem type
|
|
*/
|
|
int type() const override { return Type; }
|
|
|
|
/// A picture as read from its <image> element, before and after
|
|
/// decodePicture(), which may run outside the GUI thread.
|
|
struct Picture {
|
|
QString base64; ///< text of <image>, decoded when bytes is empty
|
|
QByteArray bytes; ///< the picture file (PNG, JPEG...)
|
|
QImage image; ///< bytes, decoded
|
|
bool found = true; ///< false : neither bytes nor text
|
|
};
|
|
static Picture pictureOf(const QDomElement &, const QByteArray &picture);
|
|
static void decodePicture(Picture &picture);
|
|
|
|
virtual bool fromXml(const QDomElement &);
|
|
bool fromXml(const QDomElement &, const QByteArray &picture);
|
|
bool fromXml(const QDomElement &, Picture picture);
|
|
virtual QDomElement toXml(QDomDocument &) const;
|
|
QUuid uuid() const {return m_uuid;}
|
|
void setUuid(const QUuid &uuid) {m_uuid = uuid; emit uuidChanged();}
|
|
void newUuid() {setUuid(QUuid::createUuid());} //create new uuid for this item
|
|
QRectF imageRect() const;
|
|
void editProperty() override;
|
|
void setPixmap(const QPixmap &pixmap);
|
|
QPixmap pixmap() const { return pixmap_; }
|
|
bool pivotIsCustom() const { return m_pivotIsCustom; }
|
|
void setPivotIsCustom(bool custom) { m_pivotIsCustom = custom; }
|
|
QRectF boundingRect() const override;
|
|
QString name() const override;
|
|
|
|
/// DXF export: replay this item's paint() on an arbitrary QPainter
|
|
/// (e.g. one targeting DxfPaintDevice). paint() itself stays
|
|
/// protected, as it should for the normal
|
|
/// QGraphicsScene/QGraphicsView paint contract - this is a
|
|
/// deliberate, narrow escape hatch for exporters, not a general
|
|
/// relaxation of that contract. Matches CrossRefItem::paintForExport()
|
|
/// exactly, for the identical reason.
|
|
void paintForExport(QPainter *painter)
|
|
{
|
|
QStyleOptionGraphicsItem option;
|
|
paint(painter, &option, nullptr);
|
|
}
|
|
|
|
qreal scaleFactorX() const { return m_transform.scaleX; }
|
|
qreal scaleFactorY() const { return m_transform.scaleY; }
|
|
void setScaleFactorX(qreal factor);
|
|
void setScaleFactorY(qreal factor);
|
|
qreal rotationAngle() const { return m_transform.rotation; }
|
|
void setRotationAngle(qreal angle);
|
|
qreal skewX() const { return m_transform.skewX; }
|
|
void setSkewX(qreal skew);
|
|
qreal skewY() const { return m_transform.skewY; }
|
|
void setSkewY(qreal skew);
|
|
QPointF pivot() const { return m_transform.pivot; }
|
|
void setPivot(const QPointF &pivot);
|
|
void setPivotRaw(const QPointF &pivot);
|
|
QString label() const { return m_label; }
|
|
void setLabel(const QString &label);
|
|
bool adaptToDarkTheme() const { return m_adapt_to_dark_theme; }
|
|
void setAdaptToDarkTheme(bool adapt);
|
|
|
|
signals:
|
|
void pixmapChanged();
|
|
void transformChanged();
|
|
void uuidChanged();
|
|
void labelChanged();
|
|
void adaptToDarkThemeChanged();
|
|
|
|
protected:
|
|
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override;
|
|
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
|
|
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
|
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
|
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
|
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
|
|
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
|
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
|
|
bool sceneEventFilter(QGraphicsItem *watched, QEvent *event) override;
|
|
void keyPressEvent(QKeyEvent *event) override;
|
|
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
|
|
|
|
private:
|
|
void replaceImage();
|
|
void mirror(bool horizontal);
|
|
void setTransparentColor();
|
|
void crop();
|
|
void pushImageSourceChange(const QString &text, const ImageSource &oldSource, const ImageSource &newSource);
|
|
void restoreAspectRatio();
|
|
void saveImageAs();
|
|
void saveOriginalImageAs();
|
|
void saveImagePixmapAs(const QPixmap &pixmap, const QString &dialogTitle, bool hasTransparency);
|
|
static bool writeRasterAsSvg(const QPixmap &pixmap, const QString &path);
|
|
static const QByteArray &encodedPng(const QPixmap &pixmap, QByteArray &cache, qint64 &cacheKey);
|
|
static QPixmap computeDisplayPixmap(const QPixmap &base, const QRect &cropRect, const QList<ImageTransparentColorDialog::PickedColor> &colors);
|
|
|
|
void toggleHandleMode();
|
|
HandleMode nextHandleMode() const;
|
|
static QString handleModeLabel(HandleMode mode);
|
|
void updateModeHint();
|
|
void refreshInteractionHints();
|
|
QString currentModeStatusHint() const;
|
|
void rebuildHandles();
|
|
void repositionHandles();
|
|
void clearHandles();
|
|
void resetPivotToBoundingRectCenter();
|
|
void handlerMousePressEvent(int index, Qt::KeyboardModifiers mods);
|
|
void handlerMouseMoveEvent(int index, QGraphicsSceneMouseEvent *event);
|
|
void handlerMouseReleaseEvent(int index);
|
|
void cancelHandleDrag();
|
|
void endHandleDrag();
|
|
void releaseKeyboardItem();
|
|
void dragResize(int index, const QPointF &localPos, Qt::KeyboardModifiers mods);
|
|
void dragRotateHandle(int cornerIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
|
|
void dragSkewHandle(int edgeIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
|
|
void dragPivot(const QPointF &localPos);
|
|
QPointF scaleOnlyOffset(const QPointF &localPoint) const;
|
|
QPointF scaleAndShearOffset(const QPointF &localPoint) const;
|
|
QPointF handlePosition(int index) const;
|
|
static QPointF cornerPosition(int cornerIndex, qreal w, qreal h);
|
|
static QPointF edgeMidpointPosition(int edgeIndex, qreal w, qreal h);
|
|
static QColor colorForHandleRole(HandleRole role);
|
|
static QString hintForHandleRole(HandleRole role);
|
|
void showStatusHint(const QString &text) const;
|
|
void clearStatusHint() const;
|
|
QRectF labelRect() const;
|
|
void updateLabelScale();
|
|
|
|
protected:
|
|
QUuid m_uuid = QUuid::createUuid();
|
|
QPixmap pixmap_;
|
|
// The true, pristine original -- never itself cropped or colour-
|
|
// keyed. pixmap_ (the displayed result) is always re-derived from
|
|
// this plus m_crop_rect and m_transparent_colors (each colour
|
|
// carrying its own tolerance), via computeDisplayPixmap(). Without
|
|
// keeping this separate, re-opening either the crop or
|
|
// transparency dialog after using the other would show an
|
|
// already-modified image as if it were the source -- areas already
|
|
// cropped away or coloured out would be gone for good, with no way
|
|
// to recover or adjust them, only start over. Updated by whatever
|
|
// genuinely replaces or reorients the image's actual content
|
|
// (construction, replaceImage(), and mirror(), which also mirrors
|
|
// m_crop_rect to keep referring to the same region of the
|
|
// now-flipped base) -- never by crop() or setTransparentColor()
|
|
// themselves, which only ever change which subset of this base is
|
|
// shown.
|
|
QPixmap m_base_pixmap;
|
|
QRect m_crop_rect; // relative to m_base_pixmap; equals m_base_pixmap.rect() when nothing has been cropped
|
|
QList<ImageTransparentColorDialog::PickedColor> m_transparent_colors;
|
|
// PNG bytes of pixmap_ and m_base_pixmap as last written, reused by
|
|
// toXml() while the pixmap's cacheKey() still matches. PNG encoding
|
|
// is the bulk of the time a save spends on a picture, and redoing it
|
|
// on every save, autosave and copy of an unchanged picture is pure
|
|
// waste. Filled from the file itself on load, so even the first save
|
|
// encodes nothing.
|
|
mutable QByteArray m_png_cache;
|
|
mutable qint64 m_png_cache_key = 0;
|
|
mutable QByteArray m_base_png_cache;
|
|
mutable qint64 m_base_png_cache_key = 0;
|
|
|
|
// Independent scaleX/scaleY here is the actual point of this whole
|
|
// member: QGraphicsItem::scale() is a single, uniform float, which
|
|
// is exactly why an image could never break its own aspect ratio
|
|
// before this. skewX/skewY exist in the struct but are never
|
|
// written by anything below -- deliberately deferred, not an
|
|
// oversight (see the .cpp for why).
|
|
ShapeTransform m_transform;
|
|
bool m_pivotIsCustom = false;
|
|
HandleMode m_handleMode = HandleMode::Size;
|
|
QVector<QetGraphicsHandlerItem *> m_handler_vector;
|
|
QVector<HandleRole> m_handleRoles;
|
|
int m_vector_index = -1;
|
|
QPointF m_original_pos; // scene position at the start of a resize/rotate/pivot drag, for Escape-to-cancel
|
|
bool m_drag_cancelled = false; // Escape pressed: ignore the rest of this mouse gesture
|
|
// Crop mode: the crop being edited, in the original's pixels; the
|
|
// whole original, colour-keyed, shown dimmed around it; and the
|
|
// state of a drag that moves the crop window.
|
|
QRectF m_pending_crop;
|
|
QPixmap m_crop_preview;
|
|
bool m_crop_moving = false;
|
|
QPointF m_crop_move_start;
|
|
QRectF m_crop_move_origin;
|
|
QRectF cropFrameLocal() const;
|
|
void dragCropHandle(int index, const QPointF &scenePos);
|
|
ShapeTransform m_original_transform;
|
|
bool m_original_pivotIsCustom = false;
|
|
bool m_deferHandleReposition = false; // see setPivot()'s comment
|
|
// Optional caption drawn centred under the picture (issue #349).
|
|
// Empty by default, and then neither saved nor painted, so a picture
|
|
// without one costs exactly what it did before.
|
|
QString m_label;
|
|
bool m_adapt_to_dark_theme = false;
|
|
QFont m_label_font;
|
|
QSizeF m_label_size; // in scene units, measured once in setLabel()
|
|
QPointF m_label_scale{1.0, 1.0}; // scale the label rect was last computed for -- see updateLabelScale()
|
|
bool m_resizeCenterAnchored = false; // decided once, at press time -- see handlerMousePressEvent()'s comment for why, mirroring the identical fix already made for shape creation
|
|
};
|
|
Q_DECLARE_METATYPE(DiagramImageItem::ImageSource)
|
|
|
|
#endif
|