File indexing completed on 2024-05-12 03:48:19

0001 /*
0002     File                 : Image.cpp
0003     Project              : LabPlot
0004     Description          : Worksheet element to draw images
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2019-2022 Alexander Semke <alexander.semke@web.de>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "Image.h"
0012 #include "ImagePrivate.h"
0013 #include "Worksheet.h"
0014 #include "backend/lib/XmlStreamReader.h"
0015 #include "backend/lib/commandtemplates.h"
0016 #include "backend/worksheet/Line.h"
0017 
0018 #include <QBuffer>
0019 #include <QFileInfo>
0020 #include <QGraphicsScene>
0021 #include <QGraphicsSceneMouseEvent>
0022 #include <QIcon>
0023 #include <QMenu>
0024 #include <QPainter>
0025 
0026 #include <KConfig>
0027 #include <KConfigGroup>
0028 #include <KLocalizedString>
0029 
0030 /**
0031  * \class Image
0032  * \brief A label supporting rendering of html- and tex-formatted texts.
0033  *
0034  * The label is aligned relative to the specified position.
0035  * The position can be either specified by providing the x- and y- coordinates
0036  * in parent's coordinate system, or by specifying one of the predefined position
0037  * flags (\c HorizontalPosition, \c VerticalPosition).
0038  */
0039 
0040 Image::Image(const QString& name)
0041     : WorksheetElement(name, new ImagePrivate(this), AspectType::Image) {
0042     init();
0043 }
0044 
0045 Image::Image(const QString& name, ImagePrivate* dd)
0046     : WorksheetElement(name, dd, AspectType::Image) {
0047     init();
0048 }
0049 
0050 void Image::init() {
0051     Q_D(Image);
0052 
0053     KConfig config;
0054     KConfigGroup group = config.group(QStringLiteral("Image"));
0055 
0056     d->embedded = group.readEntry(QStringLiteral("embedded"), true);
0057     d->opacity = group.readEntry(QStringLiteral("opacity"), d->opacity);
0058 
0059     // geometry
0060     d->position.point.setX(group.readEntry(QStringLiteral("PositionXValue"), 0.));
0061     d->position.point.setY(group.readEntry(QStringLiteral("PositionYValue"), 0.));
0062     d->position.horizontalPosition =
0063         (WorksheetElement::HorizontalPosition)group.readEntry(QStringLiteral("PositionX"), (int)WorksheetElement::HorizontalPosition::Center);
0064     d->position.verticalPosition =
0065         (WorksheetElement::VerticalPosition)group.readEntry(QStringLiteral("PositionY"), (int)WorksheetElement::VerticalPosition::Center);
0066     d->horizontalAlignment =
0067         (WorksheetElement::HorizontalAlignment)group.readEntry(QStringLiteral("HorizontalAlignment"), (int)WorksheetElement::HorizontalAlignment::Center);
0068     d->verticalAlignment =
0069         (WorksheetElement::VerticalAlignment)group.readEntry(QStringLiteral("VerticalAlignment"), (int)WorksheetElement::VerticalAlignment::Center);
0070     d->setRotation(group.readEntry(QStringLiteral("Rotation"), d->rotation()));
0071 
0072     // border
0073     d->borderLine = new Line(QString());
0074     d->borderLine->setPrefix(QStringLiteral("Border"));
0075     d->borderLine->setHidden(true);
0076     addChild(d->borderLine);
0077     d->borderLine->init(group);
0078     connect(d->borderLine, &Line::updatePixmapRequested, [=] {
0079         d->update();
0080     });
0081     connect(d->borderLine, &Line::updateRequested, [=] {
0082         d->recalcShapeAndBoundingRect();
0083     });
0084 }
0085 
0086 // no need to delete the d-pointer here - it inherits from QGraphicsItem
0087 // and is deleted during the cleanup in QGraphicsScene
0088 Image::~Image() = default;
0089 
0090 void Image::setParentGraphicsItem(QGraphicsItem* item) {
0091     Q_D(Image);
0092     d->setParentItem(item);
0093     d->updatePosition();
0094 }
0095 
0096 void Image::retransform() {
0097     Q_D(Image);
0098     d->retransform();
0099 }
0100 
0101 void Image::handleResize(double /*horizontalRatio*/, double /*verticalRatio*/, bool /*pageResize*/) {
0102     DEBUG(Q_FUNC_INFO);
0103     //  Q_D(Image);
0104 
0105     //  double ratio = 0;
0106     //  if (horizontalRatio > 1.0 || verticalRatio > 1.0)
0107     //      ratio = std::max(horizontalRatio, verticalRatio);
0108     //  else
0109     //      ratio = std::min(horizontalRatio, verticalRatio);
0110 }
0111 
0112 /*!
0113     Returns an icon to be used in the project explorer.
0114 */
0115 QIcon Image::icon() const {
0116     return QIcon::fromTheme(QStringLiteral("viewimage"));
0117 }
0118 
0119 /* ============================ getter methods ================= */
0120 BASIC_SHARED_D_READER_IMPL(Image, QString, fileName, fileName)
0121 BASIC_SHARED_D_READER_IMPL(Image, bool, embedded, embedded)
0122 BASIC_SHARED_D_READER_IMPL(Image, qreal, opacity, opacity)
0123 BASIC_SHARED_D_READER_IMPL(Image, int, width, width)
0124 BASIC_SHARED_D_READER_IMPL(Image, int, height, height)
0125 BASIC_SHARED_D_READER_IMPL(Image, bool, keepRatio, keepRatio)
0126 
0127 Line* Image::borderLine() const {
0128     Q_D(const Image);
0129     return d->borderLine;
0130 }
0131 
0132 /* ============================ setter methods and undo commands ================= */
0133 STD_SETTER_CMD_IMPL_F_S(Image, SetFileName, QString, fileName, updateImage)
0134 void Image::setFileName(const QString& fileName) {
0135     Q_D(Image);
0136     if (fileName != d->fileName)
0137         exec(new ImageSetFileNameCmd(d, fileName, ki18n("%1: set image")));
0138 }
0139 
0140 STD_SETTER_CMD_IMPL_S(Image, SetEmbedded, bool, embedded)
0141 void Image::setEmbedded(bool embedded) {
0142     Q_D(Image);
0143     if (embedded != d->embedded)
0144         exec(new ImageSetEmbeddedCmd(d, embedded, ki18n("%1: embed image")));
0145 }
0146 
0147 STD_SETTER_CMD_IMPL_F_S(Image, SetOpacity, qreal, opacity, update)
0148 void Image::setOpacity(qreal opacity) {
0149     Q_D(Image);
0150     if (opacity != d->opacity)
0151         exec(new ImageSetOpacityCmd(d, opacity, ki18n("%1: set border opacity")));
0152 }
0153 
0154 STD_SETTER_CMD_IMPL_F_S(Image, SetWidth, int, width, retransform)
0155 void Image::setWidth(int width) {
0156     Q_D(Image);
0157     if (width != d->width) {
0158         exec(new ImageSetWidthCmd(d, width, ki18n("%1: set width")));
0159         d->scaleImage();
0160     }
0161 }
0162 
0163 STD_SETTER_CMD_IMPL_F_S(Image, SetHeight, int, height, retransform)
0164 void Image::setHeight(int height) {
0165     Q_D(Image);
0166     if (height != d->height) {
0167         exec(new ImageSetHeightCmd(d, height, ki18n("%1: set height")));
0168         d->scaleImage();
0169     }
0170 }
0171 
0172 STD_SETTER_CMD_IMPL_S(Image, SetKeepRatio, bool, keepRatio)
0173 void Image::setKeepRatio(bool keepRatio) {
0174     Q_D(Image);
0175     if (keepRatio != d->keepRatio)
0176         exec(new ImageSetKeepRatioCmd(d, keepRatio, ki18n("%1: change keep ratio")));
0177 }
0178 
0179 // ##############################################################################
0180 // ####################### Private implementation ###############################
0181 // ##############################################################################
0182 ImagePrivate::ImagePrivate(Image* owner)
0183     : WorksheetElementPrivate(owner)
0184     , q(owner) {
0185     setFlag(QGraphicsItem::ItemIsSelectable);
0186     setFlag(QGraphicsItem::ItemIsMovable);
0187     setFlag(QGraphicsItem::ItemSendsGeometryChanges);
0188     setFlag(QGraphicsItem::ItemIsFocusable);
0189     setAcceptHoverEvents(true);
0190 
0191     // initial placeholder image
0192     image = QIcon::fromTheme(QStringLiteral("viewimage")).pixmap(width, height).toImage();
0193     imageScaled = image;
0194 }
0195 
0196 /*!
0197     calculates the position and the bounding box of the label. Called on geometry or text changes.
0198  */
0199 void ImagePrivate::retransform() {
0200     const bool suppress = suppressRetransform || q->isLoading();
0201     trackRetransformCalled(suppress);
0202     if (suppress)
0203         return;
0204 
0205     int w = imageScaled.width();
0206     int h = imageScaled.height();
0207     m_boundingRectangle.setX(-w / 2);
0208     m_boundingRectangle.setY(-h / 2);
0209     m_boundingRectangle.setWidth(w);
0210     m_boundingRectangle.setHeight(h);
0211 
0212     updatePosition(); // needed, because CartesianPlot calls retransform if some operations are done
0213     updateBorder();
0214 }
0215 
0216 void ImagePrivate::updateImage() {
0217     if (!fileName.isEmpty()) {
0218         image = QImage(fileName);
0219         width = image.width();
0220         height = image.height();
0221     } else {
0222         width = Worksheet::convertToSceneUnits(2, Worksheet::Unit::Centimeter);
0223         height = Worksheet::convertToSceneUnits(3, Worksheet::Unit::Centimeter);
0224         image = QIcon::fromTheme(QStringLiteral("viewimage")).pixmap(width, height).toImage();
0225     }
0226 
0227     imageScaled = image;
0228 
0229     Q_EMIT q->widthChanged(width);
0230     Q_EMIT q->heightChanged(height);
0231 
0232     retransform();
0233 }
0234 
0235 void ImagePrivate::scaleImage() {
0236     if (keepRatio) {
0237         if (width != imageScaled.width()) {
0238             // width was changed -> rescale the height to keep the ratio
0239             if (imageScaled.width() != 0)
0240                 height = imageScaled.height() * width / imageScaled.width();
0241             else
0242                 height = 0;
0243             Q_EMIT q->heightChanged(height);
0244         } else if (height != imageScaled.height()) {
0245             // height was changed -> rescale the width to keep the ratio
0246             if (imageScaled.height() != 0)
0247                 width = imageScaled.width() * height / imageScaled.height();
0248             else
0249                 width = 0;
0250             Q_EMIT q->widthChanged(width);
0251         }
0252     }
0253 
0254     if (width != 0 && height != 0)
0255         imageScaled = image.scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
0256 
0257     retransform();
0258 }
0259 
0260 void ImagePrivate::updateBorder() {
0261     borderShapePath = QPainterPath();
0262     borderShapePath.addRect(m_boundingRectangle);
0263     recalcShapeAndBoundingRect();
0264 }
0265 
0266 /*!
0267     Returns the outer bounds of the item as a rectangle.
0268  */
0269 QRectF ImagePrivate::boundingRect() const {
0270     return transformedBoundingRectangle;
0271 }
0272 
0273 /*!
0274     Returns the shape of this item as a QPainterPath in local coordinates.
0275 */
0276 QPainterPath ImagePrivate::shape() const {
0277     return imageShape;
0278 }
0279 
0280 /*!
0281   recalculates the outer bounds and the shape of the label.
0282 */
0283 void ImagePrivate::recalcShapeAndBoundingRect() {
0284     prepareGeometryChange();
0285 
0286     QTransform matrix;
0287     imageShape = QPainterPath();
0288     if (borderLine->pen().style() != Qt::NoPen) {
0289         imageShape.addPath(WorksheetElement::shapeFromPath(borderShapePath, borderLine->pen()));
0290         transformedBoundingRectangle = matrix.mapRect(imageShape.boundingRect());
0291     } else {
0292         imageShape.addRect(m_boundingRectangle);
0293         transformedBoundingRectangle = matrix.mapRect(m_boundingRectangle);
0294     }
0295 
0296     imageShape = matrix.map(imageShape);
0297 
0298     Q_EMIT q->changed();
0299 }
0300 
0301 void ImagePrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget*) {
0302     painter->save();
0303 
0304     // draw the image
0305     painter->setOpacity(opacity);
0306     painter->drawImage(m_boundingRectangle.topLeft(), imageScaled, imageScaled.rect());
0307     painter->restore();
0308 
0309     // draw the border
0310     if (borderLine->style() != Qt::NoPen) {
0311         painter->save();
0312         painter->setPen(borderLine->pen());
0313         painter->setBrush(Qt::NoBrush);
0314         painter->setOpacity(borderLine->opacity());
0315         painter->drawPath(borderShapePath);
0316         painter->restore();
0317     }
0318 
0319     const bool selected = isSelected();
0320     const bool hovered = (m_hovered && !selected);
0321     if ((hovered || selected) && !q->isPrinting()) {
0322         static double penWidth = 2.;
0323         const QRectF& br = boundingRect();
0324         const qreal width = br.width();
0325         const qreal height = br.height();
0326         const QRectF rect = QRectF(-width / 2 + penWidth / 2, -height / 2 + penWidth / 2, width - penWidth, height - penWidth);
0327 
0328         if (hovered)
0329             painter->setPen(QPen(QApplication::palette().color(QPalette::Shadow), penWidth));
0330         else
0331             painter->setPen(QPen(QApplication::palette().color(QPalette::Highlight), penWidth));
0332 
0333         painter->drawRect(rect);
0334     }
0335 }
0336 
0337 // ##############################################################################
0338 // ##################  Serialization/Deserialization  ###########################
0339 // ##############################################################################
0340 //! Save as XML
0341 void Image::save(QXmlStreamWriter* writer) const {
0342     Q_D(const Image);
0343 
0344     writer->writeStartElement(QStringLiteral("image"));
0345     writeBasicAttributes(writer);
0346     writeCommentElement(writer);
0347 
0348     // general
0349     writer->writeStartElement(QStringLiteral("general"));
0350     if (d->embedded) {
0351         QFileInfo fi(d->fileName);
0352         writer->writeAttribute(QStringLiteral("fileName"), fi.fileName()); // save the actual file name only and not the whole path
0353     } else
0354         writer->writeAttribute(QStringLiteral("fileName"), d->fileName);
0355 
0356     writer->writeAttribute(QStringLiteral("embedded"), QString::number(d->embedded));
0357     writer->writeAttribute(QStringLiteral("opacity"), QString::number(d->opacity));
0358     writer->writeEndElement();
0359 
0360     // image data
0361     if (d->embedded && !d->image.isNull()) {
0362         writer->writeStartElement(QStringLiteral("data"));
0363         QByteArray data;
0364         QBuffer buffer(&data);
0365         buffer.open(QIODevice::WriteOnly);
0366         d->image.save(&buffer, "PNG");
0367         writer->writeCharacters(QLatin1String(data.toBase64()));
0368         writer->writeEndElement();
0369     }
0370 
0371     // geometry
0372     writer->writeStartElement(QStringLiteral("geometry"));
0373     WorksheetElement::save(writer);
0374     writer->writeAttribute(QStringLiteral("width"), QString::number(d->width));
0375     writer->writeAttribute(QStringLiteral("height"), QString::number(d->height));
0376     writer->writeAttribute(QStringLiteral("keepRatio"), QString::number(d->keepRatio));
0377     writer->writeEndElement();
0378 
0379     // border
0380     d->borderLine->save(writer);
0381 
0382     writer->writeEndElement(); // close "image" section
0383 }
0384 
0385 //! Load from XML
0386 bool Image::load(XmlStreamReader* reader, bool preview) {
0387     if (!readBasicAttributes(reader))
0388         return false;
0389 
0390     Q_D(Image);
0391     QXmlStreamAttributes attribs;
0392     QString str;
0393 
0394     while (!reader->atEnd()) {
0395         reader->readNext();
0396         if (reader->isEndElement() && reader->name() == QLatin1String("image"))
0397             break;
0398 
0399         if (!reader->isStartElement())
0400             continue;
0401 
0402         if (!preview && reader->name() == QLatin1String("comment")) {
0403             if (!readCommentElement(reader))
0404                 return false;
0405         } else if (!preview && reader->name() == QLatin1String("general")) {
0406             attribs = reader->attributes();
0407             d->fileName = attribs.value(QStringLiteral("fileName")).toString();
0408             READ_INT_VALUE("embedded", embedded, bool);
0409             READ_DOUBLE_VALUE("opacity", opacity);
0410         } else if (reader->name() == QLatin1String("data")) {
0411             QByteArray ba = QByteArray::fromBase64(reader->readElementText().toLatin1());
0412             if (!d->image.loadFromData(ba))
0413                 reader->raiseWarning(i18n("Failed to read image data"));
0414         } else if (!preview && reader->name() == QLatin1String("geometry")) {
0415             attribs = reader->attributes();
0416 
0417             READ_INT_VALUE("width", width, int);
0418             READ_INT_VALUE("height", height, int);
0419             READ_INT_VALUE("keepRatio", keepRatio, bool);
0420 
0421             WorksheetElement::load(reader, preview);
0422         } else if (!preview && reader->name() == QLatin1String("border")) {
0423             d->borderLine->load(reader, preview);
0424         } else { // unknown element
0425             reader->raiseUnknownElementWarning();
0426             if (!reader->skipToEndElement())
0427                 return false;
0428         }
0429     }
0430 
0431     if (!preview) {
0432         if (!d->embedded)
0433             d->image = QImage(d->fileName);
0434         d->imageScaled = d->image.scaled(d->width, d->height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
0435     }
0436 
0437     return true;
0438 }
0439 
0440 // ##############################################################################
0441 // #########################  Theme management ##################################
0442 // ##############################################################################
0443 void Image::loadThemeConfig(const KConfig& config) {
0444     Q_D(Image);
0445     const auto& group = config.group(QStringLiteral("CartesianPlot"));
0446     d->borderLine->loadThemeConfig(group);
0447 }
0448 
0449 void Image::saveThemeConfig(const KConfig& config) {
0450     Q_D(Image);
0451     KConfigGroup group = config.group(QStringLiteral("Image"));
0452     d->borderLine->saveThemeConfig(group);
0453 }