Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/files/favicon/PhabricatorFaviconRef.php
12241 views
1
<?php
2
3
final class PhabricatorFaviconRef extends Phobject {
4
5
private $viewer;
6
private $width;
7
private $height;
8
private $emblems;
9
private $uri;
10
private $cacheKey;
11
12
public function __construct() {
13
$this->emblems = array(null, null, null, null);
14
}
15
16
public function setViewer(PhabricatorUser $viewer) {
17
$this->viewer = $viewer;
18
return $this;
19
}
20
21
public function getViewer() {
22
return $this->viewer;
23
}
24
25
public function setWidth($width) {
26
$this->width = $width;
27
return $this;
28
}
29
30
public function getWidth() {
31
return $this->width;
32
}
33
34
public function setHeight($height) {
35
$this->height = $height;
36
return $this;
37
}
38
39
public function getHeight() {
40
return $this->height;
41
}
42
43
public function setEmblems(array $emblems) {
44
if (count($emblems) !== 4) {
45
throw new Exception(
46
pht(
47
'Expected four elements in icon emblem list. To omit an emblem, '.
48
'pass "null".'));
49
}
50
51
$this->emblems = $emblems;
52
return $this;
53
}
54
55
public function getEmblems() {
56
return $this->emblems;
57
}
58
59
public function setURI($uri) {
60
$this->uri = $uri;
61
return $this;
62
}
63
64
public function getURI() {
65
return $this->uri;
66
}
67
68
public function setCacheKey($cache_key) {
69
$this->cacheKey = $cache_key;
70
return $this;
71
}
72
73
public function getCacheKey() {
74
return $this->cacheKey;
75
}
76
77
public function newDigest() {
78
return PhabricatorHash::digestForIndex(serialize($this->toDictionary()));
79
}
80
81
public function toDictionary() {
82
return array(
83
'width' => $this->width,
84
'height' => $this->height,
85
'emblems' => $this->emblems,
86
);
87
}
88
89
public static function newConfigurationDigest() {
90
$all_resources = self::getAllResources();
91
92
// Because we need to access this cache on every page, it's very sticky.
93
// Try to dirty it automatically if any relevant configuration changes.
94
$inputs = array(
95
'resources' => $all_resources,
96
'prod' => PhabricatorEnv::getProductionURI('/'),
97
'cdn' => PhabricatorEnv::getEnvConfig('security.alternate-file-domain'),
98
'havepng' => function_exists('imagepng'),
99
);
100
101
return PhabricatorHash::digestForIndex(serialize($inputs));
102
}
103
104
private static function getAllResources() {
105
$custom_resources = PhabricatorEnv::getEnvConfig('ui.favicons');
106
107
foreach ($custom_resources as $key => $custom_resource) {
108
$custom_resources[$key] = array(
109
'source-type' => 'file',
110
'default' => false,
111
) + $custom_resource;
112
}
113
114
$builtin_resources = self::getBuiltinResources();
115
116
return array_merge($builtin_resources, $custom_resources);
117
}
118
119
private static function getBuiltinResources() {
120
return array(
121
array(
122
'source-type' => 'builtin',
123
'source' => 'favicon/default-76x76.png',
124
'version' => 1,
125
'width' => 76,
126
'height' => 76,
127
'default' => true,
128
),
129
array(
130
'source-type' => 'builtin',
131
'source' => 'favicon/default-120x120.png',
132
'version' => 1,
133
'width' => 120,
134
'height' => 120,
135
'default' => true,
136
),
137
array(
138
'source-type' => 'builtin',
139
'source' => 'favicon/default-128x128.png',
140
'version' => 1,
141
'width' => 128,
142
'height' => 128,
143
'default' => true,
144
),
145
array(
146
'source-type' => 'builtin',
147
'source' => 'favicon/default-152x152.png',
148
'version' => 1,
149
'width' => 152,
150
'height' => 152,
151
'default' => true,
152
),
153
array(
154
'source-type' => 'builtin',
155
'source' => 'favicon/dot-pink-64x64.png',
156
'version' => 1,
157
'width' => 64,
158
'height' => 64,
159
'emblem' => 'dot-pink',
160
'default' => true,
161
),
162
array(
163
'source-type' => 'builtin',
164
'source' => 'favicon/dot-red-64x64.png',
165
'version' => 1,
166
'width' => 64,
167
'height' => 64,
168
'emblem' => 'dot-red',
169
'default' => true,
170
),
171
);
172
}
173
174
public function newURI() {
175
$dst_w = $this->getWidth();
176
$dst_h = $this->getHeight();
177
178
$template = $this->newTemplateFile(null, $dst_w, $dst_h);
179
$template_file = $template['file'];
180
181
$cache = $this->loadCachedFile($template_file);
182
if ($cache) {
183
return $cache->getViewURI();
184
}
185
186
$data = $this->newCompositedFavicon($template);
187
188
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
189
190
$caught = null;
191
try {
192
$favicon_file = $this->newFaviconFile($data);
193
194
$xform = id(new PhabricatorTransformedFile())
195
->setOriginalPHID($template_file->getPHID())
196
->setTransformedPHID($favicon_file->getPHID())
197
->setTransform($this->getCacheKey());
198
199
try {
200
$xform->save();
201
} catch (AphrontDuplicateKeyQueryException $ex) {
202
unset($unguarded);
203
204
$cache = $this->loadCachedFile($template_file);
205
if (!$cache) {
206
throw $ex;
207
}
208
209
id(new PhabricatorDestructionEngine())
210
->destroyObject($favicon_file);
211
212
return $cache->getViewURI();
213
}
214
} catch (Exception $ex) {
215
$caught = $ex;
216
}
217
218
unset($unguarded);
219
220
if ($caught) {
221
throw $caught;
222
}
223
224
return $favicon_file->getViewURI();
225
}
226
227
private function loadCachedFile(PhabricatorFile $template_file) {
228
$viewer = $this->getViewer();
229
230
$xform = id(new PhabricatorTransformedFile())->loadOneWhere(
231
'originalPHID = %s AND transform = %s',
232
$template_file->getPHID(),
233
$this->getCacheKey());
234
if (!$xform) {
235
return null;
236
}
237
238
return id(new PhabricatorFileQuery())
239
->setViewer($viewer)
240
->withPHIDs(array($xform->getTransformedPHID()))
241
->executeOne();
242
}
243
244
private function newCompositedFavicon($template) {
245
$dst_w = $this->getWidth();
246
$dst_h = $this->getHeight();
247
$src_w = $template['width'];
248
$src_h = $template['height'];
249
250
try {
251
$template_data = $template['file']->loadFileData();
252
} catch (Exception $ex) {
253
// In rare cases, we can end up with a corrupted or inaccessible file.
254
// If we do, just give up: otherwise, it's impossible to get pages to
255
// generate and not obvious how to fix it.
256
return null;
257
}
258
259
if (!function_exists('imagecreatefromstring')) {
260
return $template_data;
261
}
262
263
$src = @imagecreatefromstring($template_data);
264
if (!$src) {
265
return $template_data;
266
}
267
268
$dst = imagecreatetruecolor($dst_w, $dst_h);
269
imagesavealpha($dst, true);
270
271
$transparent = imagecolorallocatealpha($dst, 0, 255, 0, 127);
272
imagefill($dst, 0, 0, $transparent);
273
274
imagecopyresampled(
275
$dst,
276
$src,
277
0,
278
0,
279
0,
280
0,
281
$dst_w,
282
$dst_h,
283
$src_w,
284
$src_h);
285
286
// Now, copy any icon emblems on top of the image. These are dots or other
287
// marks used to indicate status information.
288
$emblem_w = (int)floor(min($dst_w, $dst_h) / 2);
289
$emblem_h = $emblem_w;
290
foreach ($this->emblems as $key => $emblem) {
291
if ($emblem === null) {
292
continue;
293
}
294
295
$emblem_template = $this->newTemplateFile(
296
$emblem,
297
$emblem_w,
298
$emblem_h);
299
300
switch ($key) {
301
case 0:
302
$emblem_x = $dst_w - $emblem_w;
303
$emblem_y = 0;
304
break;
305
case 1:
306
$emblem_x = $dst_w - $emblem_w;
307
$emblem_y = $dst_h - $emblem_h;
308
break;
309
case 2:
310
$emblem_x = 0;
311
$emblem_y = $dst_h - $emblem_h;
312
break;
313
case 3:
314
$emblem_x = 0;
315
$emblem_y = 0;
316
break;
317
}
318
319
$emblem_data = $emblem_template['file']->loadFileData();
320
321
$src = @imagecreatefromstring($emblem_data);
322
if (!$src) {
323
continue;
324
}
325
326
imagecopyresampled(
327
$dst,
328
$src,
329
$emblem_x,
330
$emblem_y,
331
0,
332
0,
333
$emblem_w,
334
$emblem_h,
335
$emblem_template['width'],
336
$emblem_template['height']);
337
}
338
339
return PhabricatorImageTransformer::saveImageDataInAnyFormat(
340
$dst,
341
'image/png');
342
}
343
344
private function newTemplateFile($emblem, $width, $height) {
345
$all_resources = self::getAllResources();
346
347
$scores = array();
348
$ratio = $width / $height;
349
foreach ($all_resources as $key => $resource) {
350
// We can't use an emblem resource for a different emblem, nor for an
351
// icon base. We also can't use an icon base as an emblem. That is, if
352
// we're looking for a picture of a red dot, we have to actually find
353
// a red dot, not just any image which happens to have a similar size.
354
if (idx($resource, 'emblem') !== $emblem) {
355
continue;
356
}
357
358
$resource_width = $resource['width'];
359
$resource_height = $resource['height'];
360
361
// Never use a resource with a different aspect ratio.
362
if (($resource_width / $resource_height) !== $ratio) {
363
continue;
364
}
365
366
// Try to use custom resources instead of default resources.
367
if ($resource['default']) {
368
$default_score = 1;
369
} else {
370
$default_score = 0;
371
}
372
373
$width_diff = ($resource_width - $width);
374
375
// If we have to resize an image, we'd rather scale a larger image down
376
// than scale a smaller image up.
377
if ($width_diff < 0) {
378
$scale_score = 1;
379
} else {
380
$scale_score = 0;
381
}
382
383
// Otherwise, we'd rather scale an image a little bit (ideally, zero)
384
// than scale an image a lot.
385
$width_score = abs($width_diff);
386
387
$scores[$key] = id(new PhutilSortVector())
388
->addInt($default_score)
389
->addInt($scale_score)
390
->addInt($width_score);
391
}
392
393
if (!$scores) {
394
if ($emblem === null) {
395
throw new Exception(
396
pht(
397
'Found no background template resource for dimensions %dx%d.',
398
$width,
399
$height));
400
} else {
401
throw new Exception(
402
pht(
403
'Found no template resource (for emblem "%s") with dimensions '.
404
'%dx%d.',
405
$emblem,
406
$width,
407
$height));
408
}
409
}
410
411
$scores = msortv($scores, 'getSelf');
412
$best_score = head_key($scores);
413
414
$viewer = $this->getViewer();
415
416
$resource = $all_resources[$best_score];
417
if ($resource['source-type'] === 'builtin') {
418
$file = PhabricatorFile::loadBuiltin($viewer, $resource['source']);
419
if (!$file) {
420
throw new Exception(
421
pht(
422
'Failed to load favicon template builtin "%s".',
423
$resource['source']));
424
}
425
} else {
426
$file = id(new PhabricatorFileQuery())
427
->setViewer($viewer)
428
->withPHIDs(array($resource['source']))
429
->executeOne();
430
if (!$file) {
431
throw new Exception(
432
pht(
433
'Failed to load favicon template with PHID "%s".',
434
$resource['source']));
435
}
436
}
437
438
return array(
439
'width' => $resource['width'],
440
'height' => $resource['height'],
441
'file' => $file,
442
);
443
}
444
445
private function newFaviconFile($data) {
446
return PhabricatorFile::newFromFileData(
447
$data,
448
array(
449
'name' => 'favicon',
450
'canCDN' => true,
451
));
452
}
453
454
}
455
456