blob: d1b15f1e2984e8798b90af7f92da89b44ff52dce [file] [log] [blame]
Alexandre Lision3b0bd332015-03-15 18:43:07 -04001/*
Alexandre Lision9fe374b2016-01-06 10:17:31 -05002 * Copyright (C) 2015-2016 Savoir-faire Linux Inc.
Alexandre Lision3b0bd332015-03-15 18:43:07 -04003 * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Alexandre Lision3b0bd332015-03-15 18:43:07 -040018 */
19#import "ImageManipulationDelegate.h"
20
21#import <Cocoa/Cocoa.h>
22#import <Foundation/Foundation.h>
23
24//Qt
25#import <QSize>
26#import <QBuffer>
27#import <QtGui/QColor>
28#import <QtGui/QPainter>
Alexandre Lision4f264622016-05-08 17:08:56 -040029#import <QHash>
Alexandre Lision3b0bd332015-03-15 18:43:07 -040030#import <QtGui/QBitmap>
31#import <QtWidgets/QApplication>
32#import <QtGui/QImage>
33#import <QtMacExtras/qmacfunctions.h>
34#import <QtGui/QPalette>
35
Anthony Léonardd7952d52017-12-04 13:23:05 -050036//LRC
Anthony Léonardd7952d52017-12-04 13:23:05 -050037#import <api/conversation.h>
38#import <api/account.h>
39#import <api/contactmodel.h>
40#import <api/contact.h>
41#import <api/profile.h>
Alexandre Lision3b0bd332015-03-15 18:43:07 -040042
Alexandre Lision7a166e42015-09-02 15:04:43 -040043namespace Interfaces {
Alexandre Lision3b0bd332015-03-15 18:43:07 -040044
Anthony Léonardc40b3472017-08-01 15:19:17 -040045 // Colors from material.io
46 const QColor ImageManipulationDelegate::avatarColors_[] = {
47 {"#fff44336"}, //Red
48 {"#ffe91e63"}, //Pink
49 {"#ff9c27b0"}, //Purple
50 {"#ff673ab7"}, //Deep Purple
51 {"#ff3f51b5"}, //Indigo
52 {"#ff2196f3"}, //Blue
53 {"#ff00bcd4"}, //Cyan
54 {"#ff009688"}, //Teal
55 {"#ff4caf50"}, //Green
56 {"#ff8bc34a"}, //Light Green
57 {"#ff9e9e9e"}, //Grey
58 {"#ffcddc39"}, //Lime
59 {"#ffffc107"}, //Amber
60 {"#ffff5722"}, //Deep Orange
61 {"#ff795548"}, //Brown
62 {"#ff607d8b"} //Blue Grey
63 };
64
Alexandre Lision4f264622016-05-08 17:08:56 -040065 ImageManipulationDelegate::ImageManipulationDelegate() {}
Alexandre Lision3b0bd332015-03-15 18:43:07 -040066
Alexandre Lisionf02a32b2016-04-19 15:01:07 -040067 QPixmap
68 ImageManipulationDelegate::crop(QPixmap& photo, const QSize& destSize)
69 {
70 auto initSize = photo.size();
71 float leftDelta = 0;
72 float topDelta = 0;
73
74 if (destSize.height() == initSize.height()) {
75 leftDelta = (destSize.width() - initSize.width()) / 2;
76 } else {
77 topDelta = (destSize.height() - initSize.height()) / 2;
78 }
79
80 float xScale = (float)destSize.width() / initSize.width();
81 float yScale = (float)destSize.height() / initSize.height();
82
83 QRectF destRect(leftDelta, topDelta,
84 initSize.width() - leftDelta, initSize.height() - topDelta);
85
86 destRect.setLeft(leftDelta * xScale);
87 destRect.setTop(topDelta * yScale);
88
89 destRect.setWidth((initSize.width() - leftDelta) * xScale);
90 destRect.setHeight((initSize.height() - topDelta) * yScale);
91
92 return photo.copy(destRect.toRect());
93 }
94
Alexandre Lision7a166e42015-09-02 15:04:43 -040095 QVariant ImageManipulationDelegate::personPhoto(const QByteArray& data, const QString& type)
96 {
97 QImage image;
98 //For now, ENCODING is only base64 and image type PNG or JPG
Kateryna Kostiukbc54aba2020-07-31 16:21:22 -040099 bool ret = image.loadFromData(QByteArray::fromBase64(data),type.toLatin1());
100 if (!ret) {
101 ret = image.loadFromData(QByteArray::fromBase64(data), 0);
102 }
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400103 if (!ret) {
Alexandre Lision7a166e42015-09-02 15:04:43 -0400104 qDebug() << "vCard image loading failed";
Andreas Traczyk761aebe2018-05-08 18:02:06 -0400105 return QVariant();
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400106 }
Alexandre Lision7a166e42015-09-02 15:04:43 -0400107
108 return QPixmap::fromImage(image);
109 }
110
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400111 QChar letterForDefaultUserPixmap(const lrc::api::contact::Info& contact)
Anthony Léonardd7952d52017-12-04 13:23:05 -0500112 {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400113 if (!contact.profileInfo.alias.isEmpty()) {
114 return contact.profileInfo.alias.at(0).toUpper();
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400115 } else if((contact.profileInfo.type == lrc::api::profile::Type::RING ||
116 contact.profileInfo.type == lrc::api::profile::Type::PENDING) &&
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400117 !contact.registeredName.isEmpty()) {
118 return contact.registeredName.at(0).toUpper();
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400119 } else {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400120 return contact.profileInfo.uri.at(0).toUpper();
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400121 }
Anthony Léonardd7952d52017-12-04 13:23:05 -0500122 }
123
124 QVariant ImageManipulationDelegate::conversationPhoto(const lrc::api::conversation::Info& conversation,
125 const lrc::api::account::Info& accountInfo,
126 const QSize& size,
127 bool displayPresence)
128 {
129 Q_UNUSED(displayPresence)
130
131 try {
132 auto contact = accountInfo.contactModel->getContact(conversation.participants[0]);
133 auto& avatar = contact.profileInfo.avatar;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400134 if (!avatar.isEmpty()) {
Anthony Léonardd7952d52017-12-04 13:23:05 -0500135 QPixmap pxm;
136 const int radius = size.height() / 2;
137
Kateryna Kostiuk133364b2018-10-31 11:36:08 -0400138 /*
139 * we could not now clear cache and image coul be outdated
140 * so do not use cache now
Anthony Léonardd7952d52017-12-04 13:23:05 -0500141 // Check cache
142 auto index = QStringLiteral("%1%2%3").arg(size.width())
143 .arg(size.height())
144 .arg(QString::fromStdString(conversation.uid));
145
146 if (convPixmCache.contains(index)) {
147 return convPixmCache.value(index);
148 }
Kateryna Kostiuk133364b2018-10-31 11:36:08 -0400149 */
Anthony Léonardd7952d52017-12-04 13:23:05 -0500150
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400151 auto contactPhoto = qvariant_cast<QPixmap>(personPhoto(avatar.toUtf8()));
Anthony Léonardd7952d52017-12-04 13:23:05 -0500152 contactPhoto = contactPhoto.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
153
154 QPixmap finalImg;
155 // We crop the avatar if picture is not squared as scaled() keep ratio of original picture
156 if (contactPhoto.size() != size) {
157 finalImg = crop(contactPhoto, size);
158 } else
159 finalImg = contactPhoto;
160
161 // Creating clean QPixmap
162 pxm = QPixmap(size);
163 pxm.fill(Qt::transparent);
164
165 //Add corner radius to the Pixmap
166 QPainter painter(&pxm);
167 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
168 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
169 QRect pxRect = finalImg.rect();
170 QBitmap mask(pxRect.size());
171 QPainter customPainter(&mask);
172 customPainter.setRenderHints (QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
173 customPainter.fillRect (pxRect , Qt::white );
174 customPainter.setBackground (Qt::black );
175 customPainter.setBrush (Qt::black );
176 customPainter.drawRoundedRect(pxRect,radius,radius );
177 finalImg.setMask (mask );
178 painter.drawPixmap (0,0,finalImg );
179 painter.setBrush (Qt::NoBrush );
Anthony Léonard9afb9fe2018-01-18 16:16:32 -0500180 painter.setPen (Qt::transparent );
Anthony Léonardd7952d52017-12-04 13:23:05 -0500181 painter.setCompositionMode (QPainter::CompositionMode_SourceIn);
182 painter.drawRoundedRect(0,0,pxm.height(),pxm.height(),radius,radius);
183
184 // Save in cache
Kateryna Kostiuk133364b2018-10-31 11:36:08 -0400185 //convPixmCache.insert(index, pxm);
Anthony Léonardd7952d52017-12-04 13:23:05 -0500186
187 return pxm;
188 } else {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400189 if (contact.profileInfo.uri.isEmpty()) {
190 return QVariant();
191 }
192 auto color = contact.profileInfo.uri.at(0);
193 auto trimmed = contact.profileInfo.alias.trimmed().replace("\r","").replace("\n","");
194 if (!trimmed.isEmpty()) {
195 return drawDefaultUserPixmap(size, color.toLatin1(), trimmed.at(0).toUpper().toLatin1());
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400196 } else if((contact.profileInfo.type == lrc::api::profile::Type::RING ||
197 contact.profileInfo.type == lrc::api::profile::Type::PENDING) &&
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400198 !contact.registeredName.isEmpty()) {
199 trimmed = contact.registeredName.trimmed().replace("\r","").replace("\n","");
200 if(!trimmed.isEmpty()) {
201 return drawDefaultUserPixmap(size, color.toLatin1(), trimmed.at(0).toUpper().toLatin1());
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400202 } else {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400203 return drawDefaultUserPixmapUriOnly(size, color.toLatin1());
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400204 }
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400205 } else {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400206 return drawDefaultUserPixmapUriOnly(size, color.toLatin1());
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400207 }
Anthony Léonardd7952d52017-12-04 13:23:05 -0500208 }
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400209 } catch (...) {
Andreas Traczyk761aebe2018-05-08 18:02:06 -0400210 return QVariant();
Anthony Léonardd7952d52017-12-04 13:23:05 -0500211 }
212 }
213
Alexandre Lision7a166e42015-09-02 15:04:43 -0400214 QByteArray ImageManipulationDelegate::toByteArray(const QVariant& pxm)
215 {
216 //Preparation of our QPixmap
217 QByteArray bArray;
218 QBuffer buffer(&bArray);
219 buffer.open(QIODevice::WriteOnly);
220
221 //PNG ?
Kateryna Kostiukc1907fd2017-05-12 14:07:44 -0400222 (qvariant_cast<QPixmap>(pxm)).scaled({100,100}).save(&buffer, "PNG");
Alexandre Lision7a166e42015-09-02 15:04:43 -0400223 buffer.close();
224
225 return bArray;
226 }
227
Anthony Léonardc40b3472017-08-01 15:19:17 -0400228 QPixmap ImageManipulationDelegate::drawDefaultUserPixmap(const QSize& size, const char color, const char letter) {
229 // We start with a transparent avatar
230 QPixmap avatar(size);
231 avatar.fill(Qt::transparent);
Alexandre Lision4f264622016-05-08 17:08:56 -0400232
Anthony Léonardc40b3472017-08-01 15:19:17 -0400233 // We pick a color based on the passed character
234 QColor avColor = ImageManipulationDelegate::avatarColors_[color % 16];
Alexandre Lision4f264622016-05-08 17:08:56 -0400235
Anthony Léonardc40b3472017-08-01 15:19:17 -0400236 // We draw a circle with this color
237 QPainter painter(&avatar);
238 painter.setRenderHints(QPainter::Antialiasing|QPainter::SmoothPixmapTransform);
239 painter.setPen(Qt::transparent);
240 painter.setBrush(avColor);
241 painter.drawEllipse(avatar.rect());
Alexandre Lision3b0bd332015-03-15 18:43:07 -0400242
Anthony Léonardc40b3472017-08-01 15:19:17 -0400243 // Then we paint a letter in the circle
244 auto font = painter.font();
245 font.setPointSize(avatar.height()/2);
246 painter.setFont(font);
247 painter.setPen(Qt::white);
248 QRect textRect = avatar.rect();
249 painter.drawText(textRect, QString(letter), QTextOption(Qt::AlignCenter));
Alexandre Lision4f264622016-05-08 17:08:56 -0400250
Anthony Léonardc40b3472017-08-01 15:19:17 -0400251 return avatar;
Alexandre Lision3b0bd332015-03-15 18:43:07 -0400252 }
Alexandre Lision7a166e42015-09-02 15:04:43 -0400253
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400254 QPixmap ImageManipulationDelegate::drawDefaultUserPixmapUriOnly(const QSize& size, const char color) {
255 // We start with a transparent avatar
256 QPixmap avatar(size);
257 avatar.fill(Qt::transparent);
258
259 // We pick a color based on the passed character
260 QColor avColor = ImageManipulationDelegate::avatarColors_[color % 16];
261
262 // We draw a circle with this color
263 QPainter painter(&avatar);
264 painter.setRenderHints(QPainter::Antialiasing|QPainter::SmoothPixmapTransform);
265 painter.setPen(Qt::transparent);
266 painter.setBrush(avColor);
267 painter.drawEllipse(avatar.rect());
268
269 // Then we paint the avatar in the circle
270 QRect textRect = avatar.rect();
271 QImage defaultAvatarImage;
272 QRect rect = QRect(0, 0, size.width(), size.height());
273 NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
274 NSString *imagePath = [bundleURL.absoluteString stringByAppendingString:@"Contents/Resources/default_avatar_overlay.png"];
275 if (defaultAvatarImage.load(QString::fromNSString(imagePath).mid(7))) {
276 painter.drawImage(avatar.rect(), defaultAvatarImage);
277 } else {
278 painter.drawText(avatar.rect(), QString('?'), QTextOption(Qt::AlignCenter));
279 }
280
281 return avatar;
282 }
283
Alexandre Lision7a166e42015-09-02 15:04:43 -0400284 CGImageRef ImageManipulationDelegate::resizeCGImage(CGImageRef image, const QSize& size) {
285 // create context, keeping original image properties
Alexandre Lision7a166e42015-09-02 15:04:43 -0400286 CGContextRef context = CGBitmapContextCreate(NULL, size.width(), size.height(),
287 CGImageGetBitsPerComponent(image),
Alexandre Lisiond5229f32015-11-16 11:17:41 -0500288 CGImageGetBytesPerRow(image),
289 CGImageGetColorSpace(image),
290 kCGImageAlphaPremultipliedLast);
Alexandre Lision7a166e42015-09-02 15:04:43 -0400291
292 if(context == NULL)
293 return nil;
294
295 // draw image to context (resizing it)
296 CGContextDrawImage(context, CGRectMake(0, 0, size.width(), size.height()), image);
297 // extract resulting image from context
298 CGImageRef imgRef = CGBitmapContextCreateImage(context);
299 CGContextRelease(context);
300
301 return imgRef;
302 }
303
304 QVariant
305 ImageManipulationDelegate::numberCategoryIcon(const QVariant& p, const QSize& size, bool displayPresence, bool isPresent)
306 {
307 Q_UNUSED(p)
308 Q_UNUSED(size)
309 Q_UNUSED(displayPresence)
310 Q_UNUSED(isPresent)
311 return QVariant();
312 }
313
314 QVariant
Alexandre Lision7a166e42015-09-02 15:04:43 -0400315 ImageManipulationDelegate::userActionIcon(const UserActionElement& state) const
316 {
317 Q_UNUSED(state)
318 return QVariant();
Alexandre Lision3b0bd332015-03-15 18:43:07 -0400319 }
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400320
Stepan Salenikovich3a6ea302016-01-08 13:54:34 -0500321 QVariant ImageManipulationDelegate::decorationRole(const QModelIndex& index)
322 {
323 Q_UNUSED(index)
324 return QVariant();
325 }
Alexandre Lision7a166e42015-09-02 15:04:43 -0400326} // namespace Interfaces