diff --git a/EVENTS.txt b/EVENTS.txt index 4d1215f6e0..32bbcdde99 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -1442,6 +1442,6 @@ OtherAccountProfiles: Hook to add account profiles to a user account profile blo image: mini image for the profile CreateFileImageThumbnailSource: Hook to create image thumbnail source from a File -- $file: MediaFile object with related metadata +- $file: 'File' object to source the image from - &$imgPath: Path to image file which can be used as source for our thumbnail algorithm. - $media: MIME media type ('image', 'video', 'audio' etc.) diff --git a/actions/attachment_thumbnail.php b/actions/attachment_thumbnail.php index 6e8baeee7a..0353fa18ff 100644 --- a/actions/attachment_thumbnail.php +++ b/actions/attachment_thumbnail.php @@ -42,15 +42,16 @@ class Attachment_thumbnailAction extends AttachmentAction { protected $thumb_w = null; // max width protected $thumb_h = null; // max height - protected $thumb_a = null; // "aspect ratio" (more like "square", 1 or 0) + protected $thumb_c = null; // crop? protected function prepare(array $args=array()) { parent::prepare($args); - foreach (array('w', 'h', 'a') as $attr) { - $this->{"thumb_$attr"} = $this->arg($attr); - } + $this->thumb_w = $this->int('w'); + $this->thumb_h = $this->int('h'); + $this->thumb_c = $this->boolean('c'); + return true; } @@ -77,7 +78,7 @@ class Attachment_thumbnailAction extends AttachmentAction function showCore() { // Returns a File_thumbnail object or throws exception if not available - $thumbnail = $this->attachment->getThumbnail($this->thumb_w, $this->thumb_h, $this->thumb_a); + $thumbnail = $this->attachment->getThumbnail($this->thumb_w, $this->thumb_h, $this->thumb_c); $this->element('img', array('src' => $thumbnail->getUrl(), 'alt' => 'Thumbnail')); } } diff --git a/classes/File.php b/classes/File.php index 7748fe83f9..9c04228735 100644 --- a/classes/File.php +++ b/classes/File.php @@ -47,6 +47,8 @@ class File extends Managed_DataObject 'date' => array('type' => 'int', 'description' => 'date of resource according to http query'), 'protected' => array('type' => 'int', 'description' => 'true when URL is private (needs login)'), 'filename' => array('type' => 'varchar', 'length' => 255, 'description' => 'if a local file, name of the file'), + 'width' => array('type' => 'int', 'description' => 'width in pixels, if it can be described as such and data is available'), + 'height' => array('type' => 'int', 'description' => 'height in pixels, if it can be described as such and data is available'), 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), ), @@ -432,32 +434,118 @@ class File extends Managed_DataObject /** * Get the attachment's thumbnail record, if any. + * Make sure you supply proper 'int' typed variables (or null). * - * @param $width int Max width of thumbnail in pixels - * @param $height int Max height of thumbnail in pixels. If null, set to $width + * @param $width int Max width of thumbnail in pixels. (if null, use common_config values) + * @param $height int Max height of thumbnail in pixels. (if null, square-crop to $width) + * @param $crop bool Crop to the max-values' aspect ratio * * @return File_thumbnail */ - public function getThumbnail($width=null, $height=null) + public function getThumbnail($width=null, $height=null, $crop=false) { + if ($this->width < 1 || $this->height < 1) { + // Old files may have 0 until migrated with scripts/upgrade.php + return null; + } + if ($width === null) { - $width = common_config('attachments', 'thumb_width'); - $height = common_config('attachments', 'thumb_height'); - $square = common_config('attachments', 'thumb_square'); - } elseif ($height === null) { - $square = true; + $width = common_config('thumbnail', 'width'); + $height = common_config('thumbnail', 'height'); + $crop = common_config('thumbnail', 'crop'); + } + + if ($height === null) { + $height = $width; + $crop = true; + } + + // Get proper aspect ratio width and height before lookup + list($width, $height, $x, $y, $w2, $h2) = + ImageFile::getScalingValues($this->width, $this->height, $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. + throw new ServerException('Bad thumbnail width or height parameter'); } $params = array('file_id'=> $this->id, 'width' => $width, - 'height' => $square ? $width : $height); + 'height' => $height); $thumb = File_thumbnail::pkeyGet($params); if ($thumb === null) { - // generate a new thumbnail for desired parameters + try { + $thumb = $this->generateThumbnail($width, $height, $crop); + } catch (UnsupportedMediaException $e) { + // FIXME: Add "unknown media" icon or something + } catch (ServerException $e) { + // Probably a remote media file, maybe not available locally + } } 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) + { + $imgPath = null; + $media = common_get_mime_media($this->mimetype); + $width = intval($width); + if ($height === null) { + $height = $width; + $crop = true; + } + + if (Event::handle('CreateFileImageThumbnailSource', array($this, &$imgPath, $media))) { + switch ($media) { + case 'image': + $imgPath = $this->getPath(); + break; + default: + throw new UnsupportedMediaException(_('Unsupported media format.'), $this->getPath()); + } + } + if (!file_exists($imgPath)) { + throw new ServerException(sprintf('Thumbnail source is not stored locally: %s', $imgPath)); + } + + try { + $image = new ImageFile($this->id, $imgPath); + } catch (UnsupportedMediaException $e) { + // Avoid deleting the original + if ($image->getPath() != $this->getPath()) { + $image->unlink(); + } + throw $e; + } + + list($width, $height, $x, $y, $w2, $h2) = + $image->scaleToFit($width, $height, $crop); + + $outname = "thumb-{$width}x{$height}-" . $this->filename; + $outpath = self::path($outname); + + $image->resizeTo($outpath, $width, $height, $x, $y, $w2, $h2); + + // Avoid deleting the original + if ($image->getPath() != $this->getPath()) { + $image->unlink(); + } + return File_thumbnail::saveThumbnail($this->id, + self::url($outname), + $width, $height); + } + public function getPath() { return self::path($this->filename); diff --git a/classes/File_oembed.php b/classes/File_oembed.php index 11d054aa4d..cb8420e352 100644 --- a/classes/File_oembed.php +++ b/classes/File_oembed.php @@ -69,8 +69,8 @@ class File_oembed extends Managed_DataObject function _getOembed($url) { $parameters = array( - 'maxwidth' => common_config('attachments', 'thumb_width'), - 'maxheight' => common_config('attachments', 'thumb_height'), + 'maxwidth' => common_config('thumbnail', 'width'), + 'maxheight' => common_config('thumbnail', 'height'), ); try { return oEmbedHelper::getObject($url, $parameters); diff --git a/classes/File_thumbnail.php b/classes/File_thumbnail.php index 7c0f7477e5..ba3bf138dc 100644 --- a/classes/File_thumbnail.php +++ b/classes/File_thumbnail.php @@ -27,19 +27,13 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; class File_thumbnail extends Managed_DataObject { - ###START_AUTOCODE - /* the code below is auto generated do not remove the above tag */ - public $__table = 'file_thumbnail'; // table name public $file_id; // int(4) primary_key not_null public $url; // varchar(255) unique_key - public $width; // int(4) - public $height; // int(4) + public $width; // int(4) primary_key + public $height; // int(4) primary_key public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP - /* the code above is auto generated do not remove the tag below */ - ###END_AUTOCODE - public static function schemaDef() { return array( @@ -50,7 +44,10 @@ class File_thumbnail extends Managed_DataObject 'height' => array('type' => 'int', 'description' => 'height of thumbnail'), 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), ), - 'primary key' => array('file_id'), + 'primary key' => array('file_id', 'width', 'height'), + 'indexes' => array( + 'file_thumbnail_file_id_idx' => array('file_id'), + ), 'foreign keys' => array( 'file_thumbnail_file_id_fkey' => array('file', array('file_id' => 'id')), ) @@ -99,6 +96,7 @@ class File_thumbnail extends Managed_DataObject $tn->width = intval($width); $tn->height = intval($height); $tn->insert(); + return $tn; } public function getUrl() diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php index 1b323cd2a1..0735815933 100644 --- a/lib/attachmentlist.php +++ b/lib/attachmentlist.php @@ -201,34 +201,14 @@ class AttachmentListItem extends Widget } function showRepresentation() { - $thumb = $this->getThumbInfo(); - if ($thumb instanceof File_thumbnail) { + try { + $thumb = $this->attachment->getThumbnail(); $this->out->element('img', array('alt' => '', 'src' => $thumb->getUrl(), 'width' => $thumb->width, 'height' => $thumb->height)); + } catch (Exception $e) { + // Image representation unavailable } } - /** - * Pull a thumbnail image reference for the given file, and if necessary - * resize it to match currently thumbnail size settings. - * - * @return File_Thumbnail or false/null - */ - function getThumbInfo() - { - $thumbnail = File_thumbnail::getKV('file_id', $this->attachment->id); - if ($thumbnail) { - $maxWidth = common_config('attachments', 'thumb_width'); - $maxHeight = common_config('attachments', 'thumb_height'); - if ($thumbnail->width > $maxWidth) { - $thumb = clone($thumbnail); - $thumb->width = $maxWidth; - $thumb->height = intval($thumbnail->height * $maxWidth / $thumbnail->width); - return $thumb; - } - } - return $thumbnail; - } - /** * start a single notice. * @@ -342,10 +322,12 @@ class Attachment extends AttachmentListItem case 'video/quicktime': case 'video/webm': $mediatype = common_get_mime_media($this->attachment->mimetype); - $thumb = $this->getThumbInfo(); - $poster = ($thumb instanceof File_thumbnail) - ? $thumb->getUrl() - : null; + try { + $thumb = $this->attachment->getThumbnail(); + $poster = $thumb->getUrl(); + } catch (Exception $e) { + $poster = null; + } $this->out->elementStart($mediatype, array('class'=>'attachment_player', 'poster'=>$poster, diff --git a/lib/default.php b/lib/default.php index 6efeb406c4..8ac1fa8bbb 100644 --- a/lib/default.php +++ b/lib/default.php @@ -250,11 +250,13 @@ $default = 'monthly_quota' => 15000000, 'uploads' => true, 'show_thumbs' => true, // show thumbnails in notice lists for uploaded images, and photos and videos linked remotely that provide oEmbed info - 'thumb_width' => 150, - 'thumb_height' => 150, - 'thumb_square' => true, 'process_links' => true, // check linked resources for embeddable photos and videos; this will hit referenced external web sites when processing new messages. ), + 'thumbnail' => + array('crop' => false, // overridden to true if thumb height === null + 'maxsize' => 500, // thumbs bigger than this will not be generated + 'width' => 500, + 'height' => 250), 'application' => array('desclimit' => null), 'group' => diff --git a/lib/imagefile.php b/lib/imagefile.php index 1648980c18..a4b77702ec 100644 --- a/lib/imagefile.php +++ b/lib/imagefile.php @@ -28,9 +28,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * A wrapper on uploaded files @@ -335,6 +333,77 @@ class ImageFile return $num; } + + public function scaleToFit($maxWidth=null, $maxHeight=null, $crop=null) + { + return self::getScalingValues($this->width, $this->height, + $maxWidth, $maxHeight, $crop); + } + + /* + * Gets scaling values for images of various types. Cropping can be enabled. + * + * Values will scale _up_ to fit max values if cropping is enabled! + * With cropping disabled, the max value of each axis will be respected. + * + * @param $width int Original width + * @param $height int Original height + * @param $maxW int Resulting max width + * @param $maxH int Resulting max height + * @param $crop int Crop to the size (not preserving aspect ratio) + */ + public static function getScalingValues($width, $height, + $maxW=null, $maxH=null, + $crop=null) + { + $maxW = $maxW ?: common_config('thumbnail', 'width'); + $maxH = $maxH ?: common_config('thumbnail', 'height'); + + if ($maxW < 1 || ($maxH !== null && $maxH < 1)) { + throw new ServerException('Bad parameters for ImageFile::getScalingValues'); + } elseif ($maxH === null) { + // if maxH is null, we set maxH to equal maxW and enable crop + $maxH = $maxW; + $crop = true; + } + + // Cropping data (for original image size). Default values, 0 and null, + // imply no cropping and with preserved aspect ratio (per axis). + $cx = 0; // crop x + $cy = 0; // crop y + $cw = null; // crop area width + $ch = null; // crop area height + + if ($crop) { + $s_ar = $width / $height; + $t_ar = $maxW / $maxH; + + $rw = $maxW; + $rh = $maxH; + + // Source aspect ratio differs from target, recalculate crop points! + if ($s_ar > $t_ar) { + $cx = floor($width / 2 - $height * $t_ar / 2); + $cw = ceil($height * $t_ar); + } elseif ($s_ar < $t_ar) { + $cy = floor($height / 2 - $width / $t_ar / 2); + $ch = ceil($width / $t_ar); + } + } else { + $rw = $maxW; + $rh = ceil($height * $rw / $width); + + // Scaling caused too large height, decrease to max accepted value + if ($rh > $maxH) { + $rh = $maxH; + $rw = ceil($width * $rh / $height); + } + } + return array(intval($rw), intval($rh), + intval($cx), intval($cy), + is_null($cw) ? null : intval($cw), + is_null($ch) ? null : intval($ch)); + } } //PHP doesn't (as of 2/24/2010) have an imagecreatefrombmp so conditionally define one @@ -432,4 +501,4 @@ if(!function_exists('imagecreatefrombmp')){ // Return image-object return $image; } -} +} // if(!function_exists('imagecreatefrombmp')) diff --git a/lib/inlineattachmentlist.php b/lib/inlineattachmentlist.php index a0243c825f..622252324f 100644 --- a/lib/inlineattachmentlist.php +++ b/lib/inlineattachmentlist.php @@ -62,7 +62,7 @@ class InlineAttachmentListItem extends AttachmentListItem function show() { - $this->thumb = parent::getThumbInfo(); + $this->thumb = $this->attachment->getThumbnail(); if (!empty($this->thumb)) { parent::show(); } diff --git a/lib/mediafile.php b/lib/mediafile.php index 1791b1a698..e1a9d1247c 100644 --- a/lib/mediafile.php +++ b/lib/mediafile.php @@ -41,7 +41,6 @@ class MediaFile var $fileurl = null; var $short_fileurl = null; var $mimetype = null; - var $thumbnailRecord = null; function __construct(Profile $scoped, $filename = null, $mimetype = null) { @@ -50,12 +49,6 @@ class MediaFile $this->filename = $filename; $this->mimetype = $mimetype; $this->fileRecord = $this->storeFile(); - try { - $this->thumbnailRecord = $this->storeThumbnail(); - } catch (UnsupportedMediaException $e) { - // FIXME: Add "unknown media" icon or something - $this->thumbnailRecord = null; - } $this->fileurl = common_local_url('attachment', array('attachment' => $this->fileRecord->id)); @@ -110,109 +103,6 @@ class MediaFile return $file; } - /** - * Generate and store a thumbnail image for the uploaded file, if applicable. - * - * @return File_thumbnail or null - */ - function storeThumbnail($maxWidth=null, $maxHeight=null, $square=true) - { - $imgPath = null; - $media = common_get_mime_media($this->mimetype); - - if (Event::handle('CreateFileImageThumbnailSource', array($this, &$imgPath, $media))) { - switch ($media) { - case 'image': - $imgPath = $this->getPath(); - break; - default: - throw new UnsupportedMediaException(_('Unsupported media format.'), $this->getPath()); - } - } - if (!file_exists($imgPath)) { - throw new ServerException(sprintf('Thumbnail source is not stored locally: %s', $imgPath)); - } - - try { - $image = new ImageFile($this->fileRecord->id, $imgPath); - } catch (UnsupportedMediaException $e) { - // Avoid deleting the original - if ($image->getPath() != $this->getPath()) { - $image->unlink(); - } - throw $e; - } - - $outname = File::filename($this->scoped, 'thumb-' . $this->filename, $this->mimetype); - $outpath = File::path($outname); - - $maxWidth = $maxWidth ?: common_config('attachments', 'thumb_width'); - $maxHeight = $maxHeight ?: common_config('attachments', 'thumb_height'); - list($width, $height, $x, $y, $w2, $h2) = - $this->scaleToFit($image->width, $image->height, - $maxWidth, $maxHeight, - common_config('attachments', 'thumb_square')); - - $image->resizeTo($outpath, $width, $height, $x, $y, $w2, $h2); - - // Avoid deleting the original - if ($image->getPath() != $this->getPath()) { - $image->unlink(); - } - return File_thumbnail::saveThumbnail($this->fileRecord->id, - File::url($outname), - $width, - $height); - } - - // This will give parameters to scale up if max values are larger than original - function scaleToFit($width, $height, $maxWidth=null, $maxHeight=null, $square=true) - { - if ($width <= 0 || $height <= 0 - || ($maxWidth !== null && $maxWidth <= 0) - || ($maxHeight !== null && $maxHeight <= 0)) { - throw new ServerException('Bad scaleToFit parameters for MediaFile'); - } elseif ($maxWidth === null) { - // maxWidth must be a positive number - throw new ServerException('MediaFile::scaleToFit maxWidth is null'); - } elseif ($square || $maxHeight === null) { - // if square thumb ratio or if maxHeight is null, - // we set maxHeight to equal maxWidth - $maxHeight = $maxWidth; - $square = true; - } - - // cropping data - $cx = 0; // crop x - $cy = 0; // crop y - $cw = null; // crop area width - $ch = null; // crop area height - - if ($square) { - // resulting width and height - $rw = $maxWidth; - $rh = $maxHeight; - - // minSide will determine the smallest image size - // and crop-values are determined from this - $minSide = $width > $height ? $height : $width; - $cx = $width / 2 - $minSide / 2; - $cy = $height / 2 - $minSide / 2; - $cw = $minSide; - $ch = $minSide; - } else { - // resulting sizes - $rw = $maxWidth; - $rh = floor($height * $maxWidth / $width); - - if ($rh > $maxHeight) { - $rw = floor($width * $maxHeight / $height); - $rh = $maxHeight; - } - } - return array($rw, $rh, $cx, $cy, $cw, $ch); - } - function rememberFile($file, $short) { $this->maybeAddRedir($file->id, $short); diff --git a/lib/oembedhelper.php b/lib/oembedhelper.php index 6b5b8d34f2..fab1131648 100644 --- a/lib/oembedhelper.php +++ b/lib/oembedhelper.php @@ -246,8 +246,8 @@ class oEmbedHelper if (isset($data->thumbnail_url)) { if (!isset($data->thumbnail_width)) { // !?!?! - $data->thumbnail_width = common_config('attachments', 'thumb_width'); - $data->thumbnail_height = common_config('attachments', 'thumb_height'); + $data->thumbnail_width = common_config('thumbnail', 'width'); + $data->thumbnail_height = common_config('thumbnail', 'height'); } } diff --git a/plugins/Bookmark/forms/bookmark.php b/plugins/Bookmark/forms/bookmark.php index f577a32477..23ef6940ed 100644 --- a/plugins/Bookmark/forms/bookmark.php +++ b/plugins/Bookmark/forms/bookmark.php @@ -190,8 +190,8 @@ class BookmarkForm extends Form function scaleImage($width, $height) { - $maxwidth = common_config('attachments', 'thumb_width'); - $maxheight = common_config('attachments', 'thumb_height'); + $maxwidth = common_config('thumbnail', 'width'); + $maxheight = common_config('thumbnail', 'height'); if ($width > $height && $width > $maxwidth) { $height = (int) ((((float)$maxwidth)/(float)($width))*(float)$height); diff --git a/plugins/VideoThumbnails/VideoThumbnailsPlugin.php b/plugins/VideoThumbnails/VideoThumbnailsPlugin.php index 42f45571eb..a8f4dcbb5f 100644 --- a/plugins/VideoThumbnails/VideoThumbnailsPlugin.php +++ b/plugins/VideoThumbnails/VideoThumbnailsPlugin.php @@ -44,7 +44,7 @@ class VideoThumbnailsPlugin extends Plugin * and disregard any cropping or scaling in the resulting file, as * that will be handled in the core thumbnail algorithm. */ - public function onCreateFileImageThumbnailSource(MediaFile $file, &$imgPath, $media=null) + public function onCreateFileImageThumbnailSource(File $file, &$imgPath, $media=null) { // The calling function might accidentally pass application/ogg videos. // If that's a problem, let's fix it in the calling function. diff --git a/scripts/upgrade.php b/scripts/upgrade.php index 9ae95e2562..adce2555ef 100644 --- a/scripts/upgrade.php +++ b/scripts/upgrade.php @@ -43,6 +43,7 @@ function main() fixupNoticeConversation(); initConversation(); fixupGroupURI(); + fixupFileGeometry(); initGroupProfileId(); initLocalGroup(); @@ -414,4 +415,33 @@ function initProfileLists() printfnq("DONE.\n"); } +/* + * Added as we now store interpretd width and height in File table. + */ +function fixupFileGeometry() +{ + printfnq("Ensuring width and height is set for supported local File objects..."); + + $file = new File(); + $file->whereAdd('filename IS NOT NULL'); // local files + $file->whereAdd('width IS NULL OR width = 0'); + + if ($file->find()) { + while ($file->fetch()) { + // Add support for video sizes too + try { + $image = new ImageFile($file->id, $file->getPath()); + } catch (UnsupportedMediaException $e) { + continue; + } + $orig = clone($file); + $file->width = $image->width; + $file->height = $image->height; + $file->update($orig); + } + } + + printfnq("DONE.\n"); +} + main();