File indexing completed on 2024-12-22 05:01:10

0001 /*
0002     This file is part of KMail.
0003 
0004     SPDX-FileCopyrightText: 2004 Jakob Schröter <js@camaya.net>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "xfaceconfigurator.h"
0010 #include "encodedimagepicker.h"
0011 #include "ui_xfaceconfigurator.h"
0012 
0013 #include <MessageViewer/KXFace>
0014 
0015 #include <KLocalizedString>
0016 #include <KMessageBox>
0017 
0018 #include <QBuffer>
0019 
0020 using namespace KMail;
0021 using MessageViewer::KXFace;
0022 
0023 // The size of the PNG used in the Face header must be at most 725 bytes, as
0024 // explained here: https://quimby.gnus.org/circus/face/
0025 #define FACE_MAX_SIZE 725
0026 
0027 XFaceConfigurator::XFaceConfigurator(QWidget *parent)
0028     : QWidget(parent)
0029     , mUi(new Ui::XFaceConfigurator)
0030     , mPngquantProc(new QProcess(this))
0031 {
0032     mUi->setupUi(this);
0033 
0034     mPngquantProc->setInputChannelMode(QProcess::ManagedInputChannel);
0035     mPngquantProc->setProgram(QStringLiteral("pngquant"));
0036     mPngquantProc->setArguments(QStringList() << QStringLiteral("--strip") << QStringLiteral("7") << QStringLiteral("-"));
0037 
0038     mUi->faceConfig->setTitle(i18n("Face"));
0039     mUi->xFaceConfig->setTitle(i18n("X-Face"));
0040     mUi->faceConfig->setInfo(i18n("More information under <a href=\"https://quimby.gnus.org/circus/face/\">https://quimby.gnus.org/circus/face/</a>."));
0041     mUi->xFaceConfig->setInfo(i18n("Examples are available at <a href=\"https://ace.home.xs4all.nl/X-Faces/\">https://ace.home.xs4all.nl/X-Faces/</a>."));
0042 
0043     connect(mUi->enableComboBox, &QComboBox::currentIndexChanged, this, &XFaceConfigurator::modeChanged);
0044     connect(mUi->faceConfig, &EncodedImagePicker::imageSelected, this, &XFaceConfigurator::compressFace);
0045     connect(mUi->xFaceConfig, &EncodedImagePicker::imageSelected, this, &XFaceConfigurator::compressXFace);
0046     connect(mUi->faceConfig, &EncodedImagePicker::sourceChanged, this, &XFaceConfigurator::updateFace);
0047     connect(mUi->xFaceConfig, &EncodedImagePicker::sourceChanged, this, &XFaceConfigurator::updateXFace);
0048     connect(mPngquantProc, &QProcess::finished, this, &XFaceConfigurator::pngquantFinished);
0049 
0050     // set initial state
0051     modeChanged(mUi->enableComboBox->currentIndex());
0052 }
0053 
0054 XFaceConfigurator::~XFaceConfigurator() = default;
0055 
0056 bool XFaceConfigurator::isXFaceEnabled() const
0057 {
0058     return mUi->enableComboBox->currentIndex() & SendXFace;
0059 }
0060 
0061 void XFaceConfigurator::setXFaceEnabled(bool enable)
0062 {
0063     const int currentIndex = mUi->enableComboBox->currentIndex();
0064 
0065     if (enable) {
0066         mUi->enableComboBox->setCurrentIndex(currentIndex | SendXFace);
0067     } else {
0068         mUi->enableComboBox->setCurrentIndex(currentIndex & ~SendXFace);
0069     }
0070 }
0071 
0072 bool XFaceConfigurator::isFaceEnabled() const
0073 {
0074     return mUi->enableComboBox->currentIndex() & SendFace;
0075 }
0076 
0077 void XFaceConfigurator::setFaceEnabled(bool enable)
0078 {
0079     const int currentIndex = mUi->enableComboBox->currentIndex();
0080 
0081     if (enable) {
0082         mUi->enableComboBox->setCurrentIndex(currentIndex | SendFace);
0083     } else {
0084         mUi->enableComboBox->setCurrentIndex(currentIndex & ~SendFace);
0085     }
0086 }
0087 
0088 QString XFaceConfigurator::xface() const
0089 {
0090     QString str = mUi->xFaceConfig->source().trimmed();
0091     str.remove(QLatin1StringView("x-face:"), Qt::CaseInsensitive);
0092     str = str.trimmed();
0093 
0094     return str;
0095 }
0096 
0097 void XFaceConfigurator::setXFace(const QString &text)
0098 {
0099     mUi->xFaceConfig->setSource(text);
0100 }
0101 
0102 QString XFaceConfigurator::face() const
0103 {
0104     QString str = mUi->faceConfig->source().trimmed();
0105     str.remove(QLatin1StringView("face:"), Qt::CaseInsensitive);
0106     str = str.trimmed();
0107 
0108     return str;
0109 }
0110 
0111 void XFaceConfigurator::setFace(const QString &text)
0112 {
0113     mUi->faceConfig->setSource(text);
0114 }
0115 
0116 void XFaceConfigurator::modeChanged(int index)
0117 {
0118     mUi->faceConfig->setEnabled(index & SendFace);
0119     mUi->xFaceConfig->setEnabled(index & SendXFace);
0120 
0121     switch (index) {
0122     case DontSend:
0123         mUi->modeInfo->setText(i18n("No image will be sent."));
0124         break;
0125     case SendFace:
0126         mUi->modeInfo->setText(i18n("KMail will send a colored image through the Face header."));
0127         break;
0128     case SendXFace:
0129         mUi->modeInfo->setText(i18n("KMail will send a black-and-white image through the X-Face header."));
0130         break;
0131     case SendBoth:
0132         mUi->modeInfo->setText(i18n("KMail will send both a colored and a black-and-white image."));
0133         break;
0134     }
0135 }
0136 
0137 void XFaceConfigurator::updateFace()
0138 {
0139     const QString str = face();
0140     const QByteArray facearray = QByteArray::fromBase64(str.toUtf8());
0141     QImage faceimage;
0142 
0143     faceimage.loadFromData(facearray, "png");
0144     mUi->faceConfig->setImage(faceimage);
0145 }
0146 
0147 void XFaceConfigurator::updateXFace()
0148 {
0149     const QString str = xface();
0150 
0151     if (str.isEmpty()) {
0152         mUi->xFaceConfig->setImage(QImage());
0153     } else {
0154         KXFace xf;
0155         mUi->xFaceConfig->setImage(xf.toImage(str));
0156     }
0157 }
0158 
0159 void XFaceConfigurator::compressFace(const QImage &image)
0160 {
0161     if (!pngquant(image)) {
0162         crunch(image);
0163     }
0164 }
0165 
0166 void XFaceConfigurator::compressFaceDone(const QByteArray &data, bool fromPngquant)
0167 {
0168     if (data.isNull()) {
0169         if (fromPngquant) {
0170             KMessageBox::error(this, i18n("Failed to reduce image size to fit in header."));
0171         } else {
0172             KMessageBox::error(this, i18n("Failed to reduce image size to fit in header. Install pngquant to obtain better compression results."));
0173         }
0174         return;
0175     }
0176 
0177     mUi->faceConfig->setSource(QString::fromUtf8(data.toBase64()));
0178 
0179     if (!fromPngquant) {
0180         KMessageBox::information(this, i18n("Install pngquant to obtain better image quality."));
0181     }
0182 }
0183 
0184 void XFaceConfigurator::compressXFace(const QImage &image)
0185 {
0186     KXFace xf;
0187     const QString xFaceString = xf.fromImage(image);
0188     mUi->xFaceConfig->setSource(xFaceString);
0189 }
0190 
0191 // The builtin image compressor. It's pretty bad and pngquant is preferred when
0192 // available.
0193 void XFaceConfigurator::crunch(const QImage &image)
0194 {
0195     QImage output;
0196     QByteArray ba;
0197     int crunchLevel = 0;
0198     int maxCrunchLevel = 6 * 5; // 6 formats, 5 sizes
0199 
0200     const QImage::Format formats[6] = {
0201         QImage::Format_RGB32,
0202         QImage::Format_RGB888,
0203         QImage::Format_RGB16,
0204         QImage::Format_RGB666,
0205         QImage::Format_RGB555,
0206         QImage::Format_RGB444,
0207     };
0208 
0209     int sizes[5] = {48, 24, 12, 6, 3};
0210 
0211     while (true) {
0212         const QImage::Format targetFormat = formats[crunchLevel % 6];
0213         int targetSize = sizes[crunchLevel / 6];
0214         output = image;
0215 
0216         if (targetSize != 48) {
0217             output = output.scaled(targetSize, targetSize);
0218         }
0219 
0220         output = output.scaled(48, 48);
0221         ba.clear();
0222         QBuffer buffer(&ba);
0223 
0224         buffer.open(QIODevice::WriteOnly);
0225         output.convertTo(targetFormat);
0226         output.save(&buffer, "PNG", 0);
0227 
0228         if (ba.size() <= FACE_MAX_SIZE) {
0229             compressFaceDone(ba, false);
0230             break;
0231         } else if (crunchLevel < maxCrunchLevel - 1) {
0232             crunchLevel += 1;
0233         } else {
0234             compressFaceDone(QByteArray(), false);
0235             break;
0236         }
0237     }
0238 }
0239 
0240 bool XFaceConfigurator::pngquant(const QImage &image)
0241 {
0242     const QImage small = image.scaled(48, 48);
0243 
0244     mPngquantProc->terminate();
0245     mPngquantProc->waitForFinished();
0246 
0247     mPngquantProc->start();
0248 
0249     if (mPngquantProc->waitForStarted()) {
0250         small.save(mPngquantProc, "PNG");
0251         mPngquantProc->closeWriteChannel();
0252         return true;
0253     } else {
0254         return false;
0255     }
0256 }
0257 
0258 void XFaceConfigurator::pngquantFinished(int exitCode, QProcess::ExitStatus exitStatus)
0259 {
0260     if (exitCode == 0 && exitStatus == QProcess::NormalExit) {
0261         const QByteArray output = mPngquantProc->readAllStandardOutput();
0262         compressFaceDone(output, true);
0263     } else {
0264         const QByteArray errOut = mPngquantProc->readAllStandardError();
0265         const QString str = QString::fromLocal8Bit(errOut);
0266 
0267         KMessageBox::error(this, i18n("pngquant exited with code %1: %2", exitCode, str));
0268 
0269         compressFaceDone(QByteArray(), true);
0270     }
0271 }
0272 
0273 #include "moc_xfaceconfigurator.cpp"