Passed
Push — master ( b69b17...32a6f4 )
by Morris
11:33
created

Avatar::generateAvatarFromSvg()   A

Complexity

Conditions 3
Paths 10

Size

Total Lines 16
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 14
nc 10
nop 1
dl 0
loc 16
rs 9.7998
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
/**
4
 * @copyright Copyright (c) 2016, ownCloud, Inc.
5
 * @copyright 2018 John Molakvoæ <[email protected]>
6
 *
7
 * @author Arthur Schiwon <[email protected]>
8
 * @author Christopher Schäpers <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Olivier Mehani <[email protected]>
12
 * @author Robin Appelman <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 * @author John Molakvoæ <[email protected]>
16
 *
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
33
namespace OC\Avatar;
34
35
use OC\Color;
36
use OCP\Files\NotFoundException;
37
use OCP\IAvatar;
38
use OCP\ILogger;
39
use OC_Image;
40
use Imagick;
41
42
/**
43
 * This class gets and sets users avatars.
44
 */
45
abstract class Avatar implements IAvatar {
46
47
	/** @var ILogger  */
48
	protected $logger;
49
50
	/**
51
	 * https://github.com/sebdesign/cap-height -- for 500px height
52
	 * Automated check: https://codepen.io/skjnldsv/pen/PydLBK/
53
	 * Nunito cap-height is 0.716 and we want a 200px caps height size
54
	 * (0.4 letter-to-total-height ratio, 500*0.4=200), so: 200/0.716 = 279px.
55
	 * Since we start from the baseline (text-anchor) we need to
56
	 * shift the y axis by 100px (half the caps height): 500/2+100=350
57
	 *
58
	 * @var string
59
	 */
60
	private $svgTemplate = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>
61
		<svg width="{size}" height="{size}" version="1.1" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
62
			<rect width="100%" height="100%" fill="#{fill}"></rect>
63
			<text x="50%" y="350" style="font-weight:normal;font-size:279px;font-family:\'Nunito\';text-anchor:middle;fill:#fff">{letter}</text>
64
		</svg>';
65
66
	/**
67
	 * The base avatar constructor.
68
	 *
69
	 * @param ILogger $logger The logger
70
	 */
71
	public function __construct(ILogger $logger) {
72
		$this->logger = $logger;
73
	}
74
75
	/**
76
	 * Returns the user display name.
77
	 *
78
	 * @return string
79
	 */
80
	abstract public function getDisplayName(): string;
81
82
	/**
83
	 * Returns the first letter of the display name, or "?" if no name given.
84
	 *
85
	 * @return string
86
	 */
87
	private function getAvatarLetter(): string {
88
		$displayName = $this->getDisplayName();
89
		if (empty($displayName) === true) {
90
			return '?';
91
		} else {
92
			return mb_strtoupper(mb_substr($displayName, 0, 1), 'UTF-8');
93
		}
94
	}
95
96
	/**
97
	 * @inheritdoc
98
	 */
99
	public function get($size = 64) {
100
		$size = (int) $size;
101
102
		try {
103
			$file = $this->getFile($size);
104
		} catch (NotFoundException $e) {
105
			return false;
106
		}
107
108
		$avatar = new OC_Image();
109
		$avatar->loadFromData($file->getContent());
110
		return $avatar;
111
	}
112
113
	/**
114
	 * {size} = 500
115
	 * {fill} = hex color to fill
116
	 * {letter} = Letter to display
117
	 *
118
	 * Generate SVG avatar
119
	 *
120
	 * @param int $size The requested image size in pixel
121
	 * @return string
122
	 *
123
	 */
124
	protected function getAvatarVector(int $size): string {
125
		$userDisplayName = $this->getDisplayName();
126
		$bgRGB = $this->avatarBackgroundColor($userDisplayName);
127
		$bgHEX = sprintf("%02x%02x%02x", $bgRGB->r, $bgRGB->g, $bgRGB->b);
128
		$letter = $this->getAvatarLetter();
129
		$toReplace = ['{size}', '{fill}', '{letter}'];
130
		return str_replace($toReplace, [$size, $bgHEX, $letter], $this->svgTemplate);
131
	}
132
133
	/**
134
	 * Generate png avatar from svg with Imagick
135
	 *
136
	 * @param int $size
137
	 * @return string|boolean
138
	 */
139
	protected function generateAvatarFromSvg(int $size) {
140
		if (!extension_loaded('imagick')) {
141
			return false;
142
		}
143
		try {
144
			$font = __DIR__ . '/../../core/fonts/Nunito-Regular.ttf';
145
			$svg = $this->getAvatarVector($size);
146
			$avatar = new Imagick();
147
			$avatar->setFont($font);
148
			$avatar->readImageBlob($svg);
149
			$avatar->setImageFormat('png');
150
			$image = new OC_Image();
151
			$image->loadFromData($avatar);
152
			return $image->data();
153
		} catch (\Exception $e) {
154
			return false;
155
		}
156
	}
157
158
	/**
159
	 * Generate png avatar with GD
160
	 *
161
	 * @param string $userDisplayName
162
	 * @param int $size
163
	 * @return string
164
	 */
165
	protected function generateAvatar($userDisplayName, $size) {
166
		$letter = $this->getAvatarLetter();
167
		$backgroundColor = $this->avatarBackgroundColor($userDisplayName);
168
169
		$im = imagecreatetruecolor($size, $size);
170
		$background = imagecolorallocate(
171
			$im,
172
			$backgroundColor->r,
173
			$backgroundColor->g,
174
			$backgroundColor->b
175
		);
176
		$white = imagecolorallocate($im, 255, 255, 255);
177
		imagefilledrectangle($im, 0, 0, $size, $size, $background);
178
179
		$font = __DIR__ . '/../../../core/fonts/Nunito-Regular.ttf';
180
181
		$fontSize = $size * 0.4;
182
		list($x, $y) = $this->imageTTFCenter(
183
			$im, $letter, $font, (int)$fontSize
184
		);
185
186
		imagettftext($im, $fontSize, 0, $x, $y, $white, $font, $letter);
187
188
		ob_start();
189
		imagepng($im);
190
		$data = ob_get_contents();
191
		ob_end_clean();
192
193
		return $data;
194
	}
195
196
	/**
197
	 * Calculate real image ttf center
198
	 *
199
	 * @param resource $image
200
	 * @param string $text text string
201
	 * @param string $font font path
202
	 * @param int $size font size
203
	 * @param int $angle
204
	 * @return array
205
	 */
206
	protected function imageTTFCenter(
207
		$image,
208
		string $text,
209
		string $font,
210
		int $size,
211
		$angle = 0
212
	): array {
213
		// Image width & height
214
		$xi = imagesx($image);
215
		$yi = imagesy($image);
216
217
		// bounding box
218
		$box = imagettfbbox($size, $angle, $font, $text);
219
220
		// imagettfbbox can return negative int
221
		$xr = abs(max($box[2], $box[4]));
222
		$yr = abs(max($box[5], $box[7]));
223
224
		// calculate bottom left placement
225
		$x = intval(($xi - $xr) / 2);
226
		$y = intval(($yi + $yr) / 2);
227
228
		return array($x, $y);
229
	}
230
231
	/**
232
	 * Calculate steps between two Colors
233
	 * @param object Color $steps start color
234
	 * @param object Color $ends end color
235
	 * @return array [r,g,b] steps for each color to go from $steps to $ends
236
	 */
237
	private function stepCalc($steps, $ends) {
238
		$step = array();
239
		$step[0] = ($ends[1]->r - $ends[0]->r) / $steps;
240
		$step[1] = ($ends[1]->g - $ends[0]->g) / $steps;
241
		$step[2] = ($ends[1]->b - $ends[0]->b) / $steps;
242
		return $step;
243
	}
244
245
	/**
246
	 * Convert a string to an integer evenly
247
	 * @param string $hash the text to parse
248
	 * @param int $maximum the maximum range
249
	 * @return int[] between 0 and $maximum
250
	 */
251
	private function mixPalette($steps, $color1, $color2) {
252
		$palette = array($color1);
253
		$step = $this->stepCalc($steps, [$color1, $color2]);
254
		for ($i = 1; $i < $steps; $i++) {
255
			$r = intval($color1->r + ($step[0] * $i));
256
			$g = intval($color1->g + ($step[1] * $i));
257
			$b = intval($color1->b + ($step[2] * $i));
258
			$palette[] = new Color($r, $g, $b);
259
		}
260
		return $palette;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $palette returns an array which contains values of type OC\Color which are incompatible with the documented value type integer.
Loading history...
261
	}
262
263
	/**
264
	 * Convert a string to an integer evenly
265
	 * @param string $hash the text to parse
266
	 * @param int $maximum the maximum range
267
	 * @return int between 0 and $maximum
268
	 */
269
	private function hashToInt($hash, $maximum) {
270
		$final = 0;
271
		$result = array();
272
273
		// Splitting evenly the string
274
		for ($i = 0; $i < strlen($hash); $i++) {
275
			// chars in md5 goes up to f, hex:16
276
			$result[] = intval(substr($hash, $i, 1), 16) % 16;
277
		}
278
		// Adds up all results
279
		foreach ($result as $value) {
280
			$final += $value;
281
		}
282
		// chars in md5 goes up to f, hex:16
283
		return intval($final % $maximum);
284
	}
285
286
	/**
287
	 * @param string $hash
288
	 * @return Color Object containting r g b int in the range [0, 255]
289
	 */
290
	public function avatarBackgroundColor(string $hash) {
291
		// Normalize hash
292
		$hash = strtolower($hash);
293
294
		// Already a md5 hash?
295
		if( preg_match('/^([0-9a-f]{4}-?){8}$/', $hash, $matches) !== 1 ) {
296
			$hash = md5($hash);
297
		}
298
299
		// Remove unwanted char
300
		$hash = preg_replace('/[^0-9a-f]+/', '', $hash);
301
302
		$red = new Color(182, 70, 157);
303
		$yellow = new Color(221, 203, 85);
304
		$blue = new Color(0, 130, 201); // Nextcloud blue
305
306
		// Number of steps to go from a color to another
307
		// 3 colors * 6 will result in 18 generated colors
308
		$steps = 6;
309
310
		$palette1 = $this->mixPalette($steps, $red, $yellow);
311
		$palette2 = $this->mixPalette($steps, $yellow, $blue);
312
		$palette3 = $this->mixPalette($steps, $blue, $red);
313
314
		$finalPalette = array_merge($palette1, $palette2, $palette3);
315
316
		return $finalPalette[$this->hashToInt($hash, $steps * 3)];
317
	}
318
}
319