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"