|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
declare(strict_types=1); |
|
4
|
|
|
|
|
5
|
|
|
/** |
|
6
|
|
|
* @copyright Copyright (c) 2016, ownCloud, Inc. |
|
7
|
|
|
* @copyright 2018 John Molakvoæ <[email protected]> |
|
8
|
|
|
* |
|
9
|
|
|
* @author Christopher Schäpers <[email protected]> |
|
10
|
|
|
* @author Christoph Wurst <[email protected]> |
|
11
|
|
|
* @author Jan-Christoph Borchardt <[email protected]> |
|
12
|
|
|
* @author Joas Schilling <[email protected]> |
|
13
|
|
|
* @author John Molakvoæ <[email protected]> |
|
14
|
|
|
* @author Julius Härtl <[email protected]> |
|
15
|
|
|
* @author Michael Weimann <[email protected]> |
|
16
|
|
|
* @author Morris Jobke <[email protected]> |
|
17
|
|
|
* @author Robin Appelman <[email protected]> |
|
18
|
|
|
* @author Roeland Jago Douma <[email protected]> |
|
19
|
|
|
* @author Sergey Shliakhov <[email protected]> |
|
20
|
|
|
* @author Thomas Müller <[email protected]> |
|
21
|
|
|
* @author Pauli Järvinen <[email protected]> |
|
22
|
|
|
* |
|
23
|
|
|
* @license AGPL-3.0 |
|
24
|
|
|
* |
|
25
|
|
|
* This code is free software: you can redistribute it and/or modify |
|
26
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
|
27
|
|
|
* as published by the Free Software Foundation. |
|
28
|
|
|
* |
|
29
|
|
|
* This program is distributed in the hope that it will be useful, |
|
30
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
31
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
32
|
|
|
* GNU Affero General Public License for more details. |
|
33
|
|
|
* |
|
34
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
|
35
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
|
36
|
|
|
* |
|
37
|
|
|
*/ |
|
38
|
|
|
namespace OCA\Music\Utility; |
|
39
|
|
|
|
|
40
|
|
|
use OCA\Music\Utility\PlaceholderImage\Color; |
|
41
|
|
|
|
|
42
|
|
|
/** |
|
43
|
|
|
* Create placeholder images. This is a modified and stripped-down copy of https://github.com/nextcloud/server/blob/master/lib/private/Avatar/Avatar.php. |
|
44
|
|
|
*/ |
|
45
|
|
|
class PlaceholderImage { |
|
46
|
|
|
|
|
47
|
|
|
public static function generate(string $name, string $seed, int $size): string { |
|
48
|
|
|
$text = self::getText($name); |
|
49
|
|
|
$backgroundColor = self::getBackgroundColor($seed); |
|
50
|
|
|
|
|
51
|
|
|
$im = \imagecreatetruecolor($size, $size); |
|
52
|
|
|
$background = \imagecolorallocate( |
|
53
|
|
|
$im, |
|
54
|
|
|
$backgroundColor->red(), |
|
55
|
|
|
$backgroundColor->green(), |
|
56
|
|
|
$backgroundColor->blue() |
|
57
|
|
|
); |
|
58
|
|
|
$white = \imagecolorallocate($im, 255, 255, 255); |
|
59
|
|
|
\imagefilledrectangle($im, 0, 0, $size, $size, $background); |
|
60
|
|
|
|
|
61
|
|
|
$font = self::findFontPath(); |
|
62
|
|
|
|
|
63
|
|
|
$fontSize = $size * 0.4; |
|
64
|
|
|
[$x, $y] = self::imageTtfCenter($im, $text, $font, (int)$fontSize); |
|
|
|
|
|
|
65
|
|
|
|
|
66
|
|
|
\imagettftext($im, $fontSize, 0, $x, $y, $white, $font, $text); |
|
67
|
|
|
|
|
68
|
|
|
\ob_start(); |
|
69
|
|
|
\imagepng($im); |
|
70
|
|
|
$data = \ob_get_contents(); |
|
71
|
|
|
\ob_end_clean(); |
|
72
|
|
|
|
|
73
|
|
|
return $data; |
|
74
|
|
|
} |
|
75
|
|
|
|
|
76
|
|
|
private static function findFontPath() : string { |
|
77
|
|
|
// try to find the font from the hosting cloud instance first |
|
78
|
|
|
$files = \glob(__DIR__ . '/../../../../core/fonts/*Regular.ttf'); |
|
79
|
|
|
|
|
80
|
|
|
// If the suitable font was not found from the core, then fall back to our local font. |
|
81
|
|
|
// The drawback is that this font contains only the Latin alphabet. |
|
82
|
|
|
return $files[0] ?? __DIR__ . '/../../fonts/OpenSans-Regular.woff'; |
|
83
|
|
|
} |
|
84
|
|
|
|
|
85
|
|
|
/** |
|
86
|
|
|
* Returns the first letter of the display name, or "?" if no name given. |
|
87
|
|
|
*/ |
|
88
|
|
|
private static function getText(string $displayName): string { |
|
89
|
|
|
if (empty($displayName) === true) { |
|
90
|
|
|
return '?'; |
|
91
|
|
|
} |
|
92
|
|
|
$firstTwoLetters = array_map(function ($namePart) { |
|
93
|
|
|
return \mb_strtoupper(mb_substr($namePart, 0, 1), 'UTF-8'); |
|
94
|
|
|
}, explode(' ', $displayName, 2)); |
|
95
|
|
|
return \implode('', $firstTwoLetters); |
|
96
|
|
|
} |
|
97
|
|
|
|
|
98
|
|
|
/** |
|
99
|
|
|
* Calculate real image ttf center |
|
100
|
|
|
* |
|
101
|
|
|
* @param resource $image |
|
102
|
|
|
* @param string $text text string |
|
103
|
|
|
* @param string $font font path |
|
104
|
|
|
* @param int $size font size |
|
105
|
|
|
* @param int $angle |
|
106
|
|
|
* @return int[] |
|
107
|
|
|
*/ |
|
108
|
|
|
private static function imageTtfCenter($image, string $text, string $font, int $size, int $angle = 0): array { |
|
109
|
|
|
// Image width & height |
|
110
|
|
|
$xi = imagesx($image); |
|
111
|
|
|
$yi = imagesy($image); |
|
112
|
|
|
|
|
113
|
|
|
// bounding box |
|
114
|
|
|
$box = \imagettfbbox($size, $angle, $font, $text); |
|
115
|
|
|
|
|
116
|
|
|
// imagettfbbox can return negative int |
|
117
|
|
|
$xr = \abs(\max($box[2], $box[4])); |
|
118
|
|
|
$yr = \abs(\max($box[5], $box[7])); |
|
119
|
|
|
|
|
120
|
|
|
// calculate bottom left placement |
|
121
|
|
|
$x = \intval(($xi - $xr) / 2); |
|
122
|
|
|
$y = \intval(($yi + $yr) / 2); |
|
123
|
|
|
|
|
124
|
|
|
return [$x, $y]; |
|
125
|
|
|
} |
|
126
|
|
|
|
|
127
|
|
|
|
|
128
|
|
|
/** |
|
129
|
|
|
* Convert a string to an integer evenly |
|
130
|
|
|
* @param string $hash the text to parse |
|
131
|
|
|
* @param int $maximum the maximum range |
|
132
|
|
|
* @return int between 0 and $maximum |
|
133
|
|
|
*/ |
|
134
|
|
|
private static function hashToInt(string $hash, int $maximum): int { |
|
135
|
|
|
$final = 0; |
|
136
|
|
|
$result = []; |
|
137
|
|
|
|
|
138
|
|
|
// Splitting evenly the string |
|
139
|
|
|
for ($i = 0; $i < \strlen($hash); $i++) { |
|
140
|
|
|
// chars in md5 goes up to f, hex:16 |
|
141
|
|
|
$result[] = \intval(\substr($hash, $i, 1), 16) % 16; |
|
142
|
|
|
} |
|
143
|
|
|
// Adds up all results |
|
144
|
|
|
foreach ($result as $value) { |
|
145
|
|
|
$final += $value; |
|
146
|
|
|
} |
|
147
|
|
|
// chars in md5 goes up to f, hex:16 |
|
148
|
|
|
return \intval($final % $maximum); |
|
149
|
|
|
} |
|
150
|
|
|
|
|
151
|
|
|
/** |
|
152
|
|
|
* @return Color Object containing r g b int in the range [0, 255] |
|
153
|
|
|
*/ |
|
154
|
|
|
private static function getBackgroundColor(string $hash): Color { |
|
155
|
|
|
// Normalize hash |
|
156
|
|
|
$hash = \strtolower($hash); |
|
157
|
|
|
|
|
158
|
|
|
// Already a md5 hash? |
|
159
|
|
|
if (preg_match('/^([0-9a-f]{4}-?){8}$/', $hash, $matches) !== 1) { |
|
160
|
|
|
$hash = \md5($hash); |
|
161
|
|
|
} |
|
162
|
|
|
|
|
163
|
|
|
// Remove unwanted char |
|
164
|
|
|
$hash = \preg_replace('/[^0-9a-f]+/', '', $hash); |
|
165
|
|
|
|
|
166
|
|
|
$red = new Color(182, 70, 157); |
|
167
|
|
|
$yellow = new Color(221, 203, 85); |
|
168
|
|
|
$blue = new Color(0, 130, 201); // Nextcloud blue |
|
169
|
|
|
|
|
170
|
|
|
// Number of steps to go from a color to another |
|
171
|
|
|
// 3 colors * 6 will result in 18 generated colors |
|
172
|
|
|
$steps = 6; |
|
173
|
|
|
|
|
174
|
|
|
$palette1 = Color::mixPalette($steps, $red, $yellow); |
|
175
|
|
|
$palette2 = Color::mixPalette($steps, $yellow, $blue); |
|
176
|
|
|
$palette3 = Color::mixPalette($steps, $blue, $red); |
|
177
|
|
|
|
|
178
|
|
|
$finalPalette = \array_merge($palette1, $palette2, $palette3); |
|
179
|
|
|
|
|
180
|
|
|
return $finalPalette[self::hashToInt($hash, $steps * 3)]; |
|
181
|
|
|
} |
|
182
|
|
|
} |
|
183
|
|
|
|
|
184
|
|
|
|
|
185
|
|
|
namespace OCA\Music\Utility\PlaceholderImage; |
|
186
|
|
|
|
|
187
|
|
|
/** |
|
188
|
|
|
* A strripped-down copy of https://github.com/nextcloud/server/blob/master/lib/public/Color.php |
|
189
|
|
|
*/ |
|
190
|
|
|
class Color { |
|
191
|
|
|
private $r; |
|
192
|
|
|
private $g; |
|
193
|
|
|
private $b; |
|
194
|
|
|
|
|
195
|
|
|
public function __construct(int $r, int $g, int $b) { |
|
196
|
|
|
$this->r = $r; |
|
197
|
|
|
$this->g = $g; |
|
198
|
|
|
$this->b = $b; |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
public function red(): int { |
|
202
|
|
|
return $this->r; |
|
203
|
|
|
} |
|
204
|
|
|
|
|
205
|
|
|
public function green(): int { |
|
206
|
|
|
return $this->g; |
|
207
|
|
|
} |
|
208
|
|
|
|
|
209
|
|
|
public function blue(): int { |
|
210
|
|
|
return $this->b; |
|
211
|
|
|
} |
|
212
|
|
|
|
|
213
|
|
|
/** |
|
214
|
|
|
* Mix two colors |
|
215
|
|
|
* |
|
216
|
|
|
* @param int $steps the number of intermediate colors that should be generated for the palette |
|
217
|
|
|
* @param Color $color1 the first color |
|
218
|
|
|
* @param Color $color2 the second color |
|
219
|
|
|
* @return Color[] |
|
220
|
|
|
*/ |
|
221
|
|
|
public static function mixPalette(int $steps, Color $color1, Color $color2): array { |
|
222
|
|
|
$palette = [$color1]; |
|
223
|
|
|
$step = self::stepCalc($steps, [$color1, $color2]); |
|
224
|
|
|
for ($i = 1; $i < $steps; $i++) { |
|
225
|
|
|
$r = intval($color1->red() + ($step[0] * $i)); |
|
226
|
|
|
$g = intval($color1->green() + ($step[1] * $i)); |
|
227
|
|
|
$b = intval($color1->blue() + ($step[2] * $i)); |
|
228
|
|
|
$palette[] = new Color($r, $g, $b); |
|
229
|
|
|
} |
|
230
|
|
|
return $palette; |
|
231
|
|
|
} |
|
232
|
|
|
|
|
233
|
|
|
/** |
|
234
|
|
|
* Calculate steps between two Colors |
|
235
|
|
|
* @param int $steps start color |
|
236
|
|
|
* @param Color[] $ends end color |
|
237
|
|
|
* @return array{0: int, 1: int, 2: int} [r,g,b] steps for each color to go from $steps to $ends |
|
238
|
|
|
*/ |
|
239
|
|
|
private static function stepCalc(int $steps, array $ends): array { |
|
240
|
|
|
return [ |
|
241
|
|
|
($ends[1]->red() - $ends[0]->red()) / $steps, |
|
242
|
|
|
($ends[1]->green() - $ends[0]->green()) / $steps, |
|
243
|
|
|
($ends[1]->blue() - $ends[0]->blue()) / $steps |
|
244
|
|
|
]; |
|
245
|
|
|
} |
|
246
|
|
|
} |