Path: blob/master/src/applications/files/favicon/PhabricatorFaviconRef.php
12241 views
<?php12final class PhabricatorFaviconRef extends Phobject {34private $viewer;5private $width;6private $height;7private $emblems;8private $uri;9private $cacheKey;1011public function __construct() {12$this->emblems = array(null, null, null, null);13}1415public function setViewer(PhabricatorUser $viewer) {16$this->viewer = $viewer;17return $this;18}1920public function getViewer() {21return $this->viewer;22}2324public function setWidth($width) {25$this->width = $width;26return $this;27}2829public function getWidth() {30return $this->width;31}3233public function setHeight($height) {34$this->height = $height;35return $this;36}3738public function getHeight() {39return $this->height;40}4142public function setEmblems(array $emblems) {43if (count($emblems) !== 4) {44throw new Exception(45pht(46'Expected four elements in icon emblem list. To omit an emblem, '.47'pass "null".'));48}4950$this->emblems = $emblems;51return $this;52}5354public function getEmblems() {55return $this->emblems;56}5758public function setURI($uri) {59$this->uri = $uri;60return $this;61}6263public function getURI() {64return $this->uri;65}6667public function setCacheKey($cache_key) {68$this->cacheKey = $cache_key;69return $this;70}7172public function getCacheKey() {73return $this->cacheKey;74}7576public function newDigest() {77return PhabricatorHash::digestForIndex(serialize($this->toDictionary()));78}7980public function toDictionary() {81return array(82'width' => $this->width,83'height' => $this->height,84'emblems' => $this->emblems,85);86}8788public static function newConfigurationDigest() {89$all_resources = self::getAllResources();9091// Because we need to access this cache on every page, it's very sticky.92// Try to dirty it automatically if any relevant configuration changes.93$inputs = array(94'resources' => $all_resources,95'prod' => PhabricatorEnv::getProductionURI('/'),96'cdn' => PhabricatorEnv::getEnvConfig('security.alternate-file-domain'),97'havepng' => function_exists('imagepng'),98);99100return PhabricatorHash::digestForIndex(serialize($inputs));101}102103private static function getAllResources() {104$custom_resources = PhabricatorEnv::getEnvConfig('ui.favicons');105106foreach ($custom_resources as $key => $custom_resource) {107$custom_resources[$key] = array(108'source-type' => 'file',109'default' => false,110) + $custom_resource;111}112113$builtin_resources = self::getBuiltinResources();114115return array_merge($builtin_resources, $custom_resources);116}117118private static function getBuiltinResources() {119return array(120array(121'source-type' => 'builtin',122'source' => 'favicon/default-76x76.png',123'version' => 1,124'width' => 76,125'height' => 76,126'default' => true,127),128array(129'source-type' => 'builtin',130'source' => 'favicon/default-120x120.png',131'version' => 1,132'width' => 120,133'height' => 120,134'default' => true,135),136array(137'source-type' => 'builtin',138'source' => 'favicon/default-128x128.png',139'version' => 1,140'width' => 128,141'height' => 128,142'default' => true,143),144array(145'source-type' => 'builtin',146'source' => 'favicon/default-152x152.png',147'version' => 1,148'width' => 152,149'height' => 152,150'default' => true,151),152array(153'source-type' => 'builtin',154'source' => 'favicon/dot-pink-64x64.png',155'version' => 1,156'width' => 64,157'height' => 64,158'emblem' => 'dot-pink',159'default' => true,160),161array(162'source-type' => 'builtin',163'source' => 'favicon/dot-red-64x64.png',164'version' => 1,165'width' => 64,166'height' => 64,167'emblem' => 'dot-red',168'default' => true,169),170);171}172173public function newURI() {174$dst_w = $this->getWidth();175$dst_h = $this->getHeight();176177$template = $this->newTemplateFile(null, $dst_w, $dst_h);178$template_file = $template['file'];179180$cache = $this->loadCachedFile($template_file);181if ($cache) {182return $cache->getViewURI();183}184185$data = $this->newCompositedFavicon($template);186187$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();188189$caught = null;190try {191$favicon_file = $this->newFaviconFile($data);192193$xform = id(new PhabricatorTransformedFile())194->setOriginalPHID($template_file->getPHID())195->setTransformedPHID($favicon_file->getPHID())196->setTransform($this->getCacheKey());197198try {199$xform->save();200} catch (AphrontDuplicateKeyQueryException $ex) {201unset($unguarded);202203$cache = $this->loadCachedFile($template_file);204if (!$cache) {205throw $ex;206}207208id(new PhabricatorDestructionEngine())209->destroyObject($favicon_file);210211return $cache->getViewURI();212}213} catch (Exception $ex) {214$caught = $ex;215}216217unset($unguarded);218219if ($caught) {220throw $caught;221}222223return $favicon_file->getViewURI();224}225226private function loadCachedFile(PhabricatorFile $template_file) {227$viewer = $this->getViewer();228229$xform = id(new PhabricatorTransformedFile())->loadOneWhere(230'originalPHID = %s AND transform = %s',231$template_file->getPHID(),232$this->getCacheKey());233if (!$xform) {234return null;235}236237return id(new PhabricatorFileQuery())238->setViewer($viewer)239->withPHIDs(array($xform->getTransformedPHID()))240->executeOne();241}242243private function newCompositedFavicon($template) {244$dst_w = $this->getWidth();245$dst_h = $this->getHeight();246$src_w = $template['width'];247$src_h = $template['height'];248249try {250$template_data = $template['file']->loadFileData();251} catch (Exception $ex) {252// In rare cases, we can end up with a corrupted or inaccessible file.253// If we do, just give up: otherwise, it's impossible to get pages to254// generate and not obvious how to fix it.255return null;256}257258if (!function_exists('imagecreatefromstring')) {259return $template_data;260}261262$src = @imagecreatefromstring($template_data);263if (!$src) {264return $template_data;265}266267$dst = imagecreatetruecolor($dst_w, $dst_h);268imagesavealpha($dst, true);269270$transparent = imagecolorallocatealpha($dst, 0, 255, 0, 127);271imagefill($dst, 0, 0, $transparent);272273imagecopyresampled(274$dst,275$src,2760,2770,2780,2790,280$dst_w,281$dst_h,282$src_w,283$src_h);284285// Now, copy any icon emblems on top of the image. These are dots or other286// marks used to indicate status information.287$emblem_w = (int)floor(min($dst_w, $dst_h) / 2);288$emblem_h = $emblem_w;289foreach ($this->emblems as $key => $emblem) {290if ($emblem === null) {291continue;292}293294$emblem_template = $this->newTemplateFile(295$emblem,296$emblem_w,297$emblem_h);298299switch ($key) {300case 0:301$emblem_x = $dst_w - $emblem_w;302$emblem_y = 0;303break;304case 1:305$emblem_x = $dst_w - $emblem_w;306$emblem_y = $dst_h - $emblem_h;307break;308case 2:309$emblem_x = 0;310$emblem_y = $dst_h - $emblem_h;311break;312case 3:313$emblem_x = 0;314$emblem_y = 0;315break;316}317318$emblem_data = $emblem_template['file']->loadFileData();319320$src = @imagecreatefromstring($emblem_data);321if (!$src) {322continue;323}324325imagecopyresampled(326$dst,327$src,328$emblem_x,329$emblem_y,3300,3310,332$emblem_w,333$emblem_h,334$emblem_template['width'],335$emblem_template['height']);336}337338return PhabricatorImageTransformer::saveImageDataInAnyFormat(339$dst,340'image/png');341}342343private function newTemplateFile($emblem, $width, $height) {344$all_resources = self::getAllResources();345346$scores = array();347$ratio = $width / $height;348foreach ($all_resources as $key => $resource) {349// We can't use an emblem resource for a different emblem, nor for an350// icon base. We also can't use an icon base as an emblem. That is, if351// we're looking for a picture of a red dot, we have to actually find352// a red dot, not just any image which happens to have a similar size.353if (idx($resource, 'emblem') !== $emblem) {354continue;355}356357$resource_width = $resource['width'];358$resource_height = $resource['height'];359360// Never use a resource with a different aspect ratio.361if (($resource_width / $resource_height) !== $ratio) {362continue;363}364365// Try to use custom resources instead of default resources.366if ($resource['default']) {367$default_score = 1;368} else {369$default_score = 0;370}371372$width_diff = ($resource_width - $width);373374// If we have to resize an image, we'd rather scale a larger image down375// than scale a smaller image up.376if ($width_diff < 0) {377$scale_score = 1;378} else {379$scale_score = 0;380}381382// Otherwise, we'd rather scale an image a little bit (ideally, zero)383// than scale an image a lot.384$width_score = abs($width_diff);385386$scores[$key] = id(new PhutilSortVector())387->addInt($default_score)388->addInt($scale_score)389->addInt($width_score);390}391392if (!$scores) {393if ($emblem === null) {394throw new Exception(395pht(396'Found no background template resource for dimensions %dx%d.',397$width,398$height));399} else {400throw new Exception(401pht(402'Found no template resource (for emblem "%s") with dimensions '.403'%dx%d.',404$emblem,405$width,406$height));407}408}409410$scores = msortv($scores, 'getSelf');411$best_score = head_key($scores);412413$viewer = $this->getViewer();414415$resource = $all_resources[$best_score];416if ($resource['source-type'] === 'builtin') {417$file = PhabricatorFile::loadBuiltin($viewer, $resource['source']);418if (!$file) {419throw new Exception(420pht(421'Failed to load favicon template builtin "%s".',422$resource['source']));423}424} else {425$file = id(new PhabricatorFileQuery())426->setViewer($viewer)427->withPHIDs(array($resource['source']))428->executeOne();429if (!$file) {430throw new Exception(431pht(432'Failed to load favicon template with PHID "%s".',433$resource['source']));434}435}436437return array(438'width' => $resource['width'],439'height' => $resource['height'],440'file' => $file,441);442}443444private function newFaviconFile($data) {445return PhabricatorFile::newFromFileData(446$data,447array(448'name' => 'favicon',449'canCDN' => true,450));451}452453}454455456