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"
8#include "richtextcomposer.h"
9
10#include <KCodecs>
11#include <KLocalizedString>
12#include <KMessageBox>
13#include <QBuffer>
14#include <QRandomGenerator>
15#include <QTextBlock>
16#include <QTextDocument>
17
18using namespace KPIMTextEdit;
19
20class Q_DECL_HIDDEN RichTextComposerImages::RichTextComposerImagesPrivate
21{
22public:
23 RichTextComposerImagesPrivate(RichTextComposer *editor)
24 : composer(editor)
25 {
26 }
27
28 /**
29 * The names of embedded images.
30 * Used to easily obtain the names of the images.
31 * New images are compared to the list and not added as resource if already present.
32 */
33 QStringList mImageNames;
34
35 RichTextComposer *const composer;
36};
37
38RichTextComposerImages::RichTextComposerImages(RichTextComposer *composer, QObject *parent)
39 : QObject(parent)
40 , d(new RichTextComposerImages::RichTextComposerImagesPrivate(composer))
41{
42}
43
44RichTextComposerImages::~RichTextComposerImages() = default;
45
46void RichTextComposerImages::addImage(const QUrl &url, int width, int height)
47{
48 addImageHelper(url, width, height);
49}
50
51void RichTextComposerImages::addImageHelper(const QUrl &url, int width, int height)
52{
53 QImage image;
54 if (!image.load(url.path())) {
55 KMessageBox::error(d->composer, xi18nc("@info", "Unable to load image <filename>%1</filename>.", url.path()));
56 return;
57 }
58 const QFileInfo fi(url.path());
59 const QString imageName = fi.baseName().isEmpty() ? QStringLiteral("image.png") : QString(fi.baseName() + QLatin1StringView(".png"));
60 if (width != -1 && height != -1 && (image.width() > width && image.height() > height)) {
61 image = image.scaled(width, height);
62 }
63 addImageHelper(imageName, image, width, height);
64}
65
66void RichTextComposerImages::loadImage(const QImage &image, const QString &matchName, const QString &resourceName)
67{
69 QTextBlock currentBlock = d->composer->document()->begin();
71 while (currentBlock.isValid()) {
72 for (it = currentBlock.begin(); !it.atEnd(); ++it) {
73 QTextFragment fragment = it.fragment();
74 if (fragment.isValid()) {
75 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
76 if (imageFormat.isValid() && imageFormat.name() == matchName) {
77 int pos = fragment.position();
78 if (!cursorPositionsToSkip.contains(pos)) {
79 QTextCursor cursor(d->composer->document());
80 cursor.setPosition(pos);
81 cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
82 cursor.removeSelectedText();
83 d->composer->document()->addResource(QTextDocument::ImageResource, QUrl(resourceName), QVariant(image));
84 QTextImageFormat format;
85 format.setName(resourceName);
86 if ((imageFormat.width() != 0.0) && (imageFormat.height() != 0.0)) {
87 format.setWidth(imageFormat.width());
88 format.setHeight(imageFormat.height());
89 }
90 cursor.insertImage(format);
91
92 // The textfragment iterator is now invalid, restart from the beginning
93 // Take care not to replace the same fragment again, or we would be in
94 // an infinite loop.
95 cursorPositionsToSkip.insert(pos);
96 it = currentBlock.begin();
97 }
98 }
99 }
100 }
101 currentBlock = currentBlock.next();
102 }
103}
104
105void RichTextComposerImages::addImageHelper(const QString &imageName, const QImage &image, int width, int height)
106{
107 QString imageNameToAdd = imageName;
108 QTextDocument *document = d->composer->document();
109
110 // determine the imageNameToAdd
111 int imageNumber = 1;
112 while (d->mImageNames.contains(imageNameToAdd)) {
114 if (qv == image) {
115 // use the same name
116 break;
117 }
118 const int firstDot = imageName.indexOf(QLatin1Char('.'));
119 if (firstDot == -1) {
121 } else {
122 imageNameToAdd = imageName.left(firstDot) + QString::number(imageNumber++) + imageName.mid(firstDot);
123 }
124 }
125
126 if (!d->mImageNames.contains(imageNameToAdd)) {
128 d->mImageNames << imageNameToAdd;
129 }
130 if (width != -1 && height != -1) {
131 QTextImageFormat format;
132 format.setName(imageNameToAdd);
133 format.setWidth(width);
134 format.setHeight(height);
135 d->composer->textCursor().insertImage(format);
136 } else {
137 d->composer->textCursor().insertImage(imageNameToAdd);
138 }
139 d->composer->activateRichText();
140}
141
142ImageWithNameList RichTextComposerImages::imagesWithName() const
143{
146 const QList<QTextImageFormat> imageFormats = embeddedImageFormats();
147 for (const QTextImageFormat &imageFormat : imageFormats) {
148 const QString name = imageFormat.name();
149 if (!seenImageNames.contains(name)) {
150 QVariant resourceData = d->composer->document()->resource(QTextDocument::ImageResource, QUrl(name));
152
154 newImage->image = image;
155 newImage->name = name;
156 retImages.append(newImage);
157 seenImageNames.append(name);
158 }
159 }
160 return retImages;
161}
162
163QList<QSharedPointer<EmbeddedImage>> RichTextComposerImages::embeddedImages() const
164{
165 const ImageWithNameList normalImages = imagesWithName();
167 retImages.reserve(normalImages.count());
169 retImages.append(createEmbeddedImage(normalImage->image, normalImage->name));
170 }
171 return retImages;
172}
173
174QSharedPointer<EmbeddedImage> RichTextComposerImages::createEmbeddedImage(const QImage &img, const QString &imageName) const
175{
176 QBuffer buffer;
178 img.save(&buffer, "PNG");
179
181 embeddedImage->image = KCodecs::Codec::codecForName("base64")->encode(buffer.buffer());
182 embeddedImage->imageName = imageName;
183 embeddedImage->contentID = QStringLiteral("%1@KDE").arg(QRandomGenerator::global()->generate64());
184 return embeddedImage;
185}
186
187QList<QTextImageFormat> RichTextComposerImages::embeddedImageFormats() const
188{
189 QTextDocument *doc = d->composer->document();
191
192 QTextBlock currentBlock = doc->begin();
193 while (currentBlock.isValid()) {
194 for (QTextBlock::iterator it = currentBlock.begin(); !it.atEnd(); ++it) {
195 QTextFragment fragment = it.fragment();
196 if (fragment.isValid()) {
197 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
198 if (imageFormat.isValid()) {
199 // TODO: Replace with a way to see if an image is an embedded image or a remote
200 const QUrl url(imageFormat.name());
201 if (!url.isValid() || !url.scheme().startsWith(QLatin1StringView("http"))) {
202 retList.append(imageFormat);
203 }
204 }
205 }
206 }
207 currentBlock = currentBlock.next();
208 }
209 return retList;
210}
211
212void RichTextComposerImages::insertImage(const QImage &image, const QFileInfo &fileInfo)
213{
214 const QString imageName = fileInfo.baseName().isEmpty() ? i18nc("Start of the filename for an image", "image") : fileInfo.baseName();
215 addImageHelper(imageName, image);
216}
217
218QByteArray RichTextComposerImages::imageNamesToContentIds(const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList)
219{
220 QByteArray result = htmlBody;
221 for (const QSharedPointer<EmbeddedImage> &image : imageList) {
222 const QString newImageName = QLatin1StringView("cid:") + image->contentID;
223 QByteArray quote("\"");
224 result.replace(QByteArray(quote + image->imageName.toLocal8Bit() + quote), QByteArray(quote + newImageName.toLocal8Bit() + quote));
225 }
226 return result;
227}
228
229#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(StandardShortcut 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)
QImage scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
int width() const const
T qobject_cast(QObject *object)
QRandomGenerator * global()
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
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 Tue Mar 26 2024 11:20:45 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.