KPimTextEdit

richtextcomposerimages.cpp
1/*
2 SPDX-FileCopyrightText: 2015-2024 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "richtextcomposerimages.h"
8using namespace Qt::Literals::StringLiterals;
9
10#include "richtextcomposer.h"
11
12#include <KCodecs>
13#include <KLocalizedString>
14#include <KMessageBox>
15#include <QBuffer>
16#include <QRandomGenerator>
17#include <QTextBlock>
18#include <QTextDocument>
19
20using namespace KPIMTextEdit;
21
22class Q_DECL_HIDDEN RichTextComposerImages::RichTextComposerImagesPrivate
23{
24public:
25 RichTextComposerImagesPrivate(RichTextComposer *editor)
26 : composer(editor)
27 {
28 }
29
30 /**
31 * The names of embedded images.
32 * Used to easily obtain the names of the images.
33 * New images are compared to the list and not added as resource if already present.
34 */
35 QStringList mImageNames;
36
37 RichTextComposer *const composer;
38};
39
40RichTextComposerImages::RichTextComposerImages(RichTextComposer *composer, QObject *parent)
41 : QObject(parent)
42 , d(new RichTextComposerImages::RichTextComposerImagesPrivate(composer))
43{
44}
45
46RichTextComposerImages::~RichTextComposerImages() = default;
47
48void RichTextComposerImages::addImage(const QUrl &url, int width, int height)
49{
50 addImageHelper(url, width, height);
51}
52
53void RichTextComposerImages::addImageHelper(const QUrl &url, int width, int height)
54{
55 QImage image;
56 if (!image.load(url.path())) {
57 KMessageBox::error(d->composer, xi18nc("@info", "Unable to load image <filename>%1</filename>.", url.path()));
58 return;
59 }
60 const QFileInfo fi(url.path());
61 const QString imageName = fi.baseName().isEmpty() ? QStringLiteral("image.png") : QString(fi.baseName() + ".png"_L1);
62 if (width != -1 && height != -1 && (image.width() > width && image.height() > height)) {
63 image = image.scaled(width, height);
64 }
65 addImageHelper(imageName, image, width, height);
66}
67
68void RichTextComposerImages::loadImage(const QImage &image, const QString &matchName, const QString &resourceName)
69{
70 QSet<int> cursorPositionsToSkip;
71 QTextBlock currentBlock = d->composer->document()->begin();
73 while (currentBlock.isValid()) {
74 for (it = currentBlock.begin(); !it.atEnd(); ++it) {
75 QTextFragment fragment = it.fragment();
76 if (fragment.isValid()) {
77 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
78 if (imageFormat.isValid() && imageFormat.name() == matchName) {
79 int pos = fragment.position();
80 if (!cursorPositionsToSkip.contains(pos)) {
81 QTextCursor cursor(d->composer->document());
82 cursor.setPosition(pos);
83 cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
84 cursor.removeSelectedText();
85 d->composer->document()->addResource(QTextDocument::ImageResource, QUrl(resourceName), QVariant(image));
86 QTextImageFormat format;
87 format.setName(resourceName);
88 if ((imageFormat.width() != 0.0) && (imageFormat.height() != 0.0)) {
89 format.setWidth(imageFormat.width());
90 format.setHeight(imageFormat.height());
91 }
92 cursor.insertImage(format);
93
94 // The textfragment iterator is now invalid, restart from the beginning
95 // Take care not to replace the same fragment again, or we would be in
96 // an infinite loop.
97 cursorPositionsToSkip.insert(pos);
98 it = currentBlock.begin();
99 }
100 }
101 }
102 }
103 currentBlock = currentBlock.next();
104 }
105}
106
107void RichTextComposerImages::addImageHelper(const QString &imageName, const QImage &image, int width, int height)
108{
109 QString imageNameToAdd = imageName;
110 QTextDocument *document = d->composer->document();
111
112 // determine the imageNameToAdd
113 int imageNumber = 1;
114 while (d->mImageNames.contains(imageNameToAdd)) {
115 QVariant qv = document->resource(QTextDocument::ImageResource, QUrl(imageNameToAdd));
116 if (qv == image) {
117 // use the same name
118 break;
119 }
120 const int firstDot = imageName.indexOf(QLatin1Char('.'));
121 if (firstDot == -1) {
122 imageNameToAdd = imageName + QString::number(imageNumber++);
123 } else {
124 imageNameToAdd = imageName.left(firstDot) + QString::number(imageNumber++) + imageName.mid(firstDot);
125 }
126 }
127
128 if (!d->mImageNames.contains(imageNameToAdd)) {
129 document->addResource(QTextDocument::ImageResource, QUrl(imageNameToAdd), image);
130 d->mImageNames << imageNameToAdd;
131 }
132 if (width != -1 && height != -1) {
133 QTextImageFormat format;
134 format.setName(imageNameToAdd);
135 format.setWidth(width);
136 format.setHeight(height);
137 d->composer->textCursor().insertImage(format);
138 } else {
139 d->composer->textCursor().insertImage(imageNameToAdd);
140 }
141 d->composer->activateRichText();
142}
143
144ImageWithNameList RichTextComposerImages::imagesWithName() const
145{
146 ImageWithNameList retImages;
147 QStringList seenImageNames;
148 const QList<QTextImageFormat> imageFormats = embeddedImageFormats();
149 for (const QTextImageFormat &imageFormat : imageFormats) {
150 const QString name = imageFormat.name();
151 if (!seenImageNames.contains(name)) {
152 QVariant resourceData = d->composer->document()->resource(QTextDocument::ImageResource, QUrl(name));
153 auto image = qvariant_cast<QImage>(resourceData);
154
155 ImageWithNamePtr newImage(new ImageWithName);
156 newImage->image = image;
157 newImage->name = name;
158 retImages.append(newImage);
159 seenImageNames.append(name);
160 }
161 }
162 return retImages;
163}
164
165QList<QSharedPointer<EmbeddedImage>> RichTextComposerImages::embeddedImages() const
166{
167 const ImageWithNameList normalImages = imagesWithName();
169 retImages.reserve(normalImages.count());
170 for (const ImageWithNamePtr &normalImage : normalImages) {
171 retImages.append(createEmbeddedImage(normalImage->image, normalImage->name));
172 }
173 return retImages;
174}
175
176QSharedPointer<EmbeddedImage> RichTextComposerImages::createEmbeddedImage(const QImage &img, const QString &imageName) const
177{
178 QBuffer buffer;
180 img.save(&buffer, "PNG");
181
182 QSharedPointer<EmbeddedImage> embeddedImage(new EmbeddedImage());
183 embeddedImage->image = KCodecs::Codec::codecForName("base64")->encode(buffer.buffer());
184 embeddedImage->imageName = imageName;
185 embeddedImage->contentID = QStringLiteral("%1@KDE").arg(QRandomGenerator::global()->generate64());
186 return embeddedImage;
187}
188
189QList<QTextImageFormat> RichTextComposerImages::embeddedImageFormats() const
190{
191 QTextDocument *doc = d->composer->document();
193
194 QTextBlock currentBlock = doc->begin();
195 while (currentBlock.isValid()) {
196 for (QTextBlock::iterator it = currentBlock.begin(); !it.atEnd(); ++it) {
197 QTextFragment fragment = it.fragment();
198 if (fragment.isValid()) {
199 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
200 if (imageFormat.isValid()) {
201 // TODO: Replace with a way to see if an image is an embedded image or a remote
202 const QUrl url(imageFormat.name());
203 if (!url.isValid() || !url.scheme().startsWith("http"_L1)) {
204 retList.append(imageFormat);
205 }
206 }
207 }
208 }
209 currentBlock = currentBlock.next();
210 }
211 return retList;
212}
213
214void RichTextComposerImages::insertImage(const QImage &image, const QFileInfo &fileInfo)
215{
216 const QString imageName = fileInfo.baseName().isEmpty() ? i18nc("Start of the filename for an image", "image") : fileInfo.baseName();
217 addImageHelper(imageName, image);
218}
219
220QByteArray RichTextComposerImages::imageNamesToContentIds(const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList)
221{
222 QByteArray result = htmlBody;
223 for (const QSharedPointer<EmbeddedImage> &image : imageList) {
224 const QString newImageName = "cid:"_L1 + image->contentID;
225 QByteArray quote("\"");
226 result.replace(QByteArray(quote + image->imageName.toLocal8Bit() + quote), QByteArray(quote + newImageName.toLocal8Bit() + quote));
227 }
228 return result;
229}
230
231#include "moc_richtextcomposerimages.cpp"
static Codec * codecForName(QByteArrayView name)
virtual bool encode(const char *&scursor, const char *const send, char *&dcursor, const char *const dend, NewlineType newline=NewlineLF) const
The RichTextComposer class.
QString xi18nc(const char *context, const char *text, const TYPE &arg...)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QString name(StandardAction id)
QByteArray & buffer()
virtual bool open(OpenMode flags) override
QByteArray & replace(QByteArrayView before, QByteArrayView after)
QString baseName() const const
int height() const const
bool load(QIODevice *device, const char *format)
bool save(QIODevice *device, const char *format, int quality) const const
int width() const const
void append(QList< T > &&value)
qsizetype count() const const
void reserve(qsizetype size)
QRandomGenerator * global()
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
QString arg(Args &&... args) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString left(qsizetype n) const const
QString mid(qsizetype position, qsizetype n) const const
QString number(double n, char format, int precision)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toLocal8Bit() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
iterator begin() const const
bool isValid() const const
QTextBlock next() const const
void addResource(int type, const QUrl &name, const QVariant &resource)
QTextBlock begin() const const
QVariant resource(int type, const QUrl &name) const const
QTextImageFormat toImageFormat() const const
QTextCharFormat charFormat() const const
bool isValid() const const
int position() const const
qreal height() const const
bool isValid() const const
QString name() const const
void setHeight(qreal height)
void setName(const QString &name)
void setWidth(qreal width)
qreal width() const const
bool isValid() const const
QString path(ComponentFormattingOptions options) const const
QString scheme() const const
Holds information about an embedded HTML image that will be useful for mail clients.
Holds information about an embedded HTML image that will be generally useful.
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:55:51 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.