File indexing completed on 2024-05-19 05:21:44

0001 /*
0002    SPDX-FileCopyrightText: 2015-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "richtextcomposerimages.h"
0008 #include "richtextcomposer.h"
0009 
0010 #include <KCodecs>
0011 #include <KLocalizedString>
0012 #include <KMessageBox>
0013 #include <QBuffer>
0014 #include <QRandomGenerator>
0015 #include <QTextBlock>
0016 #include <QTextDocument>
0017 
0018 using namespace KPIMTextEdit;
0019 
0020 class Q_DECL_HIDDEN RichTextComposerImages::RichTextComposerImagesPrivate
0021 {
0022 public:
0023     RichTextComposerImagesPrivate(RichTextComposer *editor)
0024         : composer(editor)
0025     {
0026     }
0027 
0028     /**
0029      * The names of embedded images.
0030      * Used to easily obtain the names of the images.
0031      * New images are compared to the list and not added as resource if already present.
0032      */
0033     QStringList mImageNames;
0034 
0035     RichTextComposer *const composer;
0036 };
0037 
0038 RichTextComposerImages::RichTextComposerImages(RichTextComposer *composer, QObject *parent)
0039     : QObject(parent)
0040     , d(new RichTextComposerImages::RichTextComposerImagesPrivate(composer))
0041 {
0042 }
0043 
0044 RichTextComposerImages::~RichTextComposerImages() = default;
0045 
0046 void RichTextComposerImages::addImage(const QUrl &url, int width, int height)
0047 {
0048     addImageHelper(url, width, height);
0049 }
0050 
0051 void RichTextComposerImages::addImageHelper(const QUrl &url, int width, int height)
0052 {
0053     QImage image;
0054     if (!image.load(url.path())) {
0055         KMessageBox::error(d->composer, xi18nc("@info", "Unable to load image <filename>%1</filename>.", url.path()));
0056         return;
0057     }
0058     const QFileInfo fi(url.path());
0059     const QString imageName = fi.baseName().isEmpty() ? QStringLiteral("image.png") : QString(fi.baseName() + QLatin1StringView(".png"));
0060     if (width != -1 && height != -1 && (image.width() > width && image.height() > height)) {
0061         image = image.scaled(width, height);
0062     }
0063     addImageHelper(imageName, image, width, height);
0064 }
0065 
0066 void RichTextComposerImages::loadImage(const QImage &image, const QString &matchName, const QString &resourceName)
0067 {
0068     QSet<int> cursorPositionsToSkip;
0069     QTextBlock currentBlock = d->composer->document()->begin();
0070     QTextBlock::iterator it;
0071     while (currentBlock.isValid()) {
0072         for (it = currentBlock.begin(); !it.atEnd(); ++it) {
0073             QTextFragment fragment = it.fragment();
0074             if (fragment.isValid()) {
0075                 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
0076                 if (imageFormat.isValid() && imageFormat.name() == matchName) {
0077                     int pos = fragment.position();
0078                     if (!cursorPositionsToSkip.contains(pos)) {
0079                         QTextCursor cursor(d->composer->document());
0080                         cursor.setPosition(pos);
0081                         cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
0082                         cursor.removeSelectedText();
0083                         d->composer->document()->addResource(QTextDocument::ImageResource, QUrl(resourceName), QVariant(image));
0084                         QTextImageFormat format;
0085                         format.setName(resourceName);
0086                         if ((imageFormat.width() != 0.0) && (imageFormat.height() != 0.0)) {
0087                             format.setWidth(imageFormat.width());
0088                             format.setHeight(imageFormat.height());
0089                         }
0090                         cursor.insertImage(format);
0091 
0092                         // The textfragment iterator is now invalid, restart from the beginning
0093                         // Take care not to replace the same fragment again, or we would be in
0094                         // an infinite loop.
0095                         cursorPositionsToSkip.insert(pos);
0096                         it = currentBlock.begin();
0097                     }
0098                 }
0099             }
0100         }
0101         currentBlock = currentBlock.next();
0102     }
0103 }
0104 
0105 void RichTextComposerImages::addImageHelper(const QString &imageName, const QImage &image, int width, int height)
0106 {
0107     QString imageNameToAdd = imageName;
0108     QTextDocument *document = d->composer->document();
0109 
0110     // determine the imageNameToAdd
0111     int imageNumber = 1;
0112     while (d->mImageNames.contains(imageNameToAdd)) {
0113         QVariant qv = document->resource(QTextDocument::ImageResource, QUrl(imageNameToAdd));
0114         if (qv == image) {
0115             // use the same name
0116             break;
0117         }
0118         const int firstDot = imageName.indexOf(QLatin1Char('.'));
0119         if (firstDot == -1) {
0120             imageNameToAdd = imageName + QString::number(imageNumber++);
0121         } else {
0122             imageNameToAdd = imageName.left(firstDot) + QString::number(imageNumber++) + imageName.mid(firstDot);
0123         }
0124     }
0125 
0126     if (!d->mImageNames.contains(imageNameToAdd)) {
0127         document->addResource(QTextDocument::ImageResource, QUrl(imageNameToAdd), image);
0128         d->mImageNames << imageNameToAdd;
0129     }
0130     if (width != -1 && height != -1) {
0131         QTextImageFormat format;
0132         format.setName(imageNameToAdd);
0133         format.setWidth(width);
0134         format.setHeight(height);
0135         d->composer->textCursor().insertImage(format);
0136     } else {
0137         d->composer->textCursor().insertImage(imageNameToAdd);
0138     }
0139     d->composer->activateRichText();
0140 }
0141 
0142 ImageWithNameList RichTextComposerImages::imagesWithName() const
0143 {
0144     ImageWithNameList retImages;
0145     QStringList seenImageNames;
0146     const QList<QTextImageFormat> imageFormats = embeddedImageFormats();
0147     for (const QTextImageFormat &imageFormat : imageFormats) {
0148         const QString name = imageFormat.name();
0149         if (!seenImageNames.contains(name)) {
0150             QVariant resourceData = d->composer->document()->resource(QTextDocument::ImageResource, QUrl(name));
0151             auto image = qvariant_cast<QImage>(resourceData);
0152 
0153             ImageWithNamePtr newImage(new ImageWithName);
0154             newImage->image = image;
0155             newImage->name = name;
0156             retImages.append(newImage);
0157             seenImageNames.append(name);
0158         }
0159     }
0160     return retImages;
0161 }
0162 
0163 QList<QSharedPointer<EmbeddedImage>> RichTextComposerImages::embeddedImages() const
0164 {
0165     const ImageWithNameList normalImages = imagesWithName();
0166     QList<QSharedPointer<EmbeddedImage>> retImages;
0167     retImages.reserve(normalImages.count());
0168     for (const ImageWithNamePtr &normalImage : normalImages) {
0169         retImages.append(createEmbeddedImage(normalImage->image, normalImage->name));
0170     }
0171     return retImages;
0172 }
0173 
0174 QSharedPointer<EmbeddedImage> RichTextComposerImages::createEmbeddedImage(const QImage &img, const QString &imageName) const
0175 {
0176     QBuffer buffer;
0177     buffer.open(QIODevice::WriteOnly);
0178     img.save(&buffer, "PNG");
0179 
0180     QSharedPointer<EmbeddedImage> embeddedImage(new EmbeddedImage());
0181     embeddedImage->image = KCodecs::Codec::codecForName("base64")->encode(buffer.buffer());
0182     embeddedImage->imageName = imageName;
0183     embeddedImage->contentID = QStringLiteral("%1@KDE").arg(QRandomGenerator::global()->generate64());
0184     return embeddedImage;
0185 }
0186 
0187 QList<QTextImageFormat> RichTextComposerImages::embeddedImageFormats() const
0188 {
0189     QTextDocument *doc = d->composer->document();
0190     QList<QTextImageFormat> retList;
0191 
0192     QTextBlock currentBlock = doc->begin();
0193     while (currentBlock.isValid()) {
0194         for (QTextBlock::iterator it = currentBlock.begin(); !it.atEnd(); ++it) {
0195             QTextFragment fragment = it.fragment();
0196             if (fragment.isValid()) {
0197                 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
0198                 if (imageFormat.isValid()) {
0199                     // TODO: Replace with a way to see if an image is an embedded image or a remote
0200                     const QUrl url(imageFormat.name());
0201                     if (!url.isValid() || !url.scheme().startsWith(QLatin1StringView("http"))) {
0202                         retList.append(imageFormat);
0203                     }
0204                 }
0205             }
0206         }
0207         currentBlock = currentBlock.next();
0208     }
0209     return retList;
0210 }
0211 
0212 void RichTextComposerImages::insertImage(const QImage &image, const QFileInfo &fileInfo)
0213 {
0214     const QString imageName = fileInfo.baseName().isEmpty() ? i18nc("Start of the filename for an image", "image") : fileInfo.baseName();
0215     addImageHelper(imageName, image);
0216 }
0217 
0218 QByteArray RichTextComposerImages::imageNamesToContentIds(const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList)
0219 {
0220     QByteArray result = htmlBody;
0221     for (const QSharedPointer<EmbeddedImage> &image : imageList) {
0222         const QString newImageName = QLatin1StringView("cid:") + image->contentID;
0223         QByteArray quote("\"");
0224         result.replace(QByteArray(quote + image->imageName.toLocal8Bit() + quote), QByteArray(quote + newImageName.toLocal8Bit() + quote));
0225     }
0226     return result;
0227 }
0228 
0229 #include "moc_richtextcomposerimages.cpp"