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 }