blob: e3fa84f8fecc2aeddd6fe1d82ddf4658758c0b90 [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
99 const bool ret = image.loadFromData(QByteArray::fromBase64(data),type.toLatin1());
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400100 if (!ret) {
Alexandre Lision7a166e42015-09-02 15:04:43 -0400101 qDebug() << "vCard image loading failed";
Andreas Traczyk761aebe2018-05-08 18:02:06 -0400102 return QVariant();
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400103 }
Alexandre Lision7a166e42015-09-02 15:04:43 -0400104
105 return QPixmap::fromImage(image);
106 }
107
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400108 QChar letterForDefaultUserPixmap(const lrc::api::contact::Info& contact)
Anthony Léonardd7952d52017-12-04 13:23:05 -0500109 {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400110 if (!contact.profileInfo.alias.isEmpty()) {
111 return contact.profileInfo.alias.at(0).toUpper();
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400112 } else if((contact.profileInfo.type == lrc::api::profile::Type::RING ||
113 contact.profileInfo.type == lrc::api::profile::Type::PENDING) &&
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400114 !contact.registeredName.isEmpty()) {
115 return contact.registeredName.at(0).toUpper();
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400116 } else {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400117 return contact.profileInfo.uri.at(0).toUpper();
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400118 }
Anthony Léonardd7952d52017-12-04 13:23:05 -0500119 }
120
121 QVariant ImageManipulationDelegate::conversationPhoto(const lrc::api::conversation::Info& conversation,
122 const lrc::api::account::Info& accountInfo,
123 const QSize& size,
124 bool displayPresence)
125 {
126 Q_UNUSED(displayPresence)
127
128 try {
129 auto contact = accountInfo.contactModel->getContact(conversation.participants[0]);
130 auto& avatar = contact.profileInfo.avatar;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400131 if (!avatar.isEmpty()) {
Anthony Léonardd7952d52017-12-04 13:23:05 -0500132 QPixmap pxm;
133 const int radius = size.height() / 2;
134
Kateryna Kostiuk133364b2018-10-31 11:36:08 -0400135 /*
136 * we could not now clear cache and image coul be outdated
137 * so do not use cache now
Anthony Léonardd7952d52017-12-04 13:23:05 -0500138 // Check cache
139 auto index = QStringLiteral("%1%2%3").arg(size.width())
140 .arg(size.height())
141 .arg(QString::fromStdString(conversation.uid));
142
143 if (convPixmCache.contains(index)) {
144 return convPixmCache.value(index);
145 }
Kateryna Kostiuk133364b2018-10-31 11:36:08 -0400146 */
Anthony Léonardd7952d52017-12-04 13:23:05 -0500147
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400148 auto contactPhoto = qvariant_cast<QPixmap>(personPhoto(avatar.toUtf8()));
Anthony Léonardd7952d52017-12-04 13:23:05 -0500149 contactPhoto = contactPhoto.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
150
151 QPixmap finalImg;
152 // We crop the avatar if picture is not squared as scaled() keep ratio of original picture
153 if (contactPhoto.size() != size) {
154 finalImg = crop(contactPhoto, size);
155 } else
156 finalImg = contactPhoto;
157
158 // Creating clean QPixmap
159 pxm = QPixmap(size);
160 pxm.fill(Qt::transparent);
161
162 //Add corner radius to the Pixmap
163 QPainter painter(&pxm);
164 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
165 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
166 QRect pxRect = finalImg.rect();
167 QBitmap mask(pxRect.size());
168 QPainter customPainter(&mask);
169 customPainter.setRenderHints (QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
170 customPainter.fillRect (pxRect , Qt::white );
171 customPainter.setBackground (Qt::black );
172 customPainter.setBrush (Qt::black );
173 customPainter.drawRoundedRect(pxRect,radius,radius );
174 finalImg.setMask (mask );
175 painter.drawPixmap (0,0,finalImg );
176 painter.setBrush (Qt::NoBrush );
Anthony Léonard9afb9fe2018-01-18 16:16:32 -0500177 painter.setPen (Qt::transparent );
Anthony Léonardd7952d52017-12-04 13:23:05 -0500178 painter.setCompositionMode (QPainter::CompositionMode_SourceIn);
179 painter.drawRoundedRect(0,0,pxm.height(),pxm.height(),radius,radius);
180
181 // Save in cache
Kateryna Kostiuk133364b2018-10-31 11:36:08 -0400182 //convPixmCache.insert(index, pxm);
Anthony Léonardd7952d52017-12-04 13:23:05 -0500183
184 return pxm;
185 } else {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400186 if (contact.profileInfo.uri.isEmpty()) {
187 return QVariant();
188 }
189 auto color = contact.profileInfo.uri.at(0);
190 auto trimmed = contact.profileInfo.alias.trimmed().replace("\r","").replace("\n","");
191 if (!trimmed.isEmpty()) {
192 return drawDefaultUserPixmap(size, color.toLatin1(), trimmed.at(0).toUpper().toLatin1());
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400193 } else if((contact.profileInfo.type == lrc::api::profile::Type::RING ||
194 contact.profileInfo.type == lrc::api::profile::Type::PENDING) &&
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400195 !contact.registeredName.isEmpty()) {
196 trimmed = contact.registeredName.trimmed().replace("\r","").replace("\n","");
197 if(!trimmed.isEmpty()) {
198 return drawDefaultUserPixmap(size, color.toLatin1(), trimmed.at(0).toUpper().toLatin1());
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400199 } else {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400200 return drawDefaultUserPixmapUriOnly(size, color.toLatin1());
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400201 }
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400202 } else {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400203 return drawDefaultUserPixmapUriOnly(size, color.toLatin1());
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400204 }
Anthony Léonardd7952d52017-12-04 13:23:05 -0500205 }
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400206 } catch (...) {
Andreas Traczyk761aebe2018-05-08 18:02:06 -0400207 return QVariant();
Anthony Léonardd7952d52017-12-04 13:23:05 -0500208 }
209 }
210
Alexandre Lision7a166e42015-09-02 15:04:43 -0400211 QByteArray ImageManipulationDelegate::toByteArray(const QVariant& pxm)
212 {
213 //Preparation of our QPixmap
214 QByteArray bArray;
215 QBuffer buffer(&bArray);
216 buffer.open(QIODevice::WriteOnly);
217
218 //PNG ?
Kateryna Kostiukc1907fd2017-05-12 14:07:44 -0400219 (qvariant_cast<QPixmap>(pxm)).scaled({100,100}).save(&buffer, "PNG");
Alexandre Lision7a166e42015-09-02 15:04:43 -0400220 buffer.close();
221
222 return bArray;
223 }
224
Anthony Léonardc40b3472017-08-01 15:19:17 -0400225 QPixmap ImageManipulationDelegate::drawDefaultUserPixmap(const QSize& size, const char color, const char letter) {
226 // We start with a transparent avatar
227 QPixmap avatar(size);
228 avatar.fill(Qt::transparent);
Alexandre Lision4f264622016-05-08 17:08:56 -0400229
Anthony Léonardc40b3472017-08-01 15:19:17 -0400230 // We pick a color based on the passed character
231 QColor avColor = ImageManipulationDelegate::avatarColors_[color % 16];
Alexandre Lision4f264622016-05-08 17:08:56 -0400232
Anthony Léonardc40b3472017-08-01 15:19:17 -0400233 // We draw a circle with this color
234 QPainter painter(&avatar);
235 painter.setRenderHints(QPainter::Antialiasing|QPainter::SmoothPixmapTransform);
236 painter.setPen(Qt::transparent);
237 painter.setBrush(avColor);
238 painter.drawEllipse(avatar.rect());
Alexandre Lision3b0bd332015-03-15 18:43:07 -0400239
Anthony Léonardc40b3472017-08-01 15:19:17 -0400240 // Then we paint a letter in the circle
241 auto font = painter.font();
242 font.setPointSize(avatar.height()/2);
243 painter.setFont(font);
244 painter.setPen(Qt::white);
245 QRect textRect = avatar.rect();
246 painter.drawText(textRect, QString(letter), QTextOption(Qt::AlignCenter));
Alexandre Lision4f264622016-05-08 17:08:56 -0400247
Anthony Léonardc40b3472017-08-01 15:19:17 -0400248 return avatar;
Alexandre Lision3b0bd332015-03-15 18:43:07 -0400249 }
Alexandre Lision7a166e42015-09-02 15:04:43 -0400250
Andreas Traczyk87c999f2018-04-12 17:38:25 -0400251 QPixmap ImageManipulationDelegate::drawDefaultUserPixmapUriOnly(const QSize& size, const char color) {
252 // We start with a transparent avatar
253 QPixmap avatar(size);
254 avatar.fill(Qt::transparent);
255
256 // We pick a color based on the passed character
257 QColor avColor = ImageManipulationDelegate::avatarColors_[color % 16];
258
259 // We draw a circle with this color
260 QPainter painter(&avatar);
261 painter.setRenderHints(QPainter::Antialiasing|QPainter::SmoothPixmapTransform);
262 painter.setPen(Qt::transparent);
263 painter.setBrush(avColor);
264 painter.drawEllipse(avatar.rect());
265
266 // Then we paint the avatar in the circle
267 QRect textRect = avatar.rect();
268 QImage defaultAvatarImage;
269 QRect rect = QRect(0, 0, size.width(), size.height());
270 NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
271 NSString *imagePath = [bundleURL.absoluteString stringByAppendingString:@"Contents/Resources/default_avatar_overlay.png"];
272 if (defaultAvatarImage.load(QString::fromNSString(imagePath).mid(7))) {
273 painter.drawImage(avatar.rect(), defaultAvatarImage);
274 } else {
275 painter.drawText(avatar.rect(), QString('?'), QTextOption(Qt::AlignCenter));
276 }
277
278 return avatar;
279 }
280
Alexandre Lision7a166e42015-09-02 15:04:43 -0400281 CGImageRef ImageManipulationDelegate::resizeCGImage(CGImageRef image, const QSize& size) {
282 // create context, keeping original image properties
Alexandre Lision7a166e42015-09-02 15:04:43 -0400283 CGContextRef context = CGBitmapContextCreate(NULL, size.width(), size.height(),
284 CGImageGetBitsPerComponent(image),
Alexandre Lisiond5229f32015-11-16 11:17:41 -0500285 CGImageGetBytesPerRow(image),
286 CGImageGetColorSpace(image),
287 kCGImageAlphaPremultipliedLast);
Alexandre Lision7a166e42015-09-02 15:04:43 -0400288
289 if(context == NULL)
290 return nil;
291
292 // draw image to context (resizing it)
293 CGContextDrawImage(context, CGRectMake(0, 0, size.width(), size.height()), image);
294 // extract resulting image from context
295 CGImageRef imgRef = CGBitmapContextCreateImage(context);
296 CGContextRelease(context);
297
298 return imgRef;
299 }
300
301 QVariant
302 ImageManipulationDelegate::numberCategoryIcon(const QVariant& p, const QSize& size, bool displayPresence, bool isPresent)
303 {
304 Q_UNUSED(p)
305 Q_UNUSED(size)
306 Q_UNUSED(displayPresence)
307 Q_UNUSED(isPresent)
308 return QVariant();
309 }
310
311 QVariant
Alexandre Lision7a166e42015-09-02 15:04:43 -0400312 ImageManipulationDelegate::userActionIcon(const UserActionElement& state) const
313 {
314 Q_UNUSED(state)
315 return QVariant();
Alexandre Lision3b0bd332015-03-15 18:43:07 -0400316 }
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400317
Stepan Salenikovich3a6ea302016-01-08 13:54:34 -0500318 QVariant ImageManipulationDelegate::decorationRole(const QModelIndex& index)
319 {
320 Q_UNUSED(index)
321 return QVariant();
322 }
Alexandre Lision7a166e42015-09-02 15:04:43 -0400323} // namespace Interfaces