+
+ public function scaleToFit($maxWidth=null, $maxHeight=null, $crop=null)
+ {
+ return self::getScalingValues($this->width, $this->height,
+ $maxWidth, $maxHeight, $crop, $this->rotate);
+ }
+
+ /*
+ * 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, $rotate=0)
+ {
+ $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;
+ }
+
+ // 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).
+ $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) ? $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;
+ }
+
+ public function getFileThumbnail($width, $height, $crop)
+ {
+ 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 ($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->id,
+ '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->id}-{$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->id, $width, $height));
+
+ // Perform resize and store into file
+ $this->resizeTo($outpath, $box);
+
+ // Avoid deleting the original
+ if ($this->getPath() != File_thumbnail::path($this->filename)) {
+ $this->unlink();
+ }
+ return File_thumbnail::saveThumbnail($this->fileRecord->id,
+ File_thumbnail::url($outname),
+ $width, $height,
+ $outname);
+ }