diff --git a/src/applications/files/PhabricatorImageTransformer.php b/src/applications/files/PhabricatorImageTransformer.php
index c3a10e4816..0ec46960cb 100644
--- a/src/applications/files/PhabricatorImageTransformer.php
+++ b/src/applications/files/PhabricatorImageTransformer.php
@@ -1,420 +1,426 @@
 <?php
 
 final class PhabricatorImageTransformer {
 
   public function executeMemeTransform(
     PhabricatorFile $file,
     $upper_text,
     $lower_text) {
     $image = $this->applyMemeTo($file, $upper_text, $lower_text);
     return PhabricatorFile::newFromFileData(
       $image,
       array(
         'name' => 'meme-'.$file->getName(),
         'ttl' => time() + 60 * 60 * 24,
       ));
   }
 
   public function executeThumbTransform(
     PhabricatorFile $file,
     $x,
     $y) {
 
     $image = $this->crudelyScaleTo($file, $x, $y);
 
     return PhabricatorFile::newFromFileData(
       $image,
       array(
         'name' => 'thumb-'.$file->getName(),
       ));
   }
 
   public function executeProfileTransform(
     PhabricatorFile $file,
     $x,
     $min_y,
     $max_y) {
 
     $image = $this->crudelyCropTo($file, $x, $min_y, $max_y);
 
     return PhabricatorFile::newFromFileData(
       $image,
       array(
         'name' => 'profile-'.$file->getName(),
       ));
   }
 
   public function executePreviewTransform(
     PhabricatorFile $file,
     $size) {
 
     $image = $this->generatePreview($file, $size);
 
     return PhabricatorFile::newFromFileData(
       $image,
       array(
         'name' => 'preview-'.$file->getName(),
       ));
   }
 
   public function executeConpherenceTransform(
     PhabricatorFile $file,
     $top,
     $left,
     $width,
     $height) {
 
     $image = $this->crasslyCropTo(
       $file,
       $top,
       $left,
       $width,
       $height);
 
     return PhabricatorFile::newFromFileData(
       $image,
       array(
         'name' => 'conpherence-'.$file->getName(),
       ));
   }
 
   private function crudelyCropTo(PhabricatorFile $file, $x, $min_y, $max_y) {
     $data = $file->loadFileData();
     $img = imagecreatefromstring($data);
     $sx = imagesx($img);
     $sy = imagesy($img);
 
     $scaled_y = ($x / $sx) * $sy;
     if ($scaled_y > $max_y) {
       // This image is very tall and thin.
       $scaled_y = $max_y;
     } else if ($scaled_y < $min_y) {
       // This image is very short and wide.
       $scaled_y = $min_y;
     }
 
     $cropped = $this->applyScaleWithImagemagick($file, $x, $scaled_y);
 
     if ($cropped != null) {
       return $cropped;
     }
 
     $img = $this->applyScaleTo(
       $file,
       $x,
       $scaled_y);
 
     return $this->saveImageDataInAnyFormat($img, $file->getMimeType());
   }
 
   private function crasslyCropTo(PhabricatorFile $file, $top, $left, $w, $h) {
     $data = $file->loadFileData();
     $src = imagecreatefromstring($data);
     $dst = $this->getBlankDestinationFile($w, $h);
 
     $scale = self::getScaleForCrop($file, $w, $h);
     $orig_x = $left / $scale;
     $orig_y = $top / $scale;
     $orig_w = $w / $scale;
     $orig_h = $h / $scale;
 
     imagecopyresampled(
       $dst,
       $src,
       0, 0,
       $orig_x, $orig_y,
       $w, $h,
       $orig_w, $orig_h);
 
     return $this->saveImageDataInAnyFormat($dst, $file->getMimeType());
   }
 
 
   /**
    * Very crudely scale an image up or down to an exact size.
    */
   private function crudelyScaleTo(PhabricatorFile $file, $dx, $dy) {
     $scaled = $this->applyScaleWithImagemagick($file, $dx, $dy);
 
     if ($scaled != null) {
       return $scaled;
     }
 
     $dst = $this->applyScaleTo($file, $dx, $dy);
     return $this->saveImageDataInAnyFormat($dst, $file->getMimeType());
   }
 
   private function getBlankDestinationFile($dx, $dy) {
     $dst = imagecreatetruecolor($dx, $dy);
     imagesavealpha($dst, true);
     imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
 
     return $dst;
   }
 
   private function applyScaleTo(PhabricatorFile $file, $dx, $dy) {
     $data = $file->loadFileData();
     $src = imagecreatefromstring($data);
 
     $x = imagesx($src);
     $y = imagesy($src);
 
     $scale = min(($dx / $x), ($dy / $y), 1);
 
     $sdx = $scale * $x;
     $sdy = $scale * $y;
 
     $dst = $this->getBlankDestinationFile($dx, $dy);
     imagesavealpha($dst, true);
     imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
 
     imagecopyresampled(
       $dst,
       $src,
       ($dx - $sdx) / 2,  ($dy - $sdy) / 2,
       0, 0,
       $sdx, $sdy,
       $x, $y);
 
     return $dst;
 
   }
 
   public static function getPreviewDimensions(PhabricatorFile $file, $size) {
-    $data = $file->loadFileData();
-    $src = imagecreatefromstring($data);
+    $metadata = $file->getMetadata();
+    $x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH);
+    $y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT);
 
-    $x = imagesx($src);
-    $y = imagesy($src);
+    if (!$x || !$y) {
+      $data = $file->loadFileData();
+      $src = imagecreatefromstring($data);
+
+      $x = imagesx($src);
+      $y = imagesy($src);
+    }
 
     $scale = min($size / $x, $size / $y, 1);
 
     $dx = max($size / 4, $scale * $x);
     $dy = max($size / 4, $scale * $y);
 
     $sdx = $scale * $x;
     $sdy = $scale * $y;
 
     return array(
       'x' => $x,
       'y' => $y,
       'dx' => $dx,
       'dy' => $dy,
       'sdx' => $sdx,
       'sdy' => $sdy
     );
   }
 
   public static function getScaleForCrop(
     PhabricatorFile $file,
     $des_width,
     $des_height) {
 
     $metadata = $file->getMetadata();
     $width = $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH];
     $height = $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT];
 
     if ($height < $des_height) {
       $scale = $height / $des_height;
     } else if ($width < $des_width) {
       $scale = $width / $des_width;
     } else {
       $scale_x = $des_width / $width;
       $scale_y = $des_height / $height;
       $scale = max($scale_x, $scale_y);
     }
 
     return $scale;
   }
   private function generatePreview(PhabricatorFile $file, $size) {
     $data = $file->loadFileData();
     $src = imagecreatefromstring($data);
 
     $dimensions = self::getPreviewDimensions($file, $size);
     $x = $dimensions['x'];
     $y = $dimensions['y'];
     $dx = $dimensions['dx'];
     $dy = $dimensions['dy'];
     $sdx = $dimensions['sdx'];
     $sdy = $dimensions['sdy'];
 
     $dst = $this->getBlankDestinationFile($dx, $dy);
 
     imagecopyresampled(
       $dst,
       $src,
       ($dx - $sdx) / 2, ($dy - $sdy) / 2,
       0, 0,
       $sdx, $sdy,
       $x, $y);
 
     return $this->saveImageDataInAnyFormat($dst, $file->getMimeType());
   }
 
   private function applyMemeTo(
     PhabricatorFile $file,
     $upper_text,
     $lower_text) {
     $data = $file->loadFileData();
     $img = imagecreatefromstring($data);
     $phabricator_root = dirname(phutil_get_library_root('phabricator'));
     $font_root = $phabricator_root.'/resources/font/';
     $font_path = $font_root.'tuffy.ttf';
     if (Filesystem::pathExists($font_root.'impact.ttf')) {
       $font_path = $font_root.'impact.ttf';
     }
     $text_color = imagecolorallocate($img, 255, 255, 255);
     $border_color = imagecolorallocatealpha($img, 0, 0, 0, 110);
     $border_width = 4;
     $font_max = 200;
     $font_min = 5;
     for ($i = $font_max; $i > $font_min; $i--) {
       $fit = $this->doesTextBoundingBoxFitInImage(
         $img,
         $upper_text,
         $i,
         $font_path);
       if ($fit['doesfit']) {
         $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
         $y = $fit['txtheight'] + 10;
         $this->makeImageWithTextBorder($img,
           $i,
           $x,
           $y,
           $text_color,
           $border_color,
           $border_width,
           $font_path,
           $upper_text);
         break;
       }
     }
     for ($i = $font_max; $i > $font_min; $i--) {
       $fit = $this->doesTextBoundingBoxFitInImage($img,
         $lower_text, $i, $font_path);
       if ($fit['doesfit']) {
         $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
         $y = $fit['imgheight'] - 10;
         $this->makeImageWithTextBorder(
           $img,
           $i,
           $x,
           $y,
           $text_color,
           $border_color,
           $border_width,
           $font_path,
           $lower_text);
         break;
       }
     }
     return $this->saveImageDataInAnyFormat($img, $file->getMimeType());
   }
 
   private function makeImageWithTextBorder($img, $font_size, $x, $y,
     $color, $stroke_color, $bw, $font, $text) {
     $angle = 0;
     $bw = abs($bw);
     for ($c1 = $x - $bw; $c1 <= $x + $bw; $c1++) {
       for ($c2 = $y - $bw; $c2 <= $y + $bw; $c2++) {
         if (!(($c1 == $x - $bw || $x + $bw) &&
           $c2 == $y - $bw || $c2 == $y + $bw)) {
           $bg = imagettftext($img, $font_size,
             $angle, $c1, $c2, $stroke_color, $font, $text);
           }
         }
       }
     imagettftext($img, $font_size, $angle,
             $x , $y, $color , $font, $text);
   }
 
   private function doesTextBoundingBoxFitInImage($img,
     $text, $font_size, $font_path) {
     // Default Angle = 0
     $angle = 0;
 
     $bbox = imagettfbbox($font_size, $angle, $font_path, $text);
     $text_height = abs($bbox[3] - $bbox[5]);
     $text_width = abs($bbox[0] - $bbox[2]);
     return array(
       "doesfit" => ($text_height * 1.05 <= imagesy($img) / 2
         && $text_width * 1.05 <= imagesx($img)),
       "txtwidth" => $text_width,
       "txtheight" => $text_height,
       "imgwidth" => imagesx($img),
       "imgheight" => imagesy($img),
     );
   }
 
   private function saveImageDataInAnyFormat($data, $preferred_mime = '') {
     switch ($preferred_mime) {
       case 'image/gif': // Gif doesn't support true color
       case 'image/png':
         if (function_exists('imagepng')) {
           ob_start();
           imagepng($data);
           return ob_get_clean();
         }
         break;
     }
 
     $img = null;
 
     if (function_exists('imagejpeg')) {
       ob_start();
       imagejpeg($data);
       $img = ob_get_clean();
     } else if (function_exists('imagepng')) {
       ob_start();
       imagepng($data);
       $img = ob_get_clean();
     } else if (function_exists('imagegif')) {
       ob_start();
       imagegif($data);
       $img = ob_get_clean();
     } else {
       throw new Exception("No image generation functions exist!");
     }
 
     return $img;
   }
 
   private function applyScaleWithImagemagick(PhabricatorFile $file, $dx, $dy) {
 
     $img_type = $file->getMimeType();
     $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
 
     if ($img_type != 'image/gif' || $imagemagick == false) {
       return null;
     }
 
     $data = $file->loadFileData();
     $src = imagecreatefromstring($data);
 
     $x = imagesx($src);
     $y = imagesy($src);
 
     $scale = min(($dx / $x), ($dy / $y), 1);
 
     $sdx = $scale * $x;
     $sdy = $scale * $y;
 
     $input = new TempFile();
     Filesystem::writeFile($input, $data);
 
     $resized = new TempFile();
 
     list($err) = exec_manual(
                  'convert %s -coalesce -resize %sX%s\! %s'
                   , $input, $sdx, $sdy, $resized);
 
     if (!$err) {
       $new_data = Filesystem::readFile($resized);
       return $new_data;
     } else {
       return null;
     }
 
   }
 
 }
diff --git a/src/applications/files/controller/PhabricatorFileTransformController.php b/src/applications/files/controller/PhabricatorFileTransformController.php
index 90f926439a..3cf5adeece 100644
--- a/src/applications/files/controller/PhabricatorFileTransformController.php
+++ b/src/applications/files/controller/PhabricatorFileTransformController.php
@@ -1,146 +1,149 @@
 <?php
 
 final class PhabricatorFileTransformController
   extends PhabricatorFileController {
 
   private $transform;
   private $phid;
   private $key;
 
   public function willProcessRequest(array $data) {
     $this->transform = $data['transform'];
     $this->phid      = $data['phid'];
     $this->key       = $data['key'];
   }
 
   public function shouldRequireLogin() {
     return false;
   }
 
   public function processRequest() {
 
     $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $this->phid);
     if (!$file) {
       return new Aphront404Response();
     }
 
     if (!$file->validateSecretKey($this->key)) {
       return new Aphront403Response();
     }
 
     $xform = id(new PhabricatorTransformedFile())
       ->loadOneWhere(
         'originalPHID = %s AND transform = %s',
         $this->phid,
         $this->transform);
 
     if ($xform) {
       return $this->buildTransformedFileResponse($xform);
     }
 
     $type = $file->getMimeType();
 
     if (!$file->isViewableInBrowser() || !$file->isTransformableImage()) {
       return $this->buildDefaultTransformation($file);
     }
 
     // We're essentially just building a cache here and don't need CSRF
     // protection.
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
     switch ($this->transform) {
       case 'thumb-220x165':
         $xformed_file = $this->executeThumbTransform($file, 220, 165);
         break;
+      case 'preview-140':
+        $xformed_file = $this->executePreviewTransform($file, 140);
+        break;
       case 'preview-220':
         $xformed_file = $this->executePreviewTransform($file, 220);
         break;
       case 'thumb-160x120':
         $xformed_file = $this->executeThumbTransform($file, 160, 120);
         break;
       case 'thumb-60x45':
         $xformed_file = $this->executeThumbTransform($file, 60, 45);
         break;
       default:
         return new Aphront400Response();
     }
 
     if (!$xformed_file) {
       return new Aphront400Response();
     }
 
     $xform = new PhabricatorTransformedFile();
     $xform->setOriginalPHID($this->phid);
     $xform->setTransform($this->transform);
     $xform->setTransformedPHID($xformed_file->getPHID());
     $xform->save();
 
     return $this->buildTransformedFileResponse($xform);
   }
 
   private function buildDefaultTransformation(PhabricatorFile $file) {
     static $regexps = array(
       '@application/zip@'     => 'zip',
       '@image/@'              => 'image',
       '@application/pdf@'     => 'pdf',
       '@.*@'                  => 'default',
     );
 
     $type = $file->getMimeType();
     $prefix = 'default';
     foreach ($regexps as $regexp => $implied_prefix) {
       if (preg_match($regexp, $type)) {
         $prefix = $implied_prefix;
         break;
       }
     }
 
     switch ($this->transform) {
       case 'thumb-160x120':
         $suffix = '160x120';
         break;
       case 'thumb-60x45':
         $suffix = '60x45';
         break;
       default:
         throw new Exception("Unsupported transformation type!");
     }
 
     $path = celerity_get_resource_uri(
       "/rsrc/image/icon/fatcow/thumbnails/{$prefix}{$suffix}.png");
 
     return id(new AphrontRedirectResponse())
       ->setURI($path);
   }
 
   private function buildTransformedFileResponse(
     PhabricatorTransformedFile $xform) {
 
     $file = id(new PhabricatorFile())->loadOneWhere(
       'phid = %s',
       $xform->getTransformedPHID());
     if ($file) {
       $uri = $file->getBestURI();
     } else {
       $bad_phid = $xform->getTransformedPHID();
       throw new Exception(
         "Unable to load file with phid {$bad_phid}."
       );
     }
 
     // TODO: We could just delegate to the file view controller instead,
     // which would save the client a roundtrip, but is slightly more complex.
     return id(new AphrontRedirectResponse())->setURI($uri);
   }
 
   private function executePreviewTransform(PhabricatorFile $file, $size) {
     $xformer = new PhabricatorImageTransformer();
     return $xformer->executePreviewTransform($file, $size);
   }
 
   private function executeThumbTransform(PhabricatorFile $file, $x, $y) {
     $xformer = new PhabricatorImageTransformer();
     return $xformer->executeThumbTransform($file, $x, $y);
   }
 
 }
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
index 1ddbb34c7a..1f3c46e20d 100644
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -1,656 +1,662 @@
 <?php
 
 final class PhabricatorFile extends PhabricatorFileDAO
   implements PhabricatorPolicyInterface {
 
   const STORAGE_FORMAT_RAW  = 'raw';
 
   const METADATA_IMAGE_WIDTH  = 'width';
   const METADATA_IMAGE_HEIGHT = 'height';
 
   protected $phid;
   protected $name;
   protected $mimeType;
   protected $byteSize;
   protected $authorPHID;
   protected $secretKey;
   protected $contentHash;
   protected $metadata = array();
 
   protected $storageEngine;
   protected $storageFormat;
   protected $storageHandle;
 
   protected $ttl;
 
   public function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'metadata' => self::SERIALIZATION_JSON,
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorPHIDConstants::PHID_TYPE_FILE);
   }
 
   public static function readUploadedFileData($spec) {
     if (!$spec) {
       throw new Exception("No file was uploaded!");
     }
 
     $err = idx($spec, 'error');
     if ($err) {
       throw new PhabricatorFileUploadException($err);
     }
 
     $tmp_name = idx($spec, 'tmp_name');
     $is_valid = @is_uploaded_file($tmp_name);
     if (!$is_valid) {
       throw new Exception("File is not an uploaded file.");
     }
 
     $file_data = Filesystem::readFile($tmp_name);
     $file_size = idx($spec, 'size');
 
     if (strlen($file_data) != $file_size) {
       throw new Exception("File size disagrees with uploaded size.");
     }
 
     self::validateFileSize(strlen($file_data));
 
     return $file_data;
   }
 
   public static function newFromPHPUpload($spec, array $params = array()) {
     $file_data = self::readUploadedFileData($spec);
 
     $file_name = nonempty(
       idx($params, 'name'),
       idx($spec,   'name'));
     $params = array(
       'name' => $file_name,
     ) + $params;
 
     return self::newFromFileData($file_data, $params);
   }
 
   public static function newFromXHRUpload($data, array $params = array()) {
     self::validateFileSize(strlen($data));
     return self::newFromFileData($data, $params);
   }
 
   private static function validateFileSize($size) {
     $limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit');
     if (!$limit) {
       return;
     }
 
     $limit = phabricator_parse_bytes($limit);
     if ($size > $limit) {
       throw new PhabricatorFileUploadException(-1000);
     }
   }
 
 
   /**
    * Given a block of data, try to load an existing file with the same content
    * if one exists. If it does not, build a new file.
    *
    * This method is generally used when we have some piece of semi-trusted data
    * like a diff or a file from a repository that we want to show to the user.
    * We can't just dump it out because it may be dangerous for any number of
    * reasons; instead, we need to serve it through the File abstraction so it
    * ends up on the CDN domain if one is configured and so on. However, if we
    * simply wrote a new file every time we'd potentially end up with a lot
    * of redundant data in file storage.
    *
    * To solve these problems, we use file storage as a cache and reuse the
    * same file again if we've previously written it.
    *
    * NOTE: This method unguards writes.
    *
    * @param string  Raw file data.
    * @param dict    Dictionary of file information.
    */
   public static function buildFromFileDataOrHash(
     $data,
     array $params = array()) {
 
     $file = id(new PhabricatorFile())->loadOneWhere(
       'name = %s AND contentHash = %s LIMIT 1',
       self::normalizeFileName(idx($params, 'name')),
       self::hashFileContent($data));
 
     if (!$file) {
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
       $file = PhabricatorFile::newFromFileData($data, $params);
       unset($unguarded);
     }
 
     return $file;
   }
 
   public static function newFileFromContentHash($hash, $params) {
 
     // Check to see if a file with same contentHash exist
     $file = id(new PhabricatorFile())->loadOneWhere(
       'contentHash = %s LIMIT 1', $hash);
 
     if ($file) {
       // copy storageEngine, storageHandle, storageFormat
       $copy_of_storage_engine = $file->getStorageEngine();
       $copy_of_storage_handle = $file->getStorageHandle();
       $copy_of_storage_format = $file->getStorageFormat();
       $copy_of_byteSize = $file->getByteSize();
       $copy_of_mimeType = $file->getMimeType();
 
       $file_name = idx($params, 'name');
       $file_name = self::normalizeFileName($file_name);
       $file_ttl = idx($params, 'ttl');
       $authorPHID = idx($params, 'authorPHID');
 
       $new_file = new  PhabricatorFile();
 
       $new_file->setName($file_name);
       $new_file->setByteSize($copy_of_byteSize);
       $new_file->setAuthorPHID($authorPHID);
       $new_file->setTtl($file_ttl);
 
       $new_file->setContentHash($hash);
       $new_file->setStorageEngine($copy_of_storage_engine);
       $new_file->setStorageHandle($copy_of_storage_handle);
       $new_file->setStorageFormat($copy_of_storage_format);
       $new_file->setMimeType($copy_of_mimeType);
 
       $new_file->save();
 
       return $new_file;
     }
 
     return $file;
   }
 
   private static function buildFromFileData($data, array $params = array()) {
     $selector = PhabricatorEnv::newObjectFromConfig('storage.engine-selector');
 
     if (isset($params['storageEngines'])) {
       $engines = $params['storageEngines'];
     } else {
       $selector = PhabricatorEnv::newObjectFromConfig(
         'storage.engine-selector');
       $engines = $selector->selectStorageEngines($data, $params);
     }
 
     assert_instances_of($engines, 'PhabricatorFileStorageEngine');
     if (!$engines) {
       throw new Exception("No valid storage engines are available!");
     }
 
     $file = new PhabricatorFile();
 
     $data_handle = null;
     $engine_identifier = null;
     $exceptions = array();
     foreach ($engines as $engine) {
       $engine_class = get_class($engine);
       try {
         list($engine_identifier, $data_handle) = $file->writeToEngine(
           $engine,
           $data,
           $params);
 
         // We stored the file somewhere so stop trying to write it to other
         // places.
         break;
       } catch (PhabricatorFileStorageConfigurationException $ex) {
         // If an engine is outright misconfigured (or misimplemented), raise
         // that immediately since it probably needs attention.
         throw $ex;
       } catch (Exception $ex) {
         phlog($ex);
 
         // If an engine doesn't work, keep trying all the other valid engines
         // in case something else works.
         $exceptions[$engine_class] = $ex;
       }
     }
 
     if (!$data_handle) {
       throw new PhutilAggregateException(
         "All storage engines failed to write file:",
         $exceptions);
     }
 
     $file_name = idx($params, 'name');
     $file_name = self::normalizeFileName($file_name);
     $file_ttl = idx($params, 'ttl');
 
     // If for whatever reason, authorPHID isn't passed as a param
     // (always the case with newFromFileDownload()), store a ''
     $authorPHID = idx($params, 'authorPHID');
 
     $file->setName($file_name);
     $file->setByteSize(strlen($data));
     $file->setAuthorPHID($authorPHID);
     $file->setTtl($file_ttl);
     $file->setContentHash(self::hashFileContent($data));
 
     $file->setStorageEngine($engine_identifier);
     $file->setStorageHandle($data_handle);
 
     // TODO: This is probably YAGNI, but allows for us to do encryption or
     // compression later if we want.
     $file->setStorageFormat(self::STORAGE_FORMAT_RAW);
 
     if (isset($params['mime-type'])) {
       $file->setMimeType($params['mime-type']);
     } else {
       $tmp = new TempFile();
       Filesystem::writeFile($tmp, $data);
       $file->setMimeType(Filesystem::getMimeType($tmp));
     }
 
     try {
       $file->updateDimensions(false);
     } catch (Exception $ex) {
       // Do nothing
     }
 
     $file->save();
 
     return $file;
   }
 
   public static function newFromFileData($data, array $params = array()) {
     $hash = self::hashFileContent($data);
     $file = self::newFileFromContentHash($hash, $params);
 
     if ($file) {
       return $file;
     }
 
     return self::buildFromFileData($data, $params);
   }
 
   public function migrateToEngine(PhabricatorFileStorageEngine $engine) {
     if (!$this->getID() || !$this->getStorageHandle()) {
       throw new Exception(
         "You can not migrate a file which hasn't yet been saved.");
     }
 
     $data = $this->loadFileData();
     $params = array(
       'name' => $this->getName(),
     );
 
     list($new_identifier, $new_handle) = $this->writeToEngine(
       $engine,
       $data,
       $params);
 
     $old_engine = $this->instantiateStorageEngine();
     $old_handle = $this->getStorageHandle();
 
     $this->setStorageEngine($new_identifier);
     $this->setStorageHandle($new_handle);
     $this->save();
 
     $old_engine->deleteFile($old_handle);
 
     return $this;
   }
 
   private function writeToEngine(
     PhabricatorFileStorageEngine $engine,
     $data,
     array $params) {
 
     $engine_class = get_class($engine);
 
     $data_handle = $engine->writeFile($data, $params);
 
     if (!$data_handle || strlen($data_handle) > 255) {
       // This indicates an improperly implemented storage engine.
       throw new PhabricatorFileStorageConfigurationException(
         "Storage engine '{$engine_class}' executed writeFile() but did ".
         "not return a valid handle ('{$data_handle}') to the data: it ".
         "must be nonempty and no longer than 255 characters.");
     }
 
     $engine_identifier = $engine->getEngineIdentifier();
     if (!$engine_identifier || strlen($engine_identifier) > 32) {
       throw new PhabricatorFileStorageConfigurationException(
         "Storage engine '{$engine_class}' returned an improper engine ".
         "identifier '{$engine_identifier}': it must be nonempty ".
         "and no longer than 32 characters.");
     }
 
     return array($engine_identifier, $data_handle);
   }
 
 
   public static function newFromFileDownload($uri, array $params = array()) {
     // Make sure we're allowed to make a request first
     if (!PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) {
       throw new Exception("Outbound HTTP requests are disabled!");
     }
 
     $uri = new PhutilURI($uri);
 
     $protocol = $uri->getProtocol();
     switch ($protocol) {
       case 'http':
       case 'https':
         break;
       default:
         // Make sure we are not accessing any file:// URIs or similar.
         return null;
     }
 
     $timeout = 5;
 
     list($file_data) = id(new HTTPSFuture($uri))
         ->setTimeout($timeout)
         ->resolvex();
 
     $params = $params + array(
       'name' => basename($uri),
     );
 
     return self::newFromFileData($file_data, $params);
   }
 
   public static function normalizeFileName($file_name) {
     return preg_replace('/[^a-zA-Z0-9.~_-]/', '_', $file_name);
   }
 
   public function delete() {
     // delete all records of this file in transformedfile
     $trans_files = id(new PhabricatorTransformedFile())->loadAllWhere(
       'TransformedPHID = %s', $this->getPHID());
 
     $this->openTransaction();
     foreach ($trans_files as $trans_file) {
       $trans_file->delete();
     }
     $ret = parent::delete();
     $this->saveTransaction();
 
     // Check to see if other files are using storage
     $other_file = id(new PhabricatorFile())->loadAllWhere(
       'storageEngine = %s AND storageHandle = %s AND
       storageFormat = %s AND id != %d LIMIT 1', $this->getStorageEngine(),
       $this->getStorageHandle(), $this->getStorageFormat(),
       $this->getID());
 
     // If this is the only file using the storage, delete storage
     if (count($other_file) == 0) {
       $engine = $this->instantiateStorageEngine();
       $engine->deleteFile($this->getStorageHandle());
     }
     return $ret;
   }
 
   public static function hashFileContent($data) {
     return sha1($data);
   }
 
   public function loadFileData() {
 
     $engine = $this->instantiateStorageEngine();
     $data = $engine->readFile($this->getStorageHandle());
 
     switch ($this->getStorageFormat()) {
       case self::STORAGE_FORMAT_RAW:
         $data = $data;
         break;
       default:
         throw new Exception("Unknown storage format.");
     }
 
     return $data;
   }
 
   public function getViewURI() {
     if (!$this->getPHID()) {
       throw new Exception(
         "You must save a file before you can generate a view URI.");
     }
 
     $name = phutil_escape_uri($this->getName());
 
     $path = '/file/data/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name;
     return PhabricatorEnv::getCDNURI($path);
   }
 
   public function getInfoURI() {
     return '/file/info/'.$this->getPHID().'/';
   }
 
   public function getBestURI() {
     if ($this->isViewableInBrowser()) {
       return $this->getViewURI();
     } else {
       return $this->getInfoURI();
     }
   }
 
   public function getDownloadURI() {
     $uri = id(new PhutilURI($this->getViewURI()))
       ->setQueryParam('download', true);
     return (string) $uri;
   }
 
   public function getThumb60x45URI() {
     $path = '/file/xform/thumb-60x45/'.$this->getPHID().'/'
       .$this->getSecretKey().'/';
     return PhabricatorEnv::getCDNURI($path);
   }
 
   public function getThumb160x120URI() {
     $path = '/file/xform/thumb-160x120/'.$this->getPHID().'/'
       .$this->getSecretKey().'/';
     return PhabricatorEnv::getCDNURI($path);
   }
 
+  public function getPreview140URI() {
+    $path = '/file/xform/preview-140/'.$this->getPHID().'/'
+      .$this->getSecretKey().'/';
+    return PhabricatorEnv::getCDNURI($path);
+  }
+
   public function getPreview220URI() {
     $path = '/file/xform/preview-220/'.$this->getPHID().'/'
       .$this->getSecretKey().'/';
     return PhabricatorEnv::getCDNURI($path);
   }
 
   public function getThumb220x165URI() {
     $path = '/file/xform/thumb-220x165/'.$this->getPHID().'/'
       .$this->getSecretKey().'/';
     return PhabricatorEnv::getCDNURI($path);
   }
 
   public function isViewableInBrowser() {
     return ($this->getViewableMimeType() !== null);
   }
 
   public function isViewableImage() {
     if (!$this->isViewableInBrowser()) {
       return false;
     }
 
     $mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types');
     $mime_type = $this->getMimeType();
     return idx($mime_map, $mime_type);
   }
 
   public function isTransformableImage() {
 
     // NOTE: The way the 'gd' extension works in PHP is that you can install it
     // with support for only some file types, so it might be able to handle
     // PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup
     // warns you if you don't have complete support.
 
     $matches = null;
     $ok = preg_match(
       '@^image/(gif|png|jpe?g)@',
       $this->getViewableMimeType(),
       $matches);
     if (!$ok) {
       return false;
     }
 
     switch ($matches[1]) {
       case 'jpg';
       case 'jpeg':
         return function_exists('imagejpeg');
         break;
       case 'png':
         return function_exists('imagepng');
         break;
       case 'gif':
         return function_exists('imagegif');
         break;
       default:
         throw new Exception('Unknown type matched as image MIME type.');
     }
   }
 
   public static function getTransformableImageFormats() {
     $supported = array();
 
     if (function_exists('imagejpeg')) {
       $supported[] = 'jpg';
     }
 
     if (function_exists('imagepng')) {
       $supported[] = 'png';
     }
 
     if (function_exists('imagegif')) {
       $supported[] = 'gif';
     }
 
     return $supported;
   }
 
   protected function instantiateStorageEngine() {
     return self::buildEngine($this->getStorageEngine());
   }
 
   public static function buildEngine($engine_identifier) {
     $engines = self::buildAllEngines();
     foreach ($engines as $engine) {
       if ($engine->getEngineIdentifier() == $engine_identifier) {
         return $engine;
       }
     }
 
     throw new Exception(
       "Storage engine '{$engine_identifier}' could not be located!");
   }
 
   public static function buildAllEngines() {
     $engines = id(new PhutilSymbolLoader())
       ->setType('class')
       ->setConcreteOnly(true)
       ->setAncestorClass('PhabricatorFileStorageEngine')
       ->selectAndLoadSymbols();
 
     $results = array();
     foreach ($engines as $engine_class) {
       $results[] = newv($engine_class['name'], array());
     }
 
     return $results;
   }
 
   public function getViewableMimeType() {
     $mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
 
     $mime_type = $this->getMimeType();
     $mime_parts = explode(';', $mime_type);
     $mime_type = trim(reset($mime_parts));
 
     return idx($mime_map, $mime_type);
   }
 
   public function validateSecretKey($key) {
     return ($key == $this->getSecretKey());
   }
 
   public function save() {
     if (!$this->getSecretKey()) {
       $this->setSecretKey($this->generateSecretKey());
     }
     return parent::save();
   }
 
   public function generateSecretKey() {
     return Filesystem::readRandomCharacters(20);
   }
 
   public function updateDimensions($save = true) {
     if (!$this->isViewableImage()) {
       throw new Exception(
         "This file is not a viewable image.");
     }
 
     if (!function_exists("imagecreatefromstring")) {
       throw new Exception(
         "Cannot retrieve image information.");
     }
 
     $data = $this->loadFileData();
 
     $img = imagecreatefromstring($data);
     if ($img === false) {
       throw new Exception(
         "Error when decoding image.");
     }
 
     $this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img);
     $this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img);
 
     if ($save) {
       $this->save();
     }
 
     return $this;
   }
 
   public static function getMetadataName($metadata) {
     switch ($metadata) {
       case self::METADATA_IMAGE_WIDTH:
         $name = pht('Width');
         break;
       case self::METADATA_IMAGE_HEIGHT:
         $name = pht('Height');
         break;
       default:
         $name = ucfirst($metadata);
         break;
     }
 
     return $name;
   }
 
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
     // TODO: Implement proper per-object policies.
     return PhabricatorPolicies::POLICY_USER;
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return false;
   }
 
 }
diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php
index b473e351b7..efa3917d14 100644
--- a/src/applications/pholio/view/PholioMockImagesView.php
+++ b/src/applications/pholio/view/PholioMockImagesView.php
@@ -1,97 +1,110 @@
 <?php
 
 final class PholioMockImagesView extends AphrontView {
 
   private $mock;
 
   public function setMock(PholioMock $mock) {
     $this->mock = $mock;
     return $this;
   }
 
   public function render() {
     if (!$this->mock) {
       throw new Exception("Call setMock() before render()!");
     }
 
     $main_image_id = celerity_generate_unique_node_id();
     require_celerity_resource('javelin-behavior-pholio-mock-view');
     $config = array(
       'mainID' => $main_image_id,
       'mockID' => $this->mock->getID());
     Javelin::initBehavior('pholio-mock-view', $config);
 
-    $mockview = "";
+    $mockview = '';
 
     $main_image = head($this->mock->getImages());
 
     $main_image_tag = javelin_tag(
       'img',
       array(
         'id' => $main_image_id,
         'src' => $main_image->getFile()->getBestURI(),
         'sigil' => 'mock-image',
         'class' => 'pholio-mock-image',
         'meta' => array(
           'fullSizeURI' => $main_image->getFile()->getBestURI(),
           'imageID' => $main_image->getID(),
         ),
     ));
 
     $main_image_tag = javelin_tag(
       'div',
       array(
         'id' => 'mock-wrapper',
         'sigil' => 'mock-wrapper',
         'class' => 'pholio-mock-wrapper'
       ),
       $main_image_tag);
 
 
     $inline_comments_holder = javelin_tag(
       'div',
       array(
         'id' => 'mock-inline-comments',
         'sigil' => 'mock-inline-comments',
         'class' => 'pholio-mock-inline-comments'
       ),
-      "");
+      '');
 
     $mockview[] = phutil_tag(
       'div',
         array(
           'class' => 'pholio-mock-image-container',
           'id' => 'pholio-mock-image-container'
         ),
       array($main_image_tag, $inline_comments_holder));
 
     if (count($this->mock->getImages()) > 1) {
       $thumbnails = array();
       foreach ($this->mock->getImages() as $image) {
         $thumbfile = $image->getFile();
 
-        $tag = javelin_tag(
+        $dimensions = PhabricatorImageTransformer::getPreviewDimensions(
+          $thumbfile,
+          140);
+
+        $tag = phutil_tag(
           'img',
           array(
-            'src' => $thumbfile->getThumb160x120URI(),
-            'sigil' => 'mock-thumbnail',
+            'width' => $dimensions['sdx'],
+            'height' => $dimensions['sdy'],
+            'src' => $thumbfile->getPreview140URI(),
             'class' => 'pholio-mock-carousel-thumbnail',
+            'style' => 'top: '.floor((140 - $dimensions['sdy'] ) / 2).'px',
+        ));
+
+        $thumbnails[] = javelin_tag(
+          'div',
+          array(
+            'sigil' => 'mock-thumbnail',
+            'class' => 'pholio-mock-carousel-thumb-item',
             'meta' => array(
               'fullSizeURI' => $thumbfile->getBestURI(),
               'imageID' => $image->getID()
             ),
-        ));
-        $thumbnails[] = $tag;
+          ),
+          $tag);
       }
 
       $mockview[] = phutil_tag(
         'div',
         array(
           'class' => 'pholio-mock-carousel',
         ),
         $thumbnails);
     }
 
     return $this->renderSingleView($mockview);
   }
 }
diff --git a/webroot/rsrc/css/application/pholio/pholio.css b/webroot/rsrc/css/application/pholio/pholio.css
index c9438de7b4..9e851b2cd2 100644
--- a/webroot/rsrc/css/application/pholio/pholio.css
+++ b/webroot/rsrc/css/application/pholio/pholio.css
@@ -1,78 +1,96 @@
 /**
  * @provides pholio-css
  */
 .pholio-mock-image-container {
   background-color: #282828;
   text-align: center;
   vertical-align: middle;
 }
 
 .pholio-mock-carousel {
-  background-color: #282828;
+  background-color: #202020;
   text-align: center;
+  border-top: 1px solid #101010;
 }
 
-.pholio-mock-carousel-thumbnail {
-  margin-right: 5px;
+.pholio-mock-carousel-thumb-item {
   display: inline-block;
   cursor: pointer;
+  width: 140px;
+  height: 140px;
+  padding: 5px;
+  margin: 3px;
+  background: #282828;
+  vertical-align: middle;
+  border: 1px solid #383838;
+  position: relative;
+}
+
+.device-desktop .pholio-mock-carousel-thumb-item:hover {
+  background: #383838;
+  border-color: #686868;
+}
+
+.pholio-mock-carousel-thumbnail {
+  margin: auto;
+  position: relative;
 }
 
 .pholio-mock-image {
   display: inline-block;
 }
 
 .pholio-mock-select-border {
   position: absolute;
   background: #ffffff;
   opacity: 0.25;
   box-sizing: border-box;
   border: 1px solid #000000;
 }
 
 .pholio-mock-select-fill {
   position: absolute;
   border: 1px dashed #ffffff;
   box-sizing: border-box;
 }
 
 .pholio-mock-wrapper {
   position: relative;
   display: inline-block;
   cursor: crosshair;
   padding: 0px;
   margin: 10px 0px;
 }
 
 
 .pholio-mock-inline-comments {
   display: inline-block;
   margin-left: 10px;
   text-align: left;
   padding-bottom: 10px;
 }
 
 .pholio-inline-comment {
   border: 1px solid #aa8;
   background: #f9f9f1;
   margin-bottom: 2px;
   padding: 8px 10px;
 }
 
 .pholio-inline-comment-header {
   border-bottom: 1px solid #cca;
   color: #333;
   font-weight: bold;
   padding-bottom: 6px;
   margin-bottom: 4px;
 }
 
 .pholio-mock-inline-comment-highlight {
   background-color: #F0B160;
 }
 
 .pholio-inline-head-links a {
   font-weight: normal;
   margin-left: 5px;
 }