File indexing completed on 2024-05-12 16:01:53

0001 /*
0002  * SPDX-FileCopyrightText: 2017 Boudewijn Rempt <boud@valdyas.org>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "KisReferenceImage.h"
0008 #include "KoColorSpaceRegistry.h"
0009 
0010 #include <QImage>
0011 #include <QMessageBox>
0012 #include <QPainter>
0013 #include <QApplication>
0014 #include <QClipboard>
0015 #include <QSharedData>
0016 #include <QFileInfo>
0017 #include <QImageReader>
0018 #include <QUrl>
0019 
0020 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
0021 #include <QColorSpace>
0022 #endif
0023 
0024 #include <kundo2command.h>
0025 #include <KoStore.h>
0026 #include <KoStoreDevice.h>
0027 #include <KoTosContainer_p.h>
0028 #include <krita_utils.h>
0029 #include <kis_coordinates_converter.h>
0030 #include <kis_dom_utils.h>
0031 #include <SvgUtil.h>
0032 #include <libs/flake/svg/parsers/SvgTransformParser.h>
0033 #include <libs/brush/kis_qimage_pyramid.h>
0034 
0035 #include <KisDocument.h>
0036 #include <KisPart.h>
0037 
0038 #include "kis_clipboard.h"
0039 
0040 struct KisReferenceImage::Private : public QSharedData
0041 {
0042     // Filename within .kra (for embedding)
0043     QString internalFilename;
0044 
0045     // File on disk (for linking)
0046     QString externalFilename;
0047 
0048     QImage image;
0049     QImage cachedImage;
0050     KisQImagePyramid mipmap;
0051 
0052     qreal saturation{1.0};
0053     int id{-1};
0054     bool embed{true};
0055 
0056     bool loadFromFile() {
0057         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!externalFilename.isEmpty(), false);
0058         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(QFileInfo(externalFilename).exists(), false);
0059         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(QFileInfo(externalFilename).isReadable(), false);
0060         {
0061             QImageReader reader(externalFilename);
0062             reader.setDecideFormatFromContent(true);
0063             image = reader.read();
0064 
0065             if (image.isNull()) {
0066                 reader.setAutoDetectImageFormat(true);
0067                 image = reader.read();
0068             }
0069 
0070         }
0071 
0072         if (image.isNull()) {
0073             image.load(externalFilename);
0074         }
0075 
0076         if (image.isNull()) {
0077             KisDocument * doc = KisPart::instance()->createTemporaryDocument();
0078             if (doc->openPath(externalFilename, KisDocument::DontAddToRecent)) {
0079                 image = doc->image()->convertToQImage(doc->image()->bounds(), 0);
0080             }
0081             KisPart::instance()->removeDocument(doc);
0082         }
0083 
0084         // See https://bugs.kde.org/show_bug.cgi?id=416515 -- a jpeg image
0085         // loaded into a qimage cannot be saved to png unless we explicitly
0086         // convert the colorspace of the QImage
0087 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
0088         image.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
0089 #endif
0090 
0091         return (!image.isNull());
0092     }
0093 
0094     void updateCache() {
0095         if (saturation < 1.0) {
0096             cachedImage = KritaUtils::convertQImageToGrayA(image);
0097 
0098             if (saturation > 0.0) {
0099                 QPainter gc2(&cachedImage);
0100                 gc2.setOpacity(saturation);
0101                 gc2.drawImage(QPoint(), image);
0102             }
0103         } else {
0104             cachedImage = image;
0105         }
0106 
0107         mipmap = KisQImagePyramid(cachedImage, false);
0108     }
0109 };
0110 
0111 
0112 KisReferenceImage::SetSaturationCommand::SetSaturationCommand(const QList<KoShape *> &shapes, qreal newSaturation, KUndo2Command *parent)
0113     : KUndo2Command(kundo2_i18n("Set saturation"), parent)
0114     , newSaturation(newSaturation)
0115 {
0116     images.reserve(shapes.count());
0117 
0118     Q_FOREACH(auto *shape, shapes) {
0119         auto *reference = dynamic_cast<KisReferenceImage*>(shape);
0120         KIS_SAFE_ASSERT_RECOVER_BREAK(reference);
0121         images.append(reference);
0122     }
0123 
0124     Q_FOREACH(auto *image, images) {
0125         oldSaturations.append(image->saturation());
0126     }
0127 }
0128 
0129 void KisReferenceImage::SetSaturationCommand::undo()
0130 {
0131     auto saturationIterator = oldSaturations.begin();
0132     Q_FOREACH(auto *image, images) {
0133         image->setSaturation(*saturationIterator);
0134         image->update();
0135         saturationIterator++;
0136     }
0137 }
0138 
0139 void KisReferenceImage::SetSaturationCommand::redo()
0140 {
0141     Q_FOREACH(auto *image, images) {
0142         image->setSaturation(newSaturation);
0143         image->update();
0144     }
0145 }
0146 
0147 KisReferenceImage::KisReferenceImage()
0148     : d(new Private())
0149 {
0150     setKeepAspectRatio(true);
0151 }
0152 
0153 KisReferenceImage::KisReferenceImage(const KisReferenceImage &rhs)
0154     : KoTosContainer(rhs)
0155     , d(rhs.d)
0156 {}
0157 
0158 KisReferenceImage::~KisReferenceImage()
0159 {}
0160 
0161 KisReferenceImage * KisReferenceImage::fromFile(const QString &filename, const KisCoordinatesConverter &converter, QWidget *parent)
0162 {
0163     KisReferenceImage *reference = new KisReferenceImage();
0164     reference->d->externalFilename = filename;
0165     bool ok = reference->d->loadFromFile();
0166 
0167     if (ok) {
0168         QRect r = QRect(QPoint(), reference->d->image.size());
0169         QSizeF shapeSize = converter.imageToDocument(r).size();
0170         reference->setSize(shapeSize);
0171     } else {
0172         delete reference;
0173 
0174         if (parent) {
0175             QMessageBox::critical(parent, i18nc("@title:window", "Krita"), i18n("Could not load %1.", filename));
0176         }
0177 
0178         return nullptr;
0179     }
0180 
0181     return reference;
0182 }
0183 
0184 KisReferenceImage *KisReferenceImage::fromClipboard(const KisCoordinatesConverter &converter)
0185 {
0186     const auto sz = KisClipboard::instance()->clipSize();
0187     KisPaintDeviceSP clip = KisClipboard::instance()->clip({0, 0, sz.width(), sz.height()}, true);
0188     return fromPaintDevice(clip, converter, nullptr);
0189 }
0190 
0191 KisReferenceImage *
0192 KisReferenceImage::fromPaintDevice(KisPaintDeviceSP src, const KisCoordinatesConverter &converter, QWidget *)
0193 {
0194     if (!src) {
0195         return nullptr;
0196     }
0197 
0198     auto *reference = new KisReferenceImage();
0199     reference->d->image = src->convertToQImage(KoColorSpaceRegistry::instance()->p709SRGBProfile());
0200 
0201     QRect r = QRect(QPoint(), reference->d->image.size());
0202     QSizeF size = converter.imageToDocument(r).size();
0203     reference->setSize(size);
0204 
0205     return reference;
0206 }
0207 
0208 void KisReferenceImage::paint(QPainter &gc) const
0209 {
0210     if (!parent()) return;
0211 
0212     gc.save();
0213 
0214     QSizeF shapeSize = size();
0215     // scale and rotation done by the user (excluding zoom)
0216     QTransform transform = QTransform::fromScale(shapeSize.width() / d->image.width(), shapeSize.height() / d->image.height());
0217 
0218     if (d->cachedImage.isNull()) {
0219         // detach the data
0220         const_cast<KisReferenceImage*>(this)->d->updateCache();
0221     }
0222 
0223     qreal scale;
0224     // scale from the highDPI display
0225     QTransform devicePixelRatioFTransform = QTransform::fromScale(gc.device()->devicePixelRatioF(), gc.device()->devicePixelRatioF());
0226     // all three transformations: scale and rotation done by the user, scale from highDPI display, and zoom + rotation of the view
0227     // order: zoom/rotation of the view; scale to high res; scale and rotation done by the user
0228     QImage prescaled = d->mipmap.getClosestWithoutWorkaroundBorder(transform * devicePixelRatioFTransform * gc.transform(), &scale);
0229     transform.scale(1.0 / scale, 1.0 / scale);
0230 
0231     if (scale > 1.0) {
0232         // enlarging should be done without smooth transformation
0233         // so the user can see pixels just as they are painted
0234         gc.setRenderHints(QPainter::Antialiasing);
0235     } else {
0236         gc.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
0237     }
0238     gc.setClipRect(QRectF(QPointF(), shapeSize), Qt::IntersectClip);
0239     gc.setTransform(transform, true);
0240     gc.drawImage(QPoint(), prescaled);
0241 
0242     gc.restore();
0243 }
0244 
0245 void KisReferenceImage::setSaturation(qreal saturation)
0246 {
0247     d->saturation = saturation;
0248     d->cachedImage = QImage();
0249 }
0250 
0251 qreal KisReferenceImage::saturation() const
0252 {
0253     return d->saturation;
0254 }
0255 
0256 void KisReferenceImage::setEmbed(bool embed)
0257 {
0258     KIS_SAFE_ASSERT_RECOVER_RETURN(embed || !d->externalFilename.isEmpty());
0259     d->embed = embed;
0260 }
0261 
0262 bool KisReferenceImage::embed()
0263 {
0264     return d->embed;
0265 }
0266 
0267 bool KisReferenceImage::hasLocalFile()
0268 {
0269     return !d->externalFilename.isEmpty();
0270 }
0271 
0272 QString KisReferenceImage::filename() const
0273 {
0274     return d->externalFilename;
0275 }
0276 
0277 QString KisReferenceImage::internalFile() const
0278 {
0279     return d->internalFilename;
0280 }
0281 
0282 
0283 void KisReferenceImage::setFilename(const QString &filename)
0284 {
0285     d->externalFilename = filename;
0286     d->embed = false;
0287 }
0288 
0289 QColor KisReferenceImage::getPixel(QPointF position)
0290 {
0291     if (transparency() == 1.0) return Qt::transparent;
0292 
0293     const QSizeF shapeSize = size();
0294     const QTransform scale = QTransform::fromScale(d->image.width() / shapeSize.width(), d->image.height() / shapeSize.height());
0295 
0296     const QTransform transform = absoluteTransformation().inverted() * scale;
0297     const QPointF localPosition = position * transform;
0298 
0299     if (d->cachedImage.isNull()) {
0300         d->updateCache();
0301     }
0302 
0303     return d->cachedImage.pixelColor(localPosition.toPoint());
0304 }
0305 
0306 void KisReferenceImage::saveXml(QDomDocument &document, QDomElement &parentElement, int id)
0307 {
0308     d->id = id;
0309 
0310     QDomElement element = document.createElement("referenceimage");
0311 
0312     if (d->embed) {
0313         d->internalFilename = QString("reference_images/%1.png").arg(id);
0314     }
0315     
0316     const QString src = d->embed ? d->internalFilename : (QString("file://") + d->externalFilename);
0317     element.setAttribute("src", src);
0318 
0319     const QSizeF &shapeSize = size();
0320     element.setAttribute("width", KisDomUtils::toString(shapeSize.width()));
0321     element.setAttribute("height", KisDomUtils::toString(shapeSize.height()));
0322     element.setAttribute("keepAspectRatio", keepAspectRatio() ? "true" : "false");
0323     element.setAttribute("transform", SvgUtil::transformToString(transform()));
0324 
0325     element.setAttribute("opacity", KisDomUtils::toString(1.0 - transparency()));
0326     element.setAttribute("saturation", KisDomUtils::toString(d->saturation));
0327 
0328     parentElement.appendChild(element);
0329 }
0330 
0331 KisReferenceImage * KisReferenceImage::fromXml(const QDomElement &elem)
0332 {
0333     auto *reference = new KisReferenceImage();
0334 
0335     const QString &src = elem.attribute("src");
0336 
0337     if (src.startsWith("file://")) {
0338         reference->d->externalFilename = src.mid(7);
0339         reference->d->embed = false;
0340     } else {
0341         reference->d->internalFilename = src;
0342         reference->d->embed = true;
0343     }
0344 
0345     qreal width = KisDomUtils::toDouble(elem.attribute("width", "100"));
0346     qreal height = KisDomUtils::toDouble(elem.attribute("height", "100"));
0347     reference->setSize(QSizeF(width, height));
0348     reference->setKeepAspectRatio(elem.attribute("keepAspectRatio", "true").toLower() == "true");
0349 
0350     auto transform = SvgTransformParser(elem.attribute("transform")).transform();
0351     reference->setTransformation(transform);
0352 
0353     qreal opacity = KisDomUtils::toDouble(elem.attribute("opacity", "1"));
0354     reference->setTransparency(1.0 - opacity);
0355 
0356     qreal saturation = KisDomUtils::toDouble(elem.attribute("saturation", "1"));
0357     reference->setSaturation(saturation);
0358 
0359     return reference;
0360 }
0361 
0362 bool KisReferenceImage::saveImage(KoStore *store) const
0363 {
0364     if (!d->embed) return true;
0365 
0366     if (!store->open(d->internalFilename)) {
0367         return false;
0368     }
0369 
0370     bool saved = false;
0371 
0372     KoStoreDevice storeDev(store);
0373     if (storeDev.open(QIODevice::WriteOnly)) {
0374         saved = d->image.save(&storeDev, "PNG");
0375     }
0376 
0377     return store->close() && saved;
0378 }
0379 
0380 bool KisReferenceImage::loadImage(KoStore *store)
0381 {
0382     if (!d->embed) {
0383         return d->loadFromFile();
0384     }
0385 
0386     if (!store->open(d->internalFilename)) {
0387         return false;
0388     }
0389 
0390     KoStoreDevice storeDev(store);
0391     if (!storeDev.open(QIODevice::ReadOnly)) {
0392         return false;
0393     }
0394 
0395     if (!d->image.load(&storeDev, "PNG")) {
0396         return false;
0397     }
0398 
0399     return store->close();
0400 }
0401 
0402 QImage KisReferenceImage::getImage()
0403 {
0404     return d->image;
0405 }
0406 
0407 KoShape *KisReferenceImage::cloneShape() const
0408 {
0409     return new KisReferenceImage(*this);
0410 }