3 final class PhabricatorFaviconRef
extends Phobject
{
12 public function __construct() {
13 $this->emblems
= array(null, null, null, null);
16 public function setViewer(PhabricatorUser
$viewer) {
17 $this->viewer
= $viewer;
21 public function getViewer() {
25 public function setWidth($width) {
26 $this->width
= $width;
30 public function getWidth() {
34 public function setHeight($height) {
35 $this->height
= $height;
39 public function getHeight() {
43 public function setEmblems(array $emblems) {
44 if (count($emblems) !== 4) {
47 'Expected four elements in icon emblem list. To omit an emblem, '.
51 $this->emblems
= $emblems;
55 public function getEmblems() {
56 return $this->emblems
;
59 public function setURI($uri) {
64 public function getURI() {
68 public function setCacheKey($cache_key) {
69 $this->cacheKey
= $cache_key;
73 public function getCacheKey() {
74 return $this->cacheKey
;
77 public function newDigest() {
78 return PhabricatorHash
::digestForIndex(serialize($this->toDictionary()));
81 public function toDictionary() {
83 'width' => $this->width
,
84 'height' => $this->height
,
85 'emblems' => $this->emblems
,
89 public static function newConfigurationDigest() {
90 $all_resources = self
::getAllResources();
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.
95 'resources' => $all_resources,
96 'prod' => PhabricatorEnv
::getProductionURI('/'),
97 'cdn' => PhabricatorEnv
::getEnvConfig('security.alternate-file-domain'),
98 'havepng' => function_exists('imagepng'),
101 return PhabricatorHash
::digestForIndex(serialize($inputs));
104 private static function getAllResources() {
105 $custom_resources = PhabricatorEnv
::getEnvConfig('ui.favicons');
107 foreach ($custom_resources as $key => $custom_resource) {
108 $custom_resources[$key] = array(
109 'source-type' => 'file',
111 ) +
$custom_resource;
114 $builtin_resources = self
::getBuiltinResources();
116 return array_merge($builtin_resources, $custom_resources);
119 private static function getBuiltinResources() {
122 'source-type' => 'builtin',
123 'source' => 'favicon/default-76x76.png',
130 'source-type' => 'builtin',
131 'source' => 'favicon/default-120x120.png',
138 'source-type' => 'builtin',
139 'source' => 'favicon/default-128x128.png',
146 'source-type' => 'builtin',
147 'source' => 'favicon/default-152x152.png',
154 'source-type' => 'builtin',
155 'source' => 'favicon/dot-pink-64x64.png',
159 'emblem' => 'dot-pink',
163 'source-type' => 'builtin',
164 'source' => 'favicon/dot-red-64x64.png',
168 'emblem' => 'dot-red',
174 public function newURI() {
175 $dst_w = $this->getWidth();
176 $dst_h = $this->getHeight();
178 $template = $this->newTemplateFile(null, $dst_w, $dst_h);
179 $template_file = $template['file'];
181 $cache = $this->loadCachedFile($template_file);
183 return $cache->getViewURI();
186 $data = $this->newCompositedFavicon($template);
188 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
192 $favicon_file = $this->newFaviconFile($data);
194 $xform = id(new PhabricatorTransformedFile())
195 ->setOriginalPHID($template_file->getPHID())
196 ->setTransformedPHID($favicon_file->getPHID())
197 ->setTransform($this->getCacheKey());
201 } catch (AphrontDuplicateKeyQueryException
$ex) {
204 $cache = $this->loadCachedFile($template_file);
209 id(new PhabricatorDestructionEngine())
210 ->destroyObject($favicon_file);
212 return $cache->getViewURI();
214 } catch (Exception
$ex) {
224 return $favicon_file->getViewURI();
227 private function loadCachedFile(PhabricatorFile
$template_file) {
228 $viewer = $this->getViewer();
230 $xform = id(new PhabricatorTransformedFile())->loadOneWhere(
231 'originalPHID = %s AND transform = %s',
232 $template_file->getPHID(),
233 $this->getCacheKey());
238 return id(new PhabricatorFileQuery())
240 ->withPHIDs(array($xform->getTransformedPHID()))
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'];
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.
259 if (!function_exists('imagecreatefromstring')) {
260 return $template_data;
263 $src = @imagecreatefromstring
($template_data);
265 return $template_data;
268 $dst = imagecreatetruecolor($dst_w, $dst_h);
269 imagesavealpha($dst, true);
271 $transparent = imagecolorallocatealpha($dst, 0, 255, 0, 127);
272 imagefill($dst, 0, 0, $transparent);
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) {
295 $emblem_template = $this->newTemplateFile(
302 $emblem_x = $dst_w - $emblem_w;
306 $emblem_x = $dst_w - $emblem_w;
307 $emblem_y = $dst_h - $emblem_h;
311 $emblem_y = $dst_h - $emblem_h;
319 $emblem_data = $emblem_template['file']->loadFileData();
321 $src = @imagecreatefromstring
($emblem_data);
335 $emblem_template['width'],
336 $emblem_template['height']);
339 return PhabricatorImageTransformer
::saveImageDataInAnyFormat(
344 private function newTemplateFile($emblem, $width, $height) {
345 $all_resources = self
::getAllResources();
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) {
358 $resource_width = $resource['width'];
359 $resource_height = $resource['height'];
361 // Never use a resource with a different aspect ratio.
362 if (($resource_width / $resource_height) !== $ratio) {
366 // Try to use custom resources instead of default resources.
367 if ($resource['default']) {
373 $width_diff = ($resource_width - $width);
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) {
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);
387 $scores[$key] = id(new PhutilSortVector())
388 ->addInt($default_score)
389 ->addInt($scale_score)
390 ->addInt($width_score);
394 if ($emblem === null) {
397 'Found no background template resource for dimensions %dx%d.',
403 'Found no template resource (for emblem "%s") with dimensions '.
411 $scores = msortv($scores, 'getSelf');
412 $best_score = head_key($scores);
414 $viewer = $this->getViewer();
416 $resource = $all_resources[$best_score];
417 if ($resource['source-type'] === 'builtin') {
418 $file = PhabricatorFile
::loadBuiltin($viewer, $resource['source']);
422 'Failed to load favicon template builtin "%s".',
423 $resource['source']));
426 $file = id(new PhabricatorFileQuery())
428 ->withPHIDs(array($resource['source']))
433 'Failed to load favicon template with PHID "%s".',
434 $resource['source']));
439 'width' => $resource['width'],
440 'height' => $resource['height'],
445 private function newFaviconFile($data) {
446 return PhabricatorFile
::newFromFileData(