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"