var $height;
var $width;
var $rotate=0; // degrees to rotate for properly oriented image (extrapolated from EXIF etc.)
- var $animated = false; // Animated image? (has more than 1 frame)
+ var $animated = null; // Animated image? (has more than 1 frame). null means untested
+ var $mimetype = null; // The _ImageFile_ mimetype, _not_ the originating File object
- function __construct($id=null, $filepath=null, $type=null, $width=null, $height=null)
+ protected $fileRecord = null;
+
+ function __construct($id, $filepath)
{
$this->id = $id;
+ if (!empty($this->id)) {
+ $this->fileRecord = new File();
+ $this->fileRecord->id = $this->id;
+ if (!$this->fileRecord->find(true)) {
+ // If we have set an ID, we need that ID to exist!
+ throw new NoResultException($this->fileRecord);
+ }
+ }
+
+ // These do not have to be the same as fileRecord->filename for example,
+ // since we may have generated an image source file from something else!
$this->filepath = $filepath;
$this->filename = basename($filepath);
throw new UnsupportedMediaException(_('Unsupported image format.'), $this->filepath);
}
- $this->type = ($info) ? $info[2]:$type;
- $this->width = ($info) ? $info[0]:$width;
- $this->height = ($info) ? $info[1]:$height;
+ $this->width = $info[0];
+ $this->height = $info[1];
+ $this->type = $info[2];
+ $this->mimetype = $info['mime'];
- if ($this->type == IMAGETYPE_JPEG && function_exists('exif_read_data')) {
+ if ($this->type === IMAGETYPE_JPEG && function_exists('exif_read_data')) {
// Orientation value to rotate thumbnails properly
- $exif = exif_read_data($this->filepath);
+ $exif = @exif_read_data($this->filepath);
if (is_array($exif) && isset($exif['Orientation'])) {
switch ((int)$exif['Orientation']) {
case 1: // top is top
}
// If we ever write this back, Orientation should be set to '1'
}
+ } elseif ($this->type === IMAGETYPE_GIF) {
+ $this->animated = $this->isAnimatedGif();
}
Event::handle('FillImageFileMetadata', array($this));
$imgPath = null;
$media = common_get_mime_media($file->mimetype);
if (Event::handle('CreateFileImageThumbnailSource', array($file, &$imgPath, $media))) {
+ if (empty($file->filename) && !file_exists($imgPath)) {
+ throw new UnsupportedMediaException(_('File without filename could not get a thumbnail source.'));
+ }
+
+ // First some mimetype specific exceptions
+ switch ($file->mimetype) {
+ case 'image/svg+xml':
+ throw new UseFileAsThumbnailException($file);
+ }
+
+ // And we'll only consider it an image if it has such a media type
switch ($media) {
case 'image':
$imgPath = $file->getPath();
}
if (!file_exists($imgPath)) {
- throw new ServerException(sprintf('Image not available locally: %s', $imgPath));
+ throw new FileNotFoundException($imgPath);
}
try {
- $image = new ImageFile($file->id, $imgPath);
- } catch (UnsupportedMediaException $e) {
+ $image = new ImageFile($file->getID(), $imgPath);
+ } catch (Exception $e) {
// Avoid deleting the original
- if ($imgPath != $file->getPath()) {
- unlink($imgPath);
+ try {
+ if (strlen($imgPath) > 0 && $imgPath !== $file->getPath()) {
+ common_debug(__METHOD__.': Deleting temporary file that was created as image file thumbnail source: '._ve($imgPath));
+ @unlink($imgPath);
+ }
+ } catch (FileNotFoundException $e) {
+ // File reported (via getPath) that the original file
+ // doesn't exist anyway, so it's safe to delete $imgPath
+ @unlink($imgPath);
}
+ common_debug(sprintf('Exception caught when creating ImageFile for File id==%s and imgPath==', _ve($file->id), _ve($imgPath)));
throw $e;
}
return $image;
public function getPath()
{
if (!file_exists($this->filepath)) {
- throw new ServerException('No file in ImageFile filepath');
+ throw new FileNotFoundException($this->filepath);
}
return $this->filepath;
case UPLOAD_ERR_NO_FILE:
// No file; probably just a non-AJAX submission.
+ throw new ClientException(_('No file uploaded.'));
+
default:
common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " . $_FILES[$param]['error']);
// TRANS: Exception thrown when uploading an image fails for an unknown reason.
return new ImageFile(null, $_FILES[$param]['tmp_name']);
}
- /**
- * Compat interface for old code generating avatar thumbnails...
- * Saves the scaled file directly into the avatar area.
- *
- * @param int $size target width & height -- must be square
- * @param int $x (default 0) upper-left corner to crop from
- * @param int $y (default 0) upper-left corner to crop from
- * @param int $w (default full) width of image area to crop
- * @param int $h (default full) height of image area to crop
- * @return string filename
- */
- function resize($size, $x = 0, $y = 0, $w = null, $h = null)
- {
- $targetType = $this->preferredType();
- $outname = Avatar::filename($this->id,
- image_type_to_extension($targetType),
- $size,
- common_timestamp());
- $outpath = Avatar::path($outname);
- $this->resizeTo($outpath, array('width'=>$size, 'height'=>$size,
- 'x'=>$x, 'y'=>$y,
- 'w'=>$w, 'h'=>$h));
- return $outname;
- }
-
/**
* Copy the image file to the given destination.
*
}
if (!file_exists($outpath)) {
- throw new UseFileAsThumbnailException($this->id);
+ if ($this->fileRecord instanceof File) {
+ throw new UseFileAsThumbnailException($this->fileRecord);
+ } else {
+ throw new UnsupportedMediaException('No local File object exists for ImageFile.');
+ }
}
return $outpath;
is_null($cw) ? $width : intval($cw),
is_null($ch) ? $height : intval($ch));
}
+
+ /**
+ * Animated GIF test, courtesy of frank at huddler dot com et al:
+ * http://php.net/manual/en/function.imagecreatefromgif.php#104473
+ * Modified so avoid landing inside of a header (and thus not matching our regexp).
+ */
+ protected function isAnimatedGif()
+ {
+ if (!($fh = @fopen($this->filepath, 'rb'))) {
+ return false;
+ }
+
+ $count = 0;
+ //an animated gif contains multiple "frames", with each frame having a
+ //header made up of:
+ // * a static 4-byte sequence (\x00\x21\xF9\x04)
+ // * 4 variable bytes
+ // * a static 2-byte sequence (\x00\x2C)
+ // In total the header is maximum 10 bytes.
+
+ // We read through the file til we reach the end of the file, or we've found
+ // at least 2 frame headers
+ while(!feof($fh) && $count < 2) {
+ $chunk = fread($fh, 1024 * 100); //read 100kb at a time
+ $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00\x2C#s', $chunk, $matches);
+ // rewind in case we ended up in the middle of the header, but avoid
+ // infinite loop (i.e. don't rewind if we're already in the end).
+ if (!feof($fh) && ftell($fh) >= 9) {
+ fseek($fh, -9, SEEK_CUR);
+ }
+ }
+
+ fclose($fh);
+ return $count >= 1; // number of animated frames apart from the original image
+ }
+
+ public function getFileThumbnail($width, $height, $crop, $upscale=false)
+ {
+ if (!$this->fileRecord instanceof File) {
+ throw new ServerException('No File object attached to this ImageFile object.');
+ }
+
+ if ($width === null) {
+ $width = common_config('thumbnail', 'width');
+ $height = common_config('thumbnail', 'height');
+ $crop = common_config('thumbnail', 'crop');
+ }
+
+ if (!$upscale) {
+ if ($width > $this->width) {
+ $width = $this->width;
+ }
+ if (!is_null($height) && $height > $this->height) {
+ $height = $this->height;
+ }
+ }
+
+ if ($height === null) {
+ $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
+ // which we don't want to do because we like original, untouched data!
+ list($width, $height, $x, $y, $w, $h) = $this->scaleToFit($width, $height, $crop);
+
+ $thumb = File_thumbnail::pkeyGet(array(
+ 'file_id'=> $this->fileRecord->getID(),
+ 'width' => $width,
+ 'height' => $height,
+ ));
+ if ($thumb instanceof File_thumbnail) {
+ return $thumb;
+ }
+
+ $filename = $this->fileRecord->filehash ?: $this->filename; // Remote files don't have $this->filehash
+ $extension = File::guessMimeExtension($this->mimetype);
+ $outname = "thumb-{$this->fileRecord->getID()}-{$width}x{$height}-{$filename}." . $extension;
+ $outpath = File_thumbnail::path($outname);
+
+ // The boundary box for our resizing
+ $box = array('width'=>$width, 'height'=>$height,
+ 'x'=>$x, 'y'=>$y,
+ 'w'=>$w, 'h'=>$h);
+
+ // Doublecheck that parameters are sane and integers.
+ if ($box['width'] < 1 || $box['width'] > common_config('thumbnail', 'maxsize')
+ || $box['height'] < 1 || $box['height'] > common_config('thumbnail', 'maxsize')
+ || $box['w'] < 1 || $box['x'] >= $this->width
+ || $box['h'] < 1 || $box['y'] >= $this->height) {
+ // Fail on bad width parameter. If this occurs, it's due to algorithm in ImageFile->scaleToFit
+ common_debug("Boundary box parameters for resize of {$this->filepath} : ".var_export($box,true));
+ throw new ServerException('Bad thumbnail size parameters.');
+ }
+
+ common_debug(sprintf('Generating a thumbnail of File id==%u of size %ux%u', $this->fileRecord->getID(), $width, $height));
+
+ // Perform resize and store into file
+ $this->resizeTo($outpath, $box);
+
+ try {
+ // Avoid deleting the original
+ if (!in_array($this->getPath(), [File::path($this->filename), File_thumbnail::path($this->filename)])) {
+ $this->unlink();
+ }
+ } catch (FileNotFoundException $e) {
+ // $this->getPath() says the file doesn't exist anyway, so no point in trying to delete it!
+ }
+
+ return File_thumbnail::saveThumbnail($this->fileRecord->getID(),
+ null, // no url since we generated it ourselves and can dynamically generate the url
+ $width, $height,
+ $outname);
+ }
}
//PHP doesn't (as of 2/24/2010) have an imagecreatefrombmp so conditionally define one