blob: b25c39ff5ad16b3db2943f0ffe33f2a8ad19578e [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
Alexandre Lision3b0bd332015-03-15 18:43:07 -040037#import <person.h>
Alexandre Lision43e91bc2016-04-19 18:04:52 -040038#import <profilemodel.h>
39#import <profile.h>
Alexandre Lision3b0bd332015-03-15 18:43:07 -040040#import <contactmethod.h>
Anthony Léonardd7952d52017-12-04 13:23:05 -050041#import <api/conversation.h>
42#import <api/account.h>
43#import <api/contactmodel.h>
44#import <api/contact.h>
45#import <api/profile.h>
Alexandre Lision3b0bd332015-03-15 18:43:07 -040046
Alexandre Lision7a166e42015-09-02 15:04:43 -040047namespace Interfaces {
Alexandre Lision3b0bd332015-03-15 18:43:07 -040048
Anthony Léonardc40b3472017-08-01 15:19:17 -040049 // Colors from material.io
50 const QColor ImageManipulationDelegate::avatarColors_[] = {
51 {"#fff44336"}, //Red
52 {"#ffe91e63"}, //Pink
53 {"#ff9c27b0"}, //Purple
54 {"#ff673ab7"}, //Deep Purple
55 {"#ff3f51b5"}, //Indigo
56 {"#ff2196f3"}, //Blue
57 {"#ff00bcd4"}, //Cyan
58 {"#ff009688"}, //Teal
59 {"#ff4caf50"}, //Green
60 {"#ff8bc34a"}, //Light Green
61 {"#ff9e9e9e"}, //Grey
62 {"#ffcddc39"}, //Lime
63 {"#ffffc107"}, //Amber
64 {"#ffff5722"}, //Deep Orange
65 {"#ff795548"}, //Brown
66 {"#ff607d8b"} //Blue Grey
67 };
68
Alexandre Lision4f264622016-05-08 17:08:56 -040069 ImageManipulationDelegate::ImageManipulationDelegate() {}
Alexandre Lision3b0bd332015-03-15 18:43:07 -040070
Alexandre Lision7a166e42015-09-02 15:04:43 -040071 QVariant ImageManipulationDelegate::contactPhoto(Person* c, const QSize& size, bool displayPresence) {
Alexandre Lisionde0314b2015-09-02 15:45:21 -040072 const int radius = size.height() / 2;
Alexandre Lision7a166e42015-09-02 15:04:43 -040073 QPixmap pxm;
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040074 if (c && c->photo().isValid()) {
Alexandre Lision196545b2016-05-13 17:05:13 -040075 // Check cache
76 auto index = QStringLiteral("%1%2%3").arg(size.width())
77 .arg(size.height())
78 .arg(QString::fromUtf8(c->uid()));
79
80 if (m_hContactsPixmap.contains(index)) {
81 return m_hContactsPixmap.value(index).second;
82 }
83
Alexandre Lisionf02a32b2016-04-19 15:01:07 -040084 QPixmap contactPhoto(qvariant_cast<QPixmap>(c->photo()).scaled(size, Qt::KeepAspectRatioByExpanding,
85 Qt::SmoothTransformation));
86
87 QPixmap finalImg;
88 if (contactPhoto.size() != size) {
89 finalImg = crop(contactPhoto, size);
90 } else
91 finalImg = contactPhoto;
92
Alexandre Lision7a166e42015-09-02 15:04:43 -040093 pxm = QPixmap(size);
94 pxm.fill(Qt::transparent);
95 QPainter painter(&pxm);
96
97 //Clear the pixmap
Alexandre Lisionde0314b2015-09-02 15:45:21 -040098 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
Alexandre Lision7a166e42015-09-02 15:04:43 -040099 painter.setCompositionMode(QPainter::CompositionMode_Clear);
100 painter.fillRect(0,0,size.width(),size.height(),QBrush(Qt::white));
101 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
102
103 //Add corner radius to the Pixmap
Alexandre Lisionf02a32b2016-04-19 15:01:07 -0400104 QRect pxRect = finalImg.rect();
Alexandre Lision7a166e42015-09-02 15:04:43 -0400105 QBitmap mask(pxRect.size());
106 QPainter customPainter(&mask);
Alexandre Lisionde0314b2015-09-02 15:45:21 -0400107 customPainter.setRenderHints (QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
Alexandre Lision7a166e42015-09-02 15:04:43 -0400108 customPainter.fillRect (pxRect , Qt::white );
109 customPainter.setBackground (Qt::black );
110 customPainter.setBrush (Qt::black );
Alexandre Lisionf02a32b2016-04-19 15:01:07 -0400111 customPainter.drawRoundedRect(pxRect,radius,radius );
112 finalImg.setMask (mask );
113 painter.drawPixmap (0,0,finalImg );
Alexandre Lisionde0314b2015-09-02 15:45:21 -0400114 painter.setBrush (Qt::NoBrush );
115 painter.setPen (Qt::black );
116 painter.setCompositionMode (QPainter::CompositionMode_SourceIn);
117 painter.drawRoundedRect(0,0,pxm.height(),pxm.height(),radius,radius);
Alexandre Lision7a166e42015-09-02 15:04:43 -0400118
Alexandre Lision196545b2016-05-13 17:05:13 -0400119 // Save in cache
120 QPair<QMetaObject::Connection, QPixmap> toInsert;
121 toInsert.first = QObject::connect(c,
122 &Person::changed,
123 [=]() {
124 if (c) {
125 auto index = QStringLiteral("%1%2%3").arg(size.width())
126 .arg(size.height())
127 .arg(QString::fromUtf8(c->uid()));
128 if (m_hContactsPixmap.contains(index)) {
129 QObject::disconnect(m_hContactsPixmap.value(index).first);
130 m_hContactsPixmap.remove(index);
131 }
132 }
133 });
134 toInsert.second = pxm;
135 m_hContactsPixmap.insert(index, toInsert);
136
137 } else {
Anthony Léonardc40b3472017-08-01 15:19:17 -0400138 return drawDefaultUserPixmap(size,
139 c->phoneNumbers().at(0)->uri().userinfo().at(0).toLatin1(),
140 c->phoneNumbers().at(0)->bestName().at(0).toUpper().toLatin1());
Alexandre Lision196545b2016-05-13 17:05:13 -0400141 }
Alexandre Lisionafa56dc2016-05-08 17:30:20 -0400142
Alexandre Lision7a166e42015-09-02 15:04:43 -0400143 return pxm;
144 }
145
Alexandre Lisionf02a32b2016-04-19 15:01:07 -0400146 QPixmap
147 ImageManipulationDelegate::crop(QPixmap& photo, const QSize& destSize)
148 {
149 auto initSize = photo.size();
150 float leftDelta = 0;
151 float topDelta = 0;
152
153 if (destSize.height() == initSize.height()) {
154 leftDelta = (destSize.width() - initSize.width()) / 2;
155 } else {
156 topDelta = (destSize.height() - initSize.height()) / 2;
157 }
158
159 float xScale = (float)destSize.width() / initSize.width();
160 float yScale = (float)destSize.height() / initSize.height();
161
162 QRectF destRect(leftDelta, topDelta,
163 initSize.width() - leftDelta, initSize.height() - topDelta);
164
165 destRect.setLeft(leftDelta * xScale);
166 destRect.setTop(topDelta * yScale);
167
168 destRect.setWidth((initSize.width() - leftDelta) * xScale);
169 destRect.setHeight((initSize.height() - topDelta) * yScale);
170
171 return photo.copy(destRect.toRect());
172 }
173
Alexandre Lision7a166e42015-09-02 15:04:43 -0400174 QVariant
175 ImageManipulationDelegate::callPhoto(Call* c, const QSize& size, bool displayPresence)
176 {
177 return callPhoto(c->peerContactMethod(), size, displayPresence);
178 }
179
180 QVariant
181 ImageManipulationDelegate::callPhoto(const ContactMethod* n, const QSize& size, bool displayPresence)
182 {
183 if (n->contact()) {
184 return contactPhoto(n->contact(), size, displayPresence);
185 } else {
Anthony Léonardc40b3472017-08-01 15:19:17 -0400186 return drawDefaultUserPixmap(size,
187 n->uri().userinfo().at(0).toLatin1(),
188 n->bestName().at(0).toUpper().toLatin1());
Alexandre Lision7a166e42015-09-02 15:04:43 -0400189 }
190 }
191
192 QVariant ImageManipulationDelegate::personPhoto(const QByteArray& data, const QString& type)
193 {
194 QImage image;
195 //For now, ENCODING is only base64 and image type PNG or JPG
196 const bool ret = image.loadFromData(QByteArray::fromBase64(data),type.toLatin1());
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400197 if (!ret) {
Alexandre Lision7a166e42015-09-02 15:04:43 -0400198 qDebug() << "vCard image loading failed";
Anthony Léonardc40b3472017-08-01 15:19:17 -0400199 return drawDefaultUserPixmap(decorationSize, '?', '?');
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400200 }
Alexandre Lision7a166e42015-09-02 15:04:43 -0400201
202 return QPixmap::fromImage(image);
203 }
204
Anthony Léonardd7952d52017-12-04 13:23:05 -0500205 char letterForDefaultUserPixmap(const lrc::api::contact::Info& contact)
206 {
207 if (!contact.profileInfo.alias.empty())
208 return std::toupper(contact.profileInfo.alias.at(0));
209 else if(contact.profileInfo.type == lrc::api::profile::Type::RING && !contact.registeredName.empty())
210 return std::toupper(contact.registeredName.at(0));
211 else
212 return std::toupper(contact.profileInfo.uri.at(0));
213 }
214
215 QVariant ImageManipulationDelegate::conversationPhoto(const lrc::api::conversation::Info& conversation,
216 const lrc::api::account::Info& accountInfo,
217 const QSize& size,
218 bool displayPresence)
219 {
220 Q_UNUSED(displayPresence)
221
222 try {
223 auto contact = accountInfo.contactModel->getContact(conversation.participants[0]);
224 auto& avatar = contact.profileInfo.avatar;
225 if (!avatar.empty()) {
226 QPixmap pxm;
227 const int radius = size.height() / 2;
228
229 // Check cache
230 auto index = QStringLiteral("%1%2%3").arg(size.width())
231 .arg(size.height())
232 .arg(QString::fromStdString(conversation.uid));
233
234 if (convPixmCache.contains(index)) {
235 return convPixmCache.value(index);
236 }
237
238 auto contactPhoto = qvariant_cast<QPixmap>(personPhoto(QByteArray::fromStdString(avatar)));
239 contactPhoto = contactPhoto.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
240
241 QPixmap finalImg;
242 // We crop the avatar if picture is not squared as scaled() keep ratio of original picture
243 if (contactPhoto.size() != size) {
244 finalImg = crop(contactPhoto, size);
245 } else
246 finalImg = contactPhoto;
247
248 // Creating clean QPixmap
249 pxm = QPixmap(size);
250 pxm.fill(Qt::transparent);
251
252 //Add corner radius to the Pixmap
253 QPainter painter(&pxm);
254 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
255 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
256 QRect pxRect = finalImg.rect();
257 QBitmap mask(pxRect.size());
258 QPainter customPainter(&mask);
259 customPainter.setRenderHints (QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
260 customPainter.fillRect (pxRect , Qt::white );
261 customPainter.setBackground (Qt::black );
262 customPainter.setBrush (Qt::black );
263 customPainter.drawRoundedRect(pxRect,radius,radius );
264 finalImg.setMask (mask );
265 painter.drawPixmap (0,0,finalImg );
266 painter.setBrush (Qt::NoBrush );
267 painter.setPen (Qt::black );
268 painter.setCompositionMode (QPainter::CompositionMode_SourceIn);
269 painter.drawRoundedRect(0,0,pxm.height(),pxm.height(),radius,radius);
270
271 // Save in cache
272 convPixmCache.insert(index, pxm);
273
274 return pxm;
275 } else {
276 char color = contact.profileInfo.uri.at(0);
277 char letter = letterForDefaultUserPixmap(contact);
278 return drawDefaultUserPixmap(size, color, letter);
279 }
280 } catch (const std::out_of_range& e) {
281 return drawDefaultUserPixmap(size, '?', '?');
282 }
283 }
284
Alexandre Lision7a166e42015-09-02 15:04:43 -0400285 QByteArray ImageManipulationDelegate::toByteArray(const QVariant& pxm)
286 {
287 //Preparation of our QPixmap
288 QByteArray bArray;
289 QBuffer buffer(&bArray);
290 buffer.open(QIODevice::WriteOnly);
291
292 //PNG ?
Kateryna Kostiukc1907fd2017-05-12 14:07:44 -0400293 (qvariant_cast<QPixmap>(pxm)).scaled({100,100}).save(&buffer, "PNG");
Alexandre Lision7a166e42015-09-02 15:04:43 -0400294 buffer.close();
295
296 return bArray;
297 }
298
Anthony Léonardc40b3472017-08-01 15:19:17 -0400299 QPixmap ImageManipulationDelegate::drawDefaultUserPixmap(const QSize& size, const char color, const char letter) {
300 // We start with a transparent avatar
301 QPixmap avatar(size);
302 avatar.fill(Qt::transparent);
Alexandre Lision4f264622016-05-08 17:08:56 -0400303
Anthony Léonardc40b3472017-08-01 15:19:17 -0400304 // We pick a color based on the passed character
305 QColor avColor = ImageManipulationDelegate::avatarColors_[color % 16];
Alexandre Lision4f264622016-05-08 17:08:56 -0400306
Anthony Léonardc40b3472017-08-01 15:19:17 -0400307 // We draw a circle with this color
308 QPainter painter(&avatar);
309 painter.setRenderHints(QPainter::Antialiasing|QPainter::SmoothPixmapTransform);
310 painter.setPen(Qt::transparent);
311 painter.setBrush(avColor);
312 painter.drawEllipse(avatar.rect());
Alexandre Lision3b0bd332015-03-15 18:43:07 -0400313
Anthony Léonardc40b3472017-08-01 15:19:17 -0400314 // Then we paint a letter in the circle
315 auto font = painter.font();
316 font.setPointSize(avatar.height()/2);
317 painter.setFont(font);
318 painter.setPen(Qt::white);
319 QRect textRect = avatar.rect();
320 painter.drawText(textRect, QString(letter), QTextOption(Qt::AlignCenter));
Alexandre Lision4f264622016-05-08 17:08:56 -0400321
Anthony Léonardc40b3472017-08-01 15:19:17 -0400322 return avatar;
Alexandre Lision3b0bd332015-03-15 18:43:07 -0400323 }
Alexandre Lision7a166e42015-09-02 15:04:43 -0400324
325 CGImageRef ImageManipulationDelegate::resizeCGImage(CGImageRef image, const QSize& size) {
326 // create context, keeping original image properties
Alexandre Lision7a166e42015-09-02 15:04:43 -0400327 CGContextRef context = CGBitmapContextCreate(NULL, size.width(), size.height(),
328 CGImageGetBitsPerComponent(image),
Alexandre Lisiond5229f32015-11-16 11:17:41 -0500329 CGImageGetBytesPerRow(image),
330 CGImageGetColorSpace(image),
331 kCGImageAlphaPremultipliedLast);
Alexandre Lision7a166e42015-09-02 15:04:43 -0400332
333 if(context == NULL)
334 return nil;
335
336 // draw image to context (resizing it)
337 CGContextDrawImage(context, CGRectMake(0, 0, size.width(), size.height()), image);
338 // extract resulting image from context
339 CGImageRef imgRef = CGBitmapContextCreateImage(context);
340 CGContextRelease(context);
341
342 return imgRef;
343 }
344
345 QVariant
346 ImageManipulationDelegate::numberCategoryIcon(const QVariant& p, const QSize& size, bool displayPresence, bool isPresent)
347 {
348 Q_UNUSED(p)
349 Q_UNUSED(size)
350 Q_UNUSED(displayPresence)
351 Q_UNUSED(isPresent)
352 return QVariant();
353 }
354
355 QVariant
356 ImageManipulationDelegate::securityIssueIcon(const QModelIndex& index)
357 {
358 Q_UNUSED(index)
359 return QVariant();
360 }
361
362 QVariant
363 ImageManipulationDelegate::collectionIcon(const CollectionInterface* interface, PixmapManipulatorI::CollectionIconHint hint) const
364 {
365 Q_UNUSED(interface)
366 Q_UNUSED(hint)
367 return QVariant();
368 }
369 QVariant
370 ImageManipulationDelegate::securityLevelIcon(const SecurityEvaluationModel::SecurityLevel level) const
371 {
372 Q_UNUSED(level)
373 return QVariant();
374 }
375 QVariant
376 ImageManipulationDelegate::historySortingCategoryIcon(const CategorizedHistoryModel::SortedProxy::Categories cat) const
377 {
378 Q_UNUSED(cat)
379 return QVariant();
380 }
381 QVariant
382 ImageManipulationDelegate::contactSortingCategoryIcon(const CategorizedContactModel::SortedProxy::Categories cat) const
383 {
384 Q_UNUSED(cat)
385 return QVariant();
386 }
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400387
Alexandre Lision7a166e42015-09-02 15:04:43 -0400388 QVariant
389 ImageManipulationDelegate::userActionIcon(const UserActionElement& state) const
390 {
391 Q_UNUSED(state)
392 return QVariant();
Alexandre Lision3b0bd332015-03-15 18:43:07 -0400393 }
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400394
Stepan Salenikovich3a6ea302016-01-08 13:54:34 -0500395 QVariant ImageManipulationDelegate::decorationRole(const QModelIndex& index)
396 {
397 Q_UNUSED(index)
398 return QVariant();
399 }
400
401 QVariant ImageManipulationDelegate::decorationRole(const Call* c)
402 {
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400403 if (c && c->peerContactMethod()
404 && c->peerContactMethod()->contact()) {
405 return contactPhoto(c->peerContactMethod()->contact(), decorationSize);
406 } else
Anthony Léonardc40b3472017-08-01 15:19:17 -0400407 return drawDefaultUserPixmap(decorationSize,
408 c->peerContactMethod()->uri().userinfo().at(0).toLatin1(),
409 c->peerContactMethod()->bestName().at(0).toUpper().toLatin1());
Stepan Salenikovich3a6ea302016-01-08 13:54:34 -0500410 }
411
412 QVariant ImageManipulationDelegate::decorationRole(const ContactMethod* cm)
413 {
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400414 QImage photo;
415 if (cm && cm->contact() && cm->contact()->photo().isValid())
416 return contactPhoto(cm->contact(), decorationSize);
417 else
Anthony Léonardc40b3472017-08-01 15:19:17 -0400418 return drawDefaultUserPixmap(decorationSize,
419 cm->uri().userinfo().at(0).toLatin1(),
420 cm->bestName().at(0).toUpper().toLatin1());
Stepan Salenikovich3a6ea302016-01-08 13:54:34 -0500421 }
422
423 QVariant ImageManipulationDelegate::decorationRole(const Person* p)
424 {
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400425 return contactPhoto(const_cast<Person*>(p), decorationSize);
426 }
427
428 QVariant ImageManipulationDelegate::decorationRole(const Account* acc)
429 {
430 Q_UNUSED(acc)
431 if (auto pro = ProfileModel::instance().selectedProfile())
432 return contactPhoto(pro->person(), decorationSize);
Anthony Léonardc40b3472017-08-01 15:19:17 -0400433 return drawDefaultUserPixmap(decorationSize, '?', '?');
Stepan Salenikovich3a6ea302016-01-08 13:54:34 -0500434 }
435
Alexandre Lision7a166e42015-09-02 15:04:43 -0400436} // namespace Interfaces