Changeset View
Changeset View
Standalone View
Standalone View
src/applications/files/favicon/PhabricatorFaviconRef.php
- This file was added.
| <?php | |||||
| final class PhabricatorFaviconRef extends Phobject { | |||||
| private $viewer; | |||||
| private $width; | |||||
| private $height; | |||||
| private $emblems; | |||||
| private $uri; | |||||
| private $cacheKey; | |||||
| public function __construct() { | |||||
| $this->emblems = array(null, null, null, null); | |||||
| } | |||||
| public function setViewer(PhabricatorUser $viewer) { | |||||
| $this->viewer = $viewer; | |||||
| return $this; | |||||
| } | |||||
| public function getViewer() { | |||||
| return $this->viewer; | |||||
| } | |||||
| public function setWidth($width) { | |||||
| $this->width = $width; | |||||
| return $this; | |||||
| } | |||||
| public function getWidth() { | |||||
| return $this->width; | |||||
| } | |||||
| public function setHeight($height) { | |||||
| $this->height = $height; | |||||
| return $this; | |||||
| } | |||||
| public function getHeight() { | |||||
| return $this->height; | |||||
| } | |||||
| public function setEmblems(array $emblems) { | |||||
| if (count($emblems) !== 4) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Expected four elements in icon emblem list. To omit an emblem, '. | |||||
| 'pass "null".')); | |||||
| } | |||||
| $this->emblems = $emblems; | |||||
| return $this; | |||||
| } | |||||
| public function getEmblems() { | |||||
| return $this->emblems; | |||||
| } | |||||
| public function setURI($uri) { | |||||
| $this->uri = $uri; | |||||
| return $this; | |||||
| } | |||||
| public function getURI() { | |||||
| return $this->uri; | |||||
| } | |||||
| public function setCacheKey($cache_key) { | |||||
| $this->cacheKey = $cache_key; | |||||
| return $this; | |||||
| } | |||||
| public function getCacheKey() { | |||||
| return $this->cacheKey; | |||||
| } | |||||
| public function newDigest() { | |||||
| return PhabricatorHash::digestForIndex(serialize($this->toDictionary())); | |||||
| } | |||||
| public function toDictionary() { | |||||
| return array( | |||||
| 'width' => $this->width, | |||||
| 'height' => $this->height, | |||||
| 'emblems' => $this->emblems, | |||||
| ); | |||||
| } | |||||
| public static function newConfigurationDigest() { | |||||
| $all_resources = self::getAllResources(); | |||||
| // Because we need to access this cache on every page, it's very sticky. | |||||
| // Try to dirty it automatically if any relevant configuration changes. | |||||
| $inputs = array( | |||||
| 'resources' => $all_resources, | |||||
| 'prod' => PhabricatorEnv::getProductionURI('/'), | |||||
| 'cdn' => PhabricatorEnv::getEnvConfig('security.alternate-file-domain'), | |||||
| 'havepng' => function_exists('imagepng'), | |||||
| ); | |||||
| return PhabricatorHash::digestForIndex(serialize($inputs)); | |||||
| } | |||||
| private static function getAllResources() { | |||||
| $custom_resources = PhabricatorEnv::getEnvConfig('ui.favicons'); | |||||
| foreach ($custom_resources as $key => $custom_resource) { | |||||
| $custom_resources[$key] = array( | |||||
| 'source-type' => 'file', | |||||
| 'default' => false, | |||||
| ) + $custom_resource; | |||||
| } | |||||
| $builtin_resources = self::getBuiltinResources(); | |||||
| return array_merge($builtin_resources, $custom_resources); | |||||
| } | |||||
| private static function getBuiltinResources() { | |||||
| return array( | |||||
| array( | |||||
| 'source-type' => 'builtin', | |||||
| 'source' => 'favicon/default-76x76.png', | |||||
| 'version' => 1, | |||||
| 'width' => 76, | |||||
| 'height' => 76, | |||||
| 'default' => true, | |||||
| ), | |||||
| array( | |||||
| 'source-type' => 'builtin', | |||||
| 'source' => 'favicon/default-120x120.png', | |||||
| 'version' => 1, | |||||
| 'width' => 120, | |||||
| 'height' => 120, | |||||
| 'default' => true, | |||||
| ), | |||||
| array( | |||||
| 'source-type' => 'builtin', | |||||
| 'source' => 'favicon/default-128x128.png', | |||||
| 'version' => 1, | |||||
| 'width' => 128, | |||||
| 'height' => 128, | |||||
| 'default' => true, | |||||
| ), | |||||
| array( | |||||
| 'source-type' => 'builtin', | |||||
| 'source' => 'favicon/default-152x152.png', | |||||
| 'version' => 1, | |||||
| 'width' => 152, | |||||
| 'height' => 152, | |||||
| 'default' => true, | |||||
| ), | |||||
| array( | |||||
| 'source-type' => 'builtin', | |||||
| 'source' => 'favicon/dot-pink-64x64.png', | |||||
| 'version' => 1, | |||||
| 'width' => 64, | |||||
| 'height' => 64, | |||||
| 'emblem' => 'dot-pink', | |||||
| 'default' => true, | |||||
| ), | |||||
| array( | |||||
| 'source-type' => 'builtin', | |||||
| 'source' => 'favicon/dot-red-64x64.png', | |||||
| 'version' => 1, | |||||
| 'width' => 64, | |||||
| 'height' => 64, | |||||
| 'emblem' => 'dot-red', | |||||
| 'default' => true, | |||||
| ), | |||||
| ); | |||||
| } | |||||
| public function newURI() { | |||||
| $dst_w = $this->getWidth(); | |||||
| $dst_h = $this->getHeight(); | |||||
| $template = $this->newTemplateFile(null, $dst_w, $dst_h); | |||||
| $template_file = $template['file']; | |||||
| $cache = $this->loadCachedFile($template_file); | |||||
| if ($cache) { | |||||
| return $cache->getViewURI(); | |||||
| } | |||||
| $data = $this->newCompositedFavicon($template); | |||||
| $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); | |||||
| $caught = null; | |||||
| try { | |||||
| $favicon_file = $this->newFaviconFile($data); | |||||
| $xform = id(new PhabricatorTransformedFile()) | |||||
| ->setOriginalPHID($template_file->getPHID()) | |||||
| ->setTransformedPHID($favicon_file->getPHID()) | |||||
| ->setTransform($this->getCacheKey()); | |||||
| try { | |||||
| $xform->save(); | |||||
| } catch (AphrontDuplicateKeyQueryException $ex) { | |||||
| unset($unguarded); | |||||
| $cache = $this->loadCachedFile($template_file); | |||||
| if (!$cache) { | |||||
| throw $ex; | |||||
| } | |||||
| id(new PhabricatorDestructionEngine()) | |||||
| ->destroyObject($favicon_file); | |||||
| return $cache->getViewURI(); | |||||
| } | |||||
| } catch (Exception $ex) { | |||||
| $caught = $ex; | |||||
| } | |||||
| unset($unguarded); | |||||
| if ($caught) { | |||||
| throw $caught; | |||||
| } | |||||
| return $favicon_file->getViewURI(); | |||||
| } | |||||
| private function loadCachedFile(PhabricatorFile $template_file) { | |||||
| $viewer = $this->getViewer(); | |||||
| $xform = id(new PhabricatorTransformedFile())->loadOneWhere( | |||||
| 'originalPHID = %s AND transform = %s', | |||||
| $template_file->getPHID(), | |||||
| $this->getCacheKey()); | |||||
| if (!$xform) { | |||||
| return null; | |||||
| } | |||||
| return id(new PhabricatorFileQuery()) | |||||
| ->setViewer($viewer) | |||||
| ->withPHIDs(array($xform->getTransformedPHID())) | |||||
| ->executeOne(); | |||||
| } | |||||
| private function newCompositedFavicon($template) { | |||||
| $dst_w = $this->getWidth(); | |||||
| $dst_h = $this->getHeight(); | |||||
| $src_w = $template['width']; | |||||
| $src_h = $template['height']; | |||||
| $template_data = $template['file']->loadFileData(); | |||||
| if (!function_exists('imagecreatefromstring')) { | |||||
| return $template_data; | |||||
| } | |||||
| $src = @imagecreatefromstring($template_data); | |||||
| if (!$src) { | |||||
| return $template_data; | |||||
| } | |||||
| $dst = imagecreatetruecolor($dst_w, $dst_h); | |||||
| imagesavealpha($dst, true); | |||||
| $transparent = imagecolorallocatealpha($dst, 0, 255, 0, 127); | |||||
| imagefill($dst, 0, 0, $transparent); | |||||
| imagecopyresampled( | |||||
| $dst, | |||||
| $src, | |||||
| 0, | |||||
| 0, | |||||
| 0, | |||||
| 0, | |||||
| $dst_w, | |||||
| $dst_h, | |||||
| $src_w, | |||||
| $src_h); | |||||
| // Now, copy any icon emblems on top of the image. These are dots or other | |||||
| // marks used to indicate status information. | |||||
| $emblem_w = (int)floor(min($dst_w, $dst_h) / 2); | |||||
| $emblem_h = $emblem_w; | |||||
| foreach ($this->emblems as $key => $emblem) { | |||||
| if ($emblem === null) { | |||||
| continue; | |||||
| } | |||||
| $emblem_template = $this->newTemplateFile( | |||||
| $emblem, | |||||
| $emblem_w, | |||||
| $emblem_h); | |||||
| switch ($key) { | |||||
| case 0: | |||||
| $emblem_x = $dst_w - $emblem_w; | |||||
| $emblem_y = 0; | |||||
| break; | |||||
| case 1: | |||||
| $emblem_x = $dst_w - $emblem_w; | |||||
| $emblem_y = $dst_h - $emblem_h; | |||||
| break; | |||||
| case 2: | |||||
| $emblem_x = 0; | |||||
| $emblem_y = $dst_h - $emblem_h; | |||||
| break; | |||||
| case 3: | |||||
| $emblem_x = 0; | |||||
| $emblem_y = 0; | |||||
| break; | |||||
| } | |||||
| $emblem_data = $emblem_template['file']->loadFileData(); | |||||
| $src = @imagecreatefromstring($emblem_data); | |||||
| if (!$src) { | |||||
| continue; | |||||
| } | |||||
| imagecopyresampled( | |||||
| $dst, | |||||
| $src, | |||||
| $emblem_x, | |||||
| $emblem_y, | |||||
| 0, | |||||
| 0, | |||||
| $emblem_w, | |||||
| $emblem_h, | |||||
| $emblem_template['width'], | |||||
| $emblem_template['height']); | |||||
| } | |||||
| return PhabricatorImageTransformer::saveImageDataInAnyFormat( | |||||
| $dst, | |||||
| 'image/png'); | |||||
| } | |||||
| private function newTemplateFile($emblem, $width, $height) { | |||||
| $all_resources = self::getAllResources(); | |||||
| $scores = array(); | |||||
| $ratio = $width / $height; | |||||
| foreach ($all_resources as $key => $resource) { | |||||
| // We can't use an emblem resource for a different emblem, nor for an | |||||
| // icon base. We also can't use an icon base as an emblem. That is, if | |||||
| // we're looking for a picture of a red dot, we have to actually find | |||||
| // a red dot, not just any image which happens to have a similar size. | |||||
| if (idx($resource, 'emblem') !== $emblem) { | |||||
| continue; | |||||
| } | |||||
| $resource_width = $resource['width']; | |||||
| $resource_height = $resource['height']; | |||||
| // Never use a resource with a different aspect ratio. | |||||
| if (($resource_width / $resource_height) !== $ratio) { | |||||
| continue; | |||||
| } | |||||
| // Try to use custom resources instead of default resources. | |||||
| if ($resource['default']) { | |||||
| $default_score = 1; | |||||
| } else { | |||||
| $default_score = 0; | |||||
| } | |||||
| $width_diff = ($resource_width - $width); | |||||
| // If we have to resize an image, we'd rather scale a larger image down | |||||
| // than scale a smaller image up. | |||||
| if ($width_diff < 0) { | |||||
| $scale_score = 1; | |||||
| } else { | |||||
| $scale_score = 0; | |||||
| } | |||||
| // Otherwise, we'd rather scale an image a little bit (ideally, zero) | |||||
| // than scale an image a lot. | |||||
| $width_score = abs($width_diff); | |||||
| $scores[$key] = id(new PhutilSortVector()) | |||||
| ->addInt($default_score) | |||||
| ->addInt($scale_score) | |||||
| ->addInt($width_score); | |||||
| } | |||||
| if (!$scores) { | |||||
| if ($emblem === null) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Found no background template resource for dimensions %dx%d.', | |||||
| $width, | |||||
| $height)); | |||||
| } else { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Found no template resource (for emblem "%s") with dimensions '. | |||||
| '%dx%d.', | |||||
| $emblem, | |||||
| $width, | |||||
| $height)); | |||||
| } | |||||
| } | |||||
| $scores = msortv($scores, 'getSelf'); | |||||
| $best_score = head_key($scores); | |||||
| $viewer = $this->getViewer(); | |||||
| $resource = $all_resources[$best_score]; | |||||
| if ($resource['source-type'] === 'builtin') { | |||||
| $file = PhabricatorFile::loadBuiltin($viewer, $resource['source']); | |||||
| if (!$file) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Failed to load favicon template builtin "%s".', | |||||
| $resource['source'])); | |||||
| } | |||||
| } else { | |||||
| $file = id(new PhabricatorFileQuery()) | |||||
| ->setViewer($viewer) | |||||
| ->withPHIDs(array($resource['source'])) | |||||
| ->executeOne(); | |||||
| if (!$file) { | |||||
| throw new Exception( | |||||
| pht( | |||||
| 'Failed to load favicon template with PHID "%s".', | |||||
| $resource['source'])); | |||||
| } | |||||
| } | |||||
| return array( | |||||
| 'width' => $resource['width'], | |||||
| 'height' => $resource['height'], | |||||
| 'file' => $file, | |||||
| ); | |||||
| } | |||||
| private function newFaviconFile($data) { | |||||
| return PhabricatorFile::newFromFileData( | |||||
| $data, | |||||
| array( | |||||
| 'name' => 'favicon', | |||||
| 'canCDN' => true, | |||||
| )); | |||||
| } | |||||
| } | |||||