diff --git a/classes/File.php b/classes/File.php index 4e85e542d5..cafbae8fb0 100644 --- a/classes/File.php +++ b/classes/File.php @@ -235,7 +235,7 @@ class File extends Managed_DataObject } // Normalize and make the original filename more URL friendly. - $origname = basename($origname); + $origname = basename($origname, $ext); if (class_exists('Normalizer')) { // http://php.net/manual/en/class.normalizer.php // http://www.unicode.org/reports/tr15/ @@ -403,15 +403,18 @@ class File extends Managed_DataObject $height = $width; $crop = true; } - + // Get proper aspect ratio width and height before lookup + // We have to do it through an ImageFile object because of orientation etc. + // Only other solution would've been to rotate + rewrite uploaded files. + $image = ImageFile::fromFileObject($this); list($width, $height, $x, $y, $w2, $h2) = - ImageFile::getScalingValues($this->width, $this->height, $width, $height, $crop); + $image->scaleToFit($width, $height, $crop); // Doublecheck that parameters are sane and integers. if ($width < 1 || $width > common_config('thumbnail', 'maxsize') || $height < 1 || $height > common_config('thumbnail', 'maxsize')) { - // Fail on bad width parameter. If this occurs, it's due to algorithm in ImageFile::getScalingValues + // Fail on bad width parameter. If this occurs, it's due to algorithm in ImageFile->scaleToFit throw new ServerException('Bad thumbnail size parameters.'); } @@ -419,35 +422,11 @@ class File extends Managed_DataObject 'width' => $width, 'height' => $height); $thumb = File_thumbnail::pkeyGet($params); - if ($thumb === null) { - // throws exception on failure to generate thumbnail - $thumb = $this->generateThumbnail($width, $height, $crop); - } - return $thumb; - } - - /** - * Generate and store a thumbnail image for the uploaded file, if applicable. - * Call this only if you know what you're doing. - * - * @param $width int Maximum thumbnail width in pixels - * @param $height int Maximum thumbnail height in pixels, if null, crop to $width - * - * @return File_thumbnail or null - */ - protected function generateThumbnail($width, $height, $crop) - { - $width = intval($width); - if ($height === null) { - $height = $width; - $crop = true; + if ($thumb instanceof File_thumbnail) { + return $thumb; } - $image = ImageFile::fromFileObject($this); - - list($width, $height, $x, $y, $w2, $h2) = - $image->scaleToFit($width, $height, $crop); - + // throws exception on failure to generate thumbnail $outname = "thumb-{$width}x{$height}-" . $this->filename; $outpath = self::path($outname); @@ -459,7 +438,8 @@ class File extends Managed_DataObject } return File_thumbnail::saveThumbnail($this->id, self::url($outname), - $width, $height); + $width, $height, + $outname); } public function getPath() @@ -524,4 +504,9 @@ class File extends Managed_DataObject return $count; } + + public function isLocal() + { + return !empty($this->filename); + } } diff --git a/classes/File_thumbnail.php b/classes/File_thumbnail.php index ba3bf138dc..10135f3d2f 100644 --- a/classes/File_thumbnail.php +++ b/classes/File_thumbnail.php @@ -30,6 +30,7 @@ class File_thumbnail extends Managed_DataObject public $__table = 'file_thumbnail'; // table name public $file_id; // int(4) primary_key not_null public $url; // varchar(255) unique_key + public $filename; // varchar(255) public $width; // int(4) primary_key public $height; // int(4) primary_key public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP @@ -40,6 +41,7 @@ class File_thumbnail extends Managed_DataObject 'fields' => array( 'file_id' => array('type' => 'int', 'not null' => true, 'description' => 'thumbnail for what URL/file'), 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL of thumbnail'), + 'filename' => array('type' => 'varchar', 'length' => 255, 'description' => 'if stored locally, filename is put here'), 'width' => array('type' => 'int', 'description' => 'width of thumbnail'), 'height' => array('type' => 'int', 'description' => 'height of thumbnail'), 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), @@ -88,19 +90,48 @@ class File_thumbnail extends Managed_DataObject * @param int $width * @param int $height */ - static function saveThumbnail($file_id, $url, $width, $height) + static function saveThumbnail($file_id, $url, $width, $height, $filename=null) { $tn = new File_thumbnail; $tn->file_id = $file_id; $tn->url = $url; + $tn->filename = $filename; $tn->width = intval($width); $tn->height = intval($height); $tn->insert(); return $tn; } + static function path($filename) + { + // TODO: Store thumbnails in their own directory and don't use File::path here + return File::path($filename); + } + public function getUrl() { return $this->url; } + + public function delete($useWhere=false) + { + if (!empty($this->filename) && file_exists(File_thumbnail::path($this->filename))) { + $deleted = @unlink(self::path($this->filename)); + if (!$deleted) { + common_log(LOG_ERR, sprintf('Could not unlink existing file: "%s"', self::path($this->filename))); + } + } + + return parent::delete($useWhere); + } + + public function getFile() + { + $file = new File(); + $file->id = $this->file_id; + if (!$file->find(true)) { + throw new NoResultException($file); + } + return $file; + } } diff --git a/lib/imagefile.php b/lib/imagefile.php index 337c27b87a..76231516f4 100644 --- a/lib/imagefile.php +++ b/lib/imagefile.php @@ -51,6 +51,7 @@ class ImageFile var $type; var $height; var $width; + var $rotate=0; // degrees to rotate for properly oriented image (extrapolated from EXIF etc.) function __construct($id=null, $filepath=null, $type=null, $width=null, $height=null) { @@ -74,6 +75,26 @@ class ImageFile $this->type = ($info) ? $info[2]:$type; $this->width = ($info) ? $info[0]:$width; $this->height = ($info) ? $info[1]:$height; + + // Orientation value to rotate thumbnails properly + $exif = exif_read_data($this->filepath); + if (isset($exif['Orientation'])) { + switch ((int)$exif['Orientation']) { + case 1: // top is top + $this->rotate = 0; + break; + case 3: // top is bottom + $this->rotate = 180; + break; + case 6: // top is right + $this->rotate = -90; + break; + case 8: // top is left + $this->rotate = 90; + break; + } + // If we ever write this back, Orientation should be set to '1' + } } public static function fromFileObject(File $file) @@ -247,6 +268,10 @@ class ImageFile throw new Exception(_('Unknown file type')); } + if ($this->rotate != 0) { + $image_src = imagerotate($image_src, $this->rotate, 0); + } + $image_dest = imagecreatetruecolor($width, $height); if ($this->type == IMAGETYPE_GIF || $this->type == IMAGETYPE_PNG || $this->type == IMAGETYPE_BMP) { @@ -367,7 +392,7 @@ class ImageFile public function scaleToFit($maxWidth=null, $maxHeight=null, $crop=null) { return self::getScalingValues($this->width, $this->height, - $maxWidth, $maxHeight, $crop); + $maxWidth, $maxHeight, $crop, $this->rotate); } /* @@ -384,7 +409,7 @@ class ImageFile */ public static function getScalingValues($width, $height, $maxW=null, $maxH=null, - $crop=null) + $crop=null, $rotate=0) { $maxW = $maxW ?: common_config('thumbnail', 'width'); $maxH = $maxH ?: common_config('thumbnail', 'height'); @@ -396,6 +421,13 @@ class ImageFile $maxH = $maxW; $crop = true; } + + // Because GD doesn't understand EXIF orientation etc. + if (abs($rotate) == 90) { + $tmp = $width; + $width = $height; + $height = $tmp; + } // Cropping data (for original image size). Default values, 0 and null, // imply no cropping and with preserved aspect ratio (per axis). diff --git a/scripts/upgrade.php b/scripts/upgrade.php index 8d89773955..902a1aa85d 100644 --- a/scripts/upgrade.php +++ b/scripts/upgrade.php @@ -44,6 +44,8 @@ function main() initConversation(); fixupGroupURI(); fixupFileGeometry(); + deleteLocalFileThumbnailsWithoutFilename(); + deleteMissingLocalFileThumbnails(); initGroupProfileId(); initLocalGroup(); @@ -451,4 +453,55 @@ function fixupFileGeometry() printfnq("DONE.\n"); } +/* + * File_thumbnail objects for local Files store their own filenames in the database. + */ +function deleteLocalFileThumbnailsWithoutFilename() +{ + printfnq("Removing all local File_thumbnail entries without filename property..."); + + $file = new File(); + $file->whereAdd('filename IS NOT NULL'); // local files + + if ($file->find()) { + // Looping through local File entries + while ($file->fetch()) { + $thumbs = new File_thumbnail(); + $thumbs->file_id = $file->id; + $thumbs->whereAdd('filename IS NULL'); + // Checking if there were any File_thumbnail entries without filename + if (!$thumbs->find()) { + continue; + } + // deleting incomplete entry to allow regeneration + while ($thumbs->fetch()) { + $thumbs->delete(); + } + } + } + + printfnq("DONE.\n"); +} + +/* + * Delete File_thumbnail entries where the referenced file does not exist. + */ +function deleteMissingLocalFileThumbnails() +{ + printfnq("Removing all local File_thumbnail entries without existing files..."); + + $thumbs = new File_thumbnail(); + $thumbs->whereAdd('filename IS NOT NULL'); // only fill in names where they're missing + // Checking if there were any File_thumbnail entries without filename + if ($thumbs->find()) { + while ($thumbs->fetch()) { + if (!file_exists(File_thumbnail::path($thumbs->filename))) { + $thumbs->delete(); + } + } + } + + printfnq("DONE.\n"); +} + main();