diff --git a/dev/Common/Links.js b/dev/Common/Links.js index 5af8323b0..6b7651e99 100644 --- a/dev/Common/Links.js +++ b/dev/Common/Links.js @@ -57,6 +57,15 @@ return this.sServer + '/Raw/' + this.sSubQuery + this.sSpecSuffix + '/View/' + this.sSubSubQuery + sDownload; }; + /** + * @param {string} sDownload + * @return {string} + */ + Links.prototype.attachmentThumbnailPreview = function (sDownload) + { + return this.sServer + '/Raw/' + this.sSubQuery + this.sSpecSuffix + '/ViewThumbnail/' + this.sSubSubQuery + sDownload; + }; + /** * @param {string} sDownload * @return {string} diff --git a/dev/Model/Attachment.js b/dev/Model/Attachment.js index 012424530..456c6ef29 100644 --- a/dev/Model/Attachment.js +++ b/dev/Model/Attachment.js @@ -27,6 +27,7 @@ this.friendlySize = ''; this.isInline = false; this.isLinked = false; + this.isThumbnail = false; this.cid = ''; this.cidWithOutTags = ''; this.contentLocation = ''; @@ -56,6 +57,7 @@ AttachmentModel.prototype.friendlySize = ''; AttachmentModel.prototype.isInline = false; AttachmentModel.prototype.isLinked = false; + AttachmentModel.prototype.isThumbnail = false; AttachmentModel.prototype.cid = ''; AttachmentModel.prototype.cidWithOutTags = ''; AttachmentModel.prototype.contentLocation = ''; @@ -78,6 +80,7 @@ this.estimatedSize = Utils.pInt(oJsonAttachment.EstimatedSize); this.isInline = !!oJsonAttachment.IsInline; this.isLinked = !!oJsonAttachment.IsLinked; + this.isThumbnail = !!oJsonAttachment.IsThumbnail; this.cid = oJsonAttachment.CID; this.contentLocation = oJsonAttachment.ContentLocation; this.download = oJsonAttachment.Download; @@ -106,6 +109,14 @@ ); }; + /** + * @return {boolean} + */ + AttachmentModel.prototype.hasThumbnail = function () + { + return this.isThumbnail; + }; + /** * @return {boolean} */ @@ -132,6 +143,14 @@ !this.isPdf() && !this.isText() && !this.isImage(); }; + /** + * @return {boolean} + */ + AttachmentModel.prototype.hasPreview = function () + { + return this.isImage() || this.isPdf() || this.isText() || this.isFramed(); + }; + /** * @return {string} */ @@ -148,6 +167,15 @@ return Links.attachmentPreview(this.download); }; + /** + * @return {string} + */ + AttachmentModel.prototype.linkThumbnailPreviewStyle = function () + { + return !this.hasThumbnail() ? '' : + 'background:url(' + Links.attachmentThumbnailPreview(this.download) + ')'; + }; + /** * @return {string} */ diff --git a/dev/Styles/MessageList.less b/dev/Styles/MessageList.less index 831f2262f..ae3e96d0f 100644 --- a/dev/Styles/MessageList.less +++ b/dev/Styles/MessageList.less @@ -52,7 +52,7 @@ html.rl-no-preview-pane { padding-left: 8px; padding-right: 8px; } - + .b-message-list-wrapper { position: absolute; top: 50px; @@ -275,7 +275,7 @@ html.rl-no-preview-pane { .flagParent { display: inline-block; float: right; - padding: 0 8px 0 5px; + padding: 0 10px 0 5px; } &.e-single-line .flagParent { @@ -299,7 +299,7 @@ html.rl-no-preview-pane { display: inline-block; float: right; position: relative; - margin: 2px 8px 0 5px; + margin: 2px 10px 0 5px; } &.e-single-line .attachmentParent { @@ -374,7 +374,7 @@ html.rl-no-preview-pane { overflow: hidden; text-overflow: ellipsis; } - + .subject-prefix { color: #888; /*font-style: italic;*/ @@ -449,11 +449,11 @@ html.rl-no-preview-pane { .sidebarParent { background-color: #bdc3c7; } - + &.focused .sidebarParent { background-color: darken(#bdc3c7, 10%) !important; } - + &.unseen { background-color: darken(#ecf0f1, 5%); @@ -489,13 +489,13 @@ html.rl-no-preview-pane { background-color: #398CF2; .opacity(20); } - + + .messageListItem .delimiter { background-color: #398CF2; opacity: 0.3; } } - + &.hasFlaggedSubMessage { .flagOff, .flagOn { display: none; diff --git a/dev/Styles/MessageView.less b/dev/Styles/MessageView.less index ec5e3a51e..159ca78fb 100644 --- a/dev/Styles/MessageView.less +++ b/dev/Styles/MessageView.less @@ -70,6 +70,13 @@ html.rl-no-preview-pane { -webkit-overflow-scrolling: touch; } + .messageItemDropdown { + z-index: 100; + position: fixed; + top: 70px; + right: 25px; + } + .messageItem { position: absolute; @@ -94,8 +101,8 @@ html.rl-no-preview-pane { .buttonUp, .buttonUnFull, .buttonFull { display: inline-block; position: fixed; - right: 30px; - top: 90px; + right: 25px; + bottom: 25px; height: 30px; width: 30px; text-align: center; @@ -119,11 +126,11 @@ html.rl-no-preview-pane { } .buttonUp { - right: 70px; + right: 65px; z-index: 0; } - .buttonUnFull { + .buttonUp, .buttonUnFull { display: none; } @@ -230,8 +237,7 @@ html.rl-no-preview-pane { display: inline-block; margin: 5px; - padding: 5px; - max-width: 170px; + max-width: 200px; min-width: 60px; overflow: hidden; cursor: pointer; @@ -246,19 +252,83 @@ html.rl-no-preview-pane { border-radius: 2px; - .attachmentIcon { - font-size: 23px; - width: 23px; - height: 23px; - color: #666; + .attachmentIconParent { + height: 56px; + width: 60px; + + background: none; } - .attachmentPreview { - color: #999; - margin: 0px 5px; - } - .attachmentPreview:hover { + .attachmentNameParent { + margin-left: 60px; + padding: 4px; + padding-left: 6px; + min-width: 90px; + + color: #fff; + background: #eee; + color: #333; + background: #fafafa; + border-left: 1px solid #ddd; + } + + &.hasThumbnail .attachmentNameParent { + background: rgba(0, 0, 0, .6); + + color: #fff; + text-shadow: 0 1px 0 #000; + border-left: 0px; + } + + .attachmentIcon { + margin: 6px 0 0 13px; + font-size: 36px; + width: 36px; + height: 36px; + color: #aaa; + } + + .attachmentIconParent.hasPreview { + .attachmentNonePreview { + display: none; + } + } + + a.attachmentPreview { + display: inline-block; + height: 100%; + width: 100%; + } + + .attachmentIconParent.hasPreview:hover { + + background: #555 !important; + background-image: none !important; + background: rgba(0, 0, 0, .8) !important; + + color: #fff; + + .attachmentIcon { + color: #fff; + text-shadow: 0 1px 0 #000; + } + } + + .attachmentIconParent.hasPreview .show-hover { + display: none; + } + + .attachmentIconParent.hasPreview:hover .show-hover { + display: inline-block; + } + + .attachmentIconParent.hasPreview:hover .hide-hover { + display: none; + } + + &.hasThumbnail .attachmentMainIcon { + display: none; } } } @@ -382,6 +452,7 @@ html.rl-no-preview-pane .messageView { .toolbar { padding-left: @rlMainBorderSize; } + .b-content { top: 50px; bottom: @rlBottomMargin; @@ -389,10 +460,6 @@ html.rl-no-preview-pane .messageView { border: @rlMainBorderSize solid @rlMainDarkColor; box-shadow: @rlMainShadow; border-radius: @rlMainBorderRadius; - - .buttonUp, .buttonUnFull, .buttonFull { - top: 70px; - } } } @@ -423,9 +490,12 @@ html.rl-message-fullscreen { border: @rlLowBorderSize solid @rlMainDarkColor; border-radius: @rlLowBorderRadius; - .buttonUp, .buttonUnFull { + .messageItemDropdown { + top: 20px; + } + + .buttonUnFull { display: inline-block; - top: 36px; } .buttonFull { @@ -433,3 +503,7 @@ html.rl-message-fullscreen { } } } + +.nano-scrolllimit-top .buttonUp { + display: inline-block !important; +} diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index e95094beb..668f73367 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -81,7 +81,10 @@ } }, this); - this.canBeRepliedOrForwarded = this.messageVisibility; + this.canBeRepliedOrForwarded = ko.computed(function () { + var bV = this.messageVisibility(); + return !this.isDraftFolder() && bV; + }, this); // commands this.closeMessage = Utils.createCommand(this, function () { diff --git a/package.json b/package.json index f9d61a4f0..7a2e91279 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "RainLoop", "title": "RainLoop Webmail", - "version": "1.7.0", - "release": "211", + "version": "1.7.1", + "release": "215", "description": "Simple, modern & fast web-based email client", "homepage": "http://rainloop.net", "main": "gulpfile.js", diff --git a/rainloop/v/0.0.0/app/handle.php b/rainloop/v/0.0.0/app/handle.php index ece49ec3b..b67e385c4 100644 --- a/rainloop/v/0.0.0/app/handle.php +++ b/rainloop/v/0.0.0/app/handle.php @@ -35,6 +35,14 @@ if (!\defined('RAINLOOP_APP_LIBRARIES_PATH')) { return include RAINLOOP_APP_LIBRARIES_PATH.'GuzzleHttp/'.\str_replace('\\', '/', \substr($sClassName, 11)).'.php'; } + else if (0 === \strpos($sClassName, 'Symfony') && false !== \strpos($sClassName, '\\')) + { + return include RAINLOOP_APP_LIBRARIES_PATH.'Symfony/'.\str_replace('\\', '/', \substr($sClassName, 8)).'.php'; + } + else if (0 === \strpos($sClassName, 'PHPThumb') && false !== \strpos($sClassName, '\\')) + { + return include RAINLOOP_APP_LIBRARIES_PATH.'PHPThumb/'.\str_replace('\\', '/', \substr($sClassName, 9)).'.php'; + } else if (0 === \strpos($sClassName, 'Sabre') && false !== \strpos($sClassName, '\\')) { if (!RAINLOOP_MB_SUPPORTED && !defined('RL_MB_FIXED')) @@ -67,7 +75,7 @@ if (\class_exists('RainLoop\Service')) if (!\defined('APP_API_STARTED')) { \define('APP_API_STARTED', true); - + \RainLoop\Api::Handle(); } } diff --git a/rainloop/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php b/rainloop/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php new file mode 100644 index 000000000..dfe6ff609 --- /dev/null +++ b/rainloop/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php @@ -0,0 +1,161 @@ +rSream = self::$aStreams[$sHashName]; + $bResult = true; + } + else + { + $this->rSream = fopen('php://memory', 'r+b'); + self::$aStreams[$sHashName] = $this->rSream; + + $bResult = true; + + \MailSo\Base\Loader::IncStatistic('CreateStream/TempFile'); + } + } + + return $bResult; + } + + /** + * @return bool + */ + public function stream_close() + { + return -1 !== fseek($this->rSream, 0); + } + + /** + * @return bool + */ + public function stream_flush() + { + return fflush($this->rSream); + } + + /** + * @param int $iLen + * + * @return string + */ + public function stream_read($iLen) + { + return fread($this->rSream, $iLen); + } + + /** + * @param string $sInputString + * + * @return int + */ + public function stream_write($sInputString) + { + return fwrite($this->rSream, $sInputString); + } + + /** + * @return int + */ + public function stream_tell() + { + return ftell($this->rSream); + } + + /** + * @return bool + */ + public function stream_eof() + { + return feof($this->rSream); + } + + /** + * @return array + */ + public function stream_stat() + { + return fstat($this->rSream); + } + + /** + * @param int $iOffset + * @param int $iWhence = SEEK_SET + * + * @return int + */ + public function stream_seek($iOffset, $iWhence = SEEK_SET) + { + return fseek($this->rSream, $iOffset, $iWhence); + } +} diff --git a/rainloop/v/0.0.0/app/libraries/MailSo/Base/Utils.php b/rainloop/v/0.0.0/app/libraries/MailSo/Base/Utils.php index 45b5c3162..6882f082f 100644 --- a/rainloop/v/0.0.0/app/libraries/MailSo/Base/Utils.php +++ b/rainloop/v/0.0.0/app/libraries/MailSo/Base/Utils.php @@ -1215,7 +1215,7 @@ class Utils */ public static function ClearFileName($sFileName) { - return \MailSo\Base\Utils::ClearNullBite(\preg_replace('/[\s]+/', ' ', + return \MailSo\Base\Utils::ClearNullBite(\preg_replace('/[\s]+/u', ' ', \str_replace(array('"', '/', '\\', '*', '?', '<', '>', '|', ':'), ' ', $sFileName))); } diff --git a/rainloop/v/0.0.0/app/libraries/PHPThumb/GD.php b/rainloop/v/0.0.0/app/libraries/PHPThumb/GD.php new file mode 100644 index 000000000..190d8a0ad --- /dev/null +++ b/rainloop/v/0.0.0/app/libraries/PHPThumb/GD.php @@ -0,0 +1,1417 @@ + + * Copyright (c) 2009, Ian Selby/Gen X Design + * + * Author(s): Ian Selby + * + * Licensed under the MIT License + * Redistributions of files must retain the above copyright notice. + * + * @author Ian Selby + * @copyright Copyright (c) 2009 Gen X Design + * @link http://phpthumb.gxdlabs.com + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ + +class GD extends PHPThumb +{ + /** + * The prior image (before manipulation) + * + * @var resource + */ + protected $oldImage; + + /** + * The working image (used during manipulation) + * + * @var resource + */ + protected $workingImage; + + /** + * The current dimensions of the image + * + * @var array + */ + protected $currentDimensions; + + /** + * The new, calculated dimensions of the image + * + * @var array + */ + protected $newDimensions; + + /** + * The options for this class + * + * This array contains various options that determine the behavior in + * various functions throughout the class. Functions note which specific + * option key / values are used in their documentation + * + * @var array + */ + protected $options; + + /** + * The maximum width an image can be after resizing (in pixels) + * + * @var int + */ + protected $maxWidth; + + /** + * The maximum height an image can be after resizing (in pixels) + * + * @var int + */ + protected $maxHeight; + + /** + * The percentage to resize the image by + * + * @var int + */ + protected $percent; + + /** + * @param string $fileName + * @param array $options + * @param array $plugins + */ + public function __construct($fileName, $options = array(), array $plugins = array()) + { + parent::__construct($fileName, $options, $plugins); + + $this->determineFormat(); + $this->verifyFormatCompatiblity(); + + switch ($this->format) { + case 'GIF': + $this->oldImage = @imagecreatefromgif($this->fileName); + break; + case 'JPG': + $this->oldImage = @imagecreatefromjpeg($this->fileName); + break; + case 'PNG': + $this->oldImage = @imagecreatefrompng($this->fileName); + break; + case 'STRING': + $this->oldImage = @imagecreatefromstring($this->fileName); + break; + } + + if (!is_resource($this->oldImage)) + { + throw new \Exception('Invalid image file'); + } + else + { + $this->currentDimensions = array ( + 'width' => imagesx($this->oldImage), + 'height' => imagesy($this->oldImage) + ); + } + } + + public function __destruct() + { + if (is_resource($this->oldImage)) { + imagedestroy($this->oldImage); + } + + if (is_resource($this->workingImage)) { + imagedestroy($this->workingImage); + } + } + + /** + * Pad an image to desired dimensions. Moves the image into the center and fills the rest with $color. + * @param $width + * @param $height + * @param array $color + * @return GD + */ + public function pad($width, $height, $color = array(255, 255, 255)) + { + // no resize - woohoo! + if ($width == $this->currentDimensions['width'] && $height == $this->currentDimensions['height']) { + return $this; + } + + // create the working image + if (function_exists('imagecreatetruecolor')) { + $this->workingImage = imagecreatetruecolor($width, $height); + } else { + $this->workingImage = imagecreate($width, $height); + } + + // create the fill color + $fillColor = imagecolorallocate( + $this->workingImage, + $color[0], + $color[1], + $color[2] + ); + + // fill our working image with the fill color + imagefill( + $this->workingImage, + 0, + 0, + $fillColor + ); + + // copy the image into the center of our working image + imagecopyresampled( + $this->workingImage, + $this->oldImage, + intval(($width-$this->currentDimensions['width']) / 2), + intval(($height-$this->currentDimensions['height']) / 2), + 0, + 0, + $this->currentDimensions['width'], + $this->currentDimensions['height'], + $this->currentDimensions['width'], + $this->currentDimensions['height'] + ); + + // update all the variables and resources to be correct + $this->oldImage = $this->workingImage; + $this->currentDimensions['width'] = $width; + $this->currentDimensions['height'] = $height; + + return $this; + } + + /** + * Resizes an image to be no larger than $maxWidth or $maxHeight + * + * If either param is set to zero, then that dimension will not be considered as a part of the resize. + * Additionally, if $this->options['resizeUp'] is set to true (false by default), then this function will + * also scale the image up to the maximum dimensions provided. + * + * @param int $maxWidth The maximum width of the image in pixels + * @param int $maxHeight The maximum height of the image in pixels + * @return \PHPThumb\GD + */ + public function resize($maxWidth = 0, $maxHeight = 0) + { + // make sure our arguments are valid + if (!is_numeric($maxWidth)) { + throw new \InvalidArgumentException('$maxWidth must be numeric'); + } + + if (!is_numeric($maxHeight)) { + throw new \InvalidArgumentException('$maxHeight must be numeric'); + } + + // make sure we're not exceeding our image size if we're not supposed to + if ($this->options['resizeUp'] === false) { + $this->maxHeight = (intval($maxHeight) > $this->currentDimensions['height']) ? $this->currentDimensions['height'] : $maxHeight; + $this->maxWidth = (intval($maxWidth) > $this->currentDimensions['width']) ? $this->currentDimensions['width'] : $maxWidth; + } else { + $this->maxHeight = intval($maxHeight); + $this->maxWidth = intval($maxWidth); + } + + // get the new dimensions... + $this->calcImageSize($this->currentDimensions['width'], $this->currentDimensions['height']); + + // create the working image + if (function_exists('imagecreatetruecolor')) { + $this->workingImage = imagecreatetruecolor($this->newDimensions['newWidth'], $this->newDimensions['newHeight']); + } else { + $this->workingImage = imagecreate($this->newDimensions['newWidth'], $this->newDimensions['newHeight']); + } + + $this->preserveAlpha(); + + // and create the newly sized image + imagecopyresampled( + $this->workingImage, + $this->oldImage, + 0, + 0, + 0, + 0, + $this->newDimensions['newWidth'], + $this->newDimensions['newHeight'], + $this->currentDimensions['width'], + $this->currentDimensions['height'] + ); + + // update all the variables and resources to be correct + $this->oldImage = $this->workingImage; + $this->currentDimensions['width'] = $this->newDimensions['newWidth']; + $this->currentDimensions['height'] = $this->newDimensions['newHeight']; + + return $this; + } + + /** + * Adaptively Resizes the Image + * + * This function attempts to get the image to as close to the provided dimensions as possible, and then crops the + * remaining overflow (from the center) to get the image to be the size specified + * + * @param int $maxWidth + * @param int $maxHeight + * @return \PHPThumb\GD + */ + public function adaptiveResize($width, $height) + { + // make sure our arguments are valid + if ((!is_numeric($width) || $width == 0) && (!is_numeric($height) || $height == 0)) { + throw new \InvalidArgumentException('$width and $height must be numeric and greater than zero'); + } + + if (!is_numeric($width) || $width == 0) { + $width = ($height * $this->currentDimensions['width']) / $this->currentDimensions['height']; + } + + if (!is_numeric($height) || $height == 0) { + $height = ($width * $this->currentDimensions['height']) / $this->currentDimensions['width']; + } + + // make sure we're not exceeding our image size if we're not supposed to + if ($this->options['resizeUp'] === false) { + $this->maxHeight = (intval($height) > $this->currentDimensions['height']) ? $this->currentDimensions['height'] : $height; + $this->maxWidth = (intval($width) > $this->currentDimensions['width']) ? $this->currentDimensions['width'] : $width; + } else { + $this->maxHeight = intval($height); + $this->maxWidth = intval($width); + } + + $this->calcImageSizeStrict($this->currentDimensions['width'], $this->currentDimensions['height']); + + // resize the image to be close to our desired dimensions + $this->resize($this->newDimensions['newWidth'], $this->newDimensions['newHeight']); + + // reset the max dimensions... + if ($this->options['resizeUp'] === false) { + $this->maxHeight = (intval($height) > $this->currentDimensions['height']) ? $this->currentDimensions['height'] : $height; + $this->maxWidth = (intval($width) > $this->currentDimensions['width']) ? $this->currentDimensions['width'] : $width; + } else { + $this->maxHeight = intval($height); + $this->maxWidth = intval($width); + } + + // create the working image + if (function_exists('imagecreatetruecolor')) { + $this->workingImage = imagecreatetruecolor($this->maxWidth, $this->maxHeight); + } else { + $this->workingImage = imagecreate($this->maxWidth, $this->maxHeight); + } + + $this->preserveAlpha(); + + $cropWidth = $this->maxWidth; + $cropHeight = $this->maxHeight; + $cropX = 0; + $cropY = 0; + + // now, figure out how to crop the rest of the image... + if ($this->currentDimensions['width'] > $this->maxWidth) { + $cropX = intval(($this->currentDimensions['width'] - $this->maxWidth) / 2); + } elseif ($this->currentDimensions['height'] > $this->maxHeight) { + $cropY = intval(($this->currentDimensions['height'] - $this->maxHeight) / 2); + } + + imagecopyresampled( + $this->workingImage, + $this->oldImage, + 0, + 0, + $cropX, + $cropY, + $cropWidth, + $cropHeight, + $cropWidth, + $cropHeight + ); + + // update all the variables and resources to be correct + $this->oldImage = $this->workingImage; + $this->currentDimensions['width'] = $this->maxWidth; + $this->currentDimensions['height'] = $this->maxHeight; + + return $this; + } + + /** + * Adaptively Resizes the Image and Crops Using a Percentage + * + * This function attempts to get the image to as close to the provided dimensions as possible, and then crops the + * remaining overflow using a provided percentage to get the image to be the size specified. + * + * The percentage mean different things depending on the orientation of the original image. + * + * For Landscape images: + * --------------------- + * + * A percentage of 1 would crop the image all the way to the left, which would be the same as + * using adaptiveResizeQuadrant() with $quadrant = 'L' + * + * A percentage of 50 would crop the image to the center which would be the same as using + * adaptiveResizeQuadrant() with $quadrant = 'C', or even the original adaptiveResize() + * + * A percentage of 100 would crop the image to the image all the way to the right, etc, etc. + * Note that you can use any percentage between 1 and 100. + * + * For Portrait images: + * -------------------- + * + * This works the same as for Landscape images except that a percentage of 1 means top and 100 means bottom + * + * @param int $maxWidth + * @param int $maxHeight + * @param int $percent + * @return \PHPThumb\GD + */ + public function adaptiveResizePercent($width, $height, $percent = 50) + { + // make sure our arguments are valid + if (!is_numeric($width) || $width == 0) { + throw new \InvalidArgumentException('$width must be numeric and greater than zero'); + } + + if (!is_numeric($height) || $height == 0) { + throw new \InvalidArgumentException('$height must be numeric and greater than zero'); + } + + // make sure we're not exceeding our image size if we're not supposed to + if ($this->options['resizeUp'] === false) { + $this->maxHeight = (intval($height) > $this->currentDimensions['height']) ? $this->currentDimensions['height'] : $height; + $this->maxWidth = (intval($width) > $this->currentDimensions['width']) ? $this->currentDimensions['width'] : $width; + } else { + $this->maxHeight = intval($height); + $this->maxWidth = intval($width); + } + + $this->calcImageSizeStrict($this->currentDimensions['width'], $this->currentDimensions['height']); + + // resize the image to be close to our desired dimensions + $this->resize($this->newDimensions['newWidth'], $this->newDimensions['newHeight']); + + // reset the max dimensions... + if ($this->options['resizeUp'] === false) { + $this->maxHeight = (intval($height) > $this->currentDimensions['height']) ? $this->currentDimensions['height'] : $height; + $this->maxWidth = (intval($width) > $this->currentDimensions['width']) ? $this->currentDimensions['width'] : $width; + } else { + $this->maxHeight = intval($height); + $this->maxWidth = intval($width); + } + + // create the working image + if (function_exists('imagecreatetruecolor')) { + $this->workingImage = imagecreatetruecolor($this->maxWidth, $this->maxHeight); + } else { + $this->workingImage = imagecreate($this->maxWidth, $this->maxHeight); + } + + $this->preserveAlpha(); + + $cropWidth = $this->maxWidth; + $cropHeight = $this->maxHeight; + $cropX = 0; + $cropY = 0; + + // Crop the rest of the image using the quadrant + + if ($percent > 100) { + $percent = 100; + } elseif ($percent < 1) { + $percent = 1; + } + + if ($this->currentDimensions['width'] > $this->maxWidth) { + // Image is landscape + $maxCropX = $this->currentDimensions['width'] - $this->maxWidth; + $cropX = intval(($percent / 100) * $maxCropX); + + } elseif ($this->currentDimensions['height'] > $this->maxHeight) { + // Image is portrait + $maxCropY = $this->currentDimensions['height'] - $this->maxHeight; + $cropY = intval(($percent / 100) * $maxCropY); + } + + imagecopyresampled( + $this->workingImage, + $this->oldImage, + 0, + 0, + $cropX, + $cropY, + $cropWidth, + $cropHeight, + $cropWidth, + $cropHeight + ); + + // update all the variables and resources to be correct + $this->oldImage = $this->workingImage; + $this->currentDimensions['width'] = $this->maxWidth; + $this->currentDimensions['height'] = $this->maxHeight; + + return $this; + } + + /** + * Adaptively Resizes the Image and Crops Using a Quadrant + * + * This function attempts to get the image to as close to the provided dimensions as possible, and then crops the + * remaining overflow using the quadrant to get the image to be the size specified. + * + * The quadrants available are Top, Bottom, Center, Left, and Right: + * + * + * +---+---+---+ + * | | T | | + * +---+---+---+ + * | L | C | R | + * +---+---+---+ + * | | B | | + * +---+---+---+ + * + * Note that if your image is Landscape and you choose either of the Top or Bottom quadrants (which won't + * make sence since only the Left and Right would be available, then the Center quadrant will be used + * to crop. This would have exactly the same result as using adaptiveResize(). + * The same goes if your image is portrait and you choose either the Left or Right quadrants. + * + * @param int $maxWidth + * @param int $maxHeight + * @param string $quadrant T, B, C, L, R + * @return \PHPThumb\GD + */ + public function adaptiveResizeQuadrant($width, $height, $quadrant = 'C') + { + // make sure our arguments are valid + if (!is_numeric($width) || $width == 0) { + throw new \InvalidArgumentException('$width must be numeric and greater than zero'); + } + + if (!is_numeric($height) || $height == 0) { + throw new \InvalidArgumentException('$height must be numeric and greater than zero'); + } + + // make sure we're not exceeding our image size if we're not supposed to + if ($this->options['resizeUp'] === false) { + $this->maxHeight = (intval($height) > $this->currentDimensions['height']) ? $this->currentDimensions['height'] : $height; + $this->maxWidth = (intval($width) > $this->currentDimensions['width']) ? $this->currentDimensions['width'] : $width; + } else { + $this->maxHeight = intval($height); + $this->maxWidth = intval($width); + } + + $this->calcImageSizeStrict($this->currentDimensions['width'], $this->currentDimensions['height']); + + // resize the image to be close to our desired dimensions + $this->resize($this->newDimensions['newWidth'], $this->newDimensions['newHeight']); + + // reset the max dimensions... + if ($this->options['resizeUp'] === false) { + $this->maxHeight = (intval($height) > $this->currentDimensions['height']) ? $this->currentDimensions['height'] : $height; + $this->maxWidth = (intval($width) > $this->currentDimensions['width']) ? $this->currentDimensions['width'] : $width; + } else { + $this->maxHeight = intval($height); + $this->maxWidth = intval($width); + } + + // create the working image + if (function_exists('imagecreatetruecolor')) { + $this->workingImage = imagecreatetruecolor($this->maxWidth, $this->maxHeight); + } else { + $this->workingImage = imagecreate($this->maxWidth, $this->maxHeight); + } + + $this->preserveAlpha(); + + $cropWidth = $this->maxWidth; + $cropHeight = $this->maxHeight; + $cropX = 0; + $cropY = 0; + + // Crop the rest of the image using the quadrant + + if ($this->currentDimensions['width'] > $this->maxWidth) { + // Image is landscape + switch ($quadrant) { + case 'L': + $cropX = 0; + break; + case 'R': + $cropX = intval(($this->currentDimensions['width'] - $this->maxWidth)); + break; + case 'C': + default: + $cropX = intval(($this->currentDimensions['width'] - $this->maxWidth) / 2); + break; + } + } elseif ($this->currentDimensions['height'] > $this->maxHeight) { + // Image is portrait + switch ($quadrant) { + case 'T': + $cropY = 0; + break; + case 'B': + $cropY = intval(($this->currentDimensions['height'] - $this->maxHeight)); + break; + case 'C': + default: + $cropY = intval(($this->currentDimensions['height'] - $this->maxHeight) / 2); + break; + } + } + + imagecopyresampled( + $this->workingImage, + $this->oldImage, + 0, + 0, + $cropX, + $cropY, + $cropWidth, + $cropHeight, + $cropWidth, + $cropHeight + ); + + // update all the variables and resources to be correct + $this->oldImage = $this->workingImage; + $this->currentDimensions['width'] = $this->maxWidth; + $this->currentDimensions['height'] = $this->maxHeight; + + return $this; + } + + /** + * Resizes an image by a given percent uniformly, + * Percentage should be whole number representation (i.e. 1-100) + * + * @param int $percent + * @return GD + * @throws \InvalidArgumentException + */ + public function resizePercent($percent = 0) + { + if (!is_numeric($percent)) { + throw new \InvalidArgumentException ('$percent must be numeric'); + } + + $this->percent = intval($percent); + + $this->calcImageSizePercent($this->currentDimensions['width'], $this->currentDimensions['height']); + + if (function_exists('imagecreatetruecolor')) { + $this->workingImage = imagecreatetruecolor($this->newDimensions['newWidth'], $this->newDimensions['newHeight']); + } else { + $this->workingImage = imagecreate($this->newDimensions['newWidth'], $this->newDimensions['newHeight']); + } + + $this->preserveAlpha(); + + imagecopyresampled( + $this->workingImage, + $this->oldImage, + 0, + 0, + 0, + 0, + $this->newDimensions['newWidth'], + $this->newDimensions['newHeight'], + $this->currentDimensions['width'], + $this->currentDimensions['height'] + ); + + $this->oldImage = $this->workingImage; + $this->currentDimensions['width'] = $this->newDimensions['newWidth']; + $this->currentDimensions['height'] = $this->newDimensions['newHeight']; + + return $this; + } + + /** + * Crops an image from the center with provided dimensions + * + * If no height is given, the width will be used as a height, thus creating a square crop + * + * @param int $cropWidth + * @param int $cropHeight + * @return \PHPThumb\GD + */ + public function cropFromCenter($cropWidth, $cropHeight = null) + { + if (!is_numeric($cropWidth)) { + throw new \InvalidArgumentException('$cropWidth must be numeric'); + } + + if ($cropHeight !== null && !is_numeric($cropHeight)) { + throw new \InvalidArgumentException('$cropHeight must be numeric'); + } + + if ($cropHeight === null) { + $cropHeight = $cropWidth; + } + + $cropWidth = ($this->currentDimensions['width'] < $cropWidth) ? $this->currentDimensions['width'] : $cropWidth; + $cropHeight = ($this->currentDimensions['height'] < $cropHeight) ? $this->currentDimensions['height'] : $cropHeight; + + $cropX = intval(($this->currentDimensions['width'] - $cropWidth) / 2); + $cropY = intval(($this->currentDimensions['height'] - $cropHeight) / 2); + + $this->crop($cropX, $cropY, $cropWidth, $cropHeight); + + return $this; + } + + /** + * Vanilla Cropping - Crops from x,y with specified width and height + * + * @param int $startX + * @param int $startY + * @param int $cropWidth + * @param int $cropHeight + * @return \PHPThumb\GD + */ + public function crop($startX, $startY, $cropWidth, $cropHeight) + { + // validate input + if (!is_numeric($startX)) { + throw new \InvalidArgumentException('$startX must be numeric'); + } + + if (!is_numeric($startY)) { + throw new \InvalidArgumentException('$startY must be numeric'); + } + + if (!is_numeric($cropWidth)) { + throw new \InvalidArgumentException('$cropWidth must be numeric'); + } + + if (!is_numeric($cropHeight)) { + throw new \InvalidArgumentException('$cropHeight must be numeric'); + } + + // do some calculations + $cropWidth = ($this->currentDimensions['width'] < $cropWidth) ? $this->currentDimensions['width'] : $cropWidth; + $cropHeight = ($this->currentDimensions['height'] < $cropHeight) ? $this->currentDimensions['height'] : $cropHeight; + + // ensure everything's in bounds + if (($startX + $cropWidth) > $this->currentDimensions['width']) { + $startX = ($this->currentDimensions['width'] - $cropWidth); + } + + if (($startY + $cropHeight) > $this->currentDimensions['height']) { + $startY = ($this->currentDimensions['height'] - $cropHeight); + } + + if ($startX < 0) { + $startX = 0; + } + + if ($startY < 0) { + $startY = 0; + } + + // create the working image + if (function_exists('imagecreatetruecolor')) { + $this->workingImage = imagecreatetruecolor($cropWidth, $cropHeight); + } else { + $this->workingImage = imagecreate($cropWidth, $cropHeight); + } + + $this->preserveAlpha(); + + imagecopyresampled( + $this->workingImage, + $this->oldImage, + 0, + 0, + $startX, + $startY, + $cropWidth, + $cropHeight, + $cropWidth, + $cropHeight + ); + + $this->oldImage = $this->workingImage; + $this->currentDimensions['width'] = $cropWidth; + $this->currentDimensions['height'] = $cropHeight; + + return $this; + } + + /** + * Rotates image either 90 degrees clockwise or counter-clockwise + * + * @param string $direction + * @retunrn \PHPThumb\GD + */ + public function rotateImage($direction = 'CW') + { + if ($direction == 'CW') { + $this->rotateImageNDegrees(90); + } else { + $this->rotateImageNDegrees(-90); + } + + return $this; + } + + /** + * Rotates image specified number of degrees + * + * @param int $degrees + * @return \PHPThumb\GD + */ + public function rotateImageNDegrees($degrees) + { + if (!is_numeric($degrees)) { + throw new \InvalidArgumentException('$degrees must be numeric'); + } + + if (!function_exists('imagerotate')) { + throw new \RuntimeException('Your version of GD does not support image rotation'); + } + + $this->workingImage = imagerotate($this->oldImage, $degrees, 0); + + $newWidth = $this->currentDimensions['height']; + $newHeight = $this->currentDimensions['width']; + $this->oldImage = $this->workingImage; + $this->currentDimensions['width'] = $newWidth; + $this->currentDimensions['height'] = $newHeight; + + return $this; + } + + /** + * Applies a filter to the image + * + * @param int $filter + * @return \PHPThumb\GD + */ + public function imageFilter($filter, $arg1 = false, $arg2 = false, $arg3 = false, $arg4 = false) + { + if (!is_numeric($filter)) { + throw new \InvalidArgumentException('$filter must be numeric'); + } + + if (!function_exists('imagefilter')) { + throw new \RuntimeException('Your version of GD does not support image filters'); + } + + $result = false; + if ($arg1 === false) { + $result = imagefilter($this->oldImage, $filter); + } elseif ($arg2 === false) { + $result = imagefilter($this->oldImage, $filter, $arg1); + } elseif ($arg3 === false) { + $result = imagefilter($this->oldImage, $filter, $arg1, $arg2); + } elseif ($arg4 === false) { + $result = imagefilter($this->oldImage, $filter, $arg1, $arg2, $arg3); + } else { + $result = imagefilter($this->oldImage, $filter, $arg1, $arg2, $arg3, $arg4); + } + + if (!$result) { + throw new \RuntimeException('GD imagefilter failed'); + } + + $this->workingImage = $this->oldImage; + + return $this; + } + + /** + * Shows an image + * + * This function will show the current image by first sending the appropriate header + * for the format, and then outputting the image data. If headers have already been sent, + * a runtime exception will be thrown + * + * @param bool $rawData Whether or not the raw image stream should be output + * @return \PHPThumb\GD + */ + public function show($rawData = false) + { + //Execute any plugins + if ($this->plugins) { + foreach ($this->plugins as $plugin) { + /* @var $plugin \PHPThumb\PluginInterface */ + $plugin->execute($this); + } + } + + if (headers_sent() && php_sapi_name() != 'cli') { + throw new \RuntimeException('Cannot show image, headers have already been sent'); + } + + // When the interlace option equals true or false call imageinterlace else leave it to default + if ($this->options['interlace'] === true) { + imageinterlace($this->oldImage, 1); + } elseif ($this->options['interlace'] === false) { + imageinterlace($this->oldImage, 0); + } + + switch ($this->format) { + case 'GIF': + if ($rawData === false) { + header('Content-type: image/gif'); + } + imagegif($this->oldImage); + break; + case 'JPG': + if ($rawData === false) { + header('Content-type: image/jpeg'); + } + imagejpeg($this->oldImage, null, $this->options['jpegQuality']); + break; + case 'PNG': + case 'STRING': + if ($rawData === false) { + header('Content-type: image/png'); + } + imagepng($this->oldImage); + break; + } + + return $this; + } + + /** + * Returns the Working Image as a String + * + * This function is useful for getting the raw image data as a string for storage in + * a database, or other similar things. + * + * @return string + */ + public function getImageAsString() + { + $data = null; + ob_start(); + $this->show(true); + $data = ob_get_contents(); + ob_end_clean(); + + return $data; + } + + /** + * Saves an image + * + * This function will make sure the target directory is writeable, and then save the image. + * + * If the target directory is not writeable, the function will try to correct the permissions (if allowed, this + * is set as an option ($this->options['correctPermissions']). If the target cannot be made writeable, then a + * \RuntimeException is thrown. + * + * @param string $fileName The full path and filename of the image to save + * @param string $format The format to save the image in (optional, must be one of [GIF,JPG,PNG] + * @return \PHPThumb\GD + */ + public function save($fileName, $format = null) + { + $validFormats = array('GIF', 'JPG', 'PNG'); + $format = ($format !== null) ? strtoupper($format) : $this->format; + + if (!in_array($format, $validFormats)) { + throw new \InvalidArgumentException("Invalid format type specified in save function: {$format}"); + } + + // make sure the directory is writeable + if (!is_writeable(dirname($fileName))) { + // try to correct the permissions + if ($this->options['correctPermissions'] === true) { + @chmod(dirname($fileName), 0777); + + // throw an exception if not writeable + if (!is_writeable(dirname($fileName))) { + throw new \RuntimeException("File is not writeable, and could not correct permissions: {$fileName}"); + } + } else { // throw an exception if not writeable + throw new \RuntimeException("File not writeable: {$fileName}"); + } + } + + // When the interlace option equals true or false call imageinterlace else leave it to default + if ($this->options['interlace'] === true) { + imageinterlace($this->oldImage, 1); + } elseif ($this->options['interlace'] === false) { + imageinterlace($this->oldImage, 0); + } + + switch ($format) { + case 'GIF': + imagegif($this->oldImage, $fileName); + break; + case 'JPG': + imagejpeg($this->oldImage, $fileName, $this->options['jpegQuality']); + break; + case 'PNG': + imagepng($this->oldImage, $fileName); + break; + } + + return $this; + } + + ################################# + # ----- GETTERS / SETTERS ----- # + ################################# + + /** + * Sets options for all operations. + * @param array $options + * @return GD + */ + public function setOptions(array $options = array()) + { + // we've yet to init the default options, so create them here + if (sizeof($this->options) == 0) { + $defaultOptions = array( + 'resizeUp' => false, + 'jpegQuality' => 100, + 'correctPermissions' => false, + 'preserveAlpha' => true, + 'alphaMaskColor' => array (255, 255, 255), + 'preserveTransparency' => true, + 'transparencyMaskColor' => array (0, 0, 0), + 'interlace' => null + ); + } else { // otherwise, let's use what we've got already + $defaultOptions = $this->options; + } + + $this->options = array_merge($defaultOptions, $options); + + return $this; + } + + /** + * Returns $currentDimensions. + * + * @see \PHPThumb\GD::$currentDimensions + */ + public function getCurrentDimensions() + { + return $this->currentDimensions; + } + + /** + * @param $currentDimensions + * @return GD + */ + public function setCurrentDimensions($currentDimensions) + { + $this->currentDimensions = $currentDimensions; + + return $this; + } + + /** + * @return int + */ + public function getMaxHeight() + { + return $this->maxHeight; + } + + /** + * @param $maxHeight + * @return GD + */ + public function setMaxHeight($maxHeight) + { + $this->maxHeight = $maxHeight; + + return $this; + } + + /** + * @return int + */ + public function getMaxWidth() + { + return $this->maxWidth; + } + + /** + * @param $maxWidth + * @return GD + */ + public function setMaxWidth($maxWidth) + { + $this->maxWidth = $maxWidth; + + return $this; + } + + /** + * Returns $newDimensions. + * + * @see \PHPThumb\GD::$newDimensions + */ + public function getNewDimensions() + { + return $this->newDimensions; + } + + /** + * Sets $newDimensions. + * + * @param object $newDimensions + * @see \PHPThumb\GD::$newDimensions + */ + public function setNewDimensions($newDimensions) + { + $this->newDimensions = $newDimensions; + + return $this; + } + + /** + * Returns $options. + * + * @see \PHPThumb\GD::$options + */ + public function getOptions() + { + return $this->options; + } + + /** + * Returns $percent. + * + * @see \PHPThumb\GD::$percent + */ + public function getPercent() + { + return $this->percent; + } + + /** + * Sets $percent. + * + * @param object $percent + * @see \PHPThumb\GD::$percent + */ + public function setPercent($percent) + { + $this->percent = $percent; + + return $this; + } + + /** + * Returns $oldImage. + * + * @see \PHPThumb\GD::$oldImage + */ + public function getOldImage() + { + return $this->oldImage; + } + + /** + * Sets $oldImage. + * + * @param object $oldImage + * @see \PHPThumb\GD::$oldImage + */ + public function setOldImage($oldImage) + { + $this->oldImage = $oldImage; + + return $this; + } + + /** + * Returns $workingImage. + * + * @see \PHPThumb\GD::$workingImage + */ + public function getWorkingImage() + { + return $this->workingImage; + } + + /** + * Sets $workingImage. + * + * @param object $workingImage + * @see \PHPThumb\GD::$workingImage + */ + public function setWorkingImage($workingImage) + { + $this->workingImage = $workingImage; + + return $this; + } + + + ################################# + # ----- UTILITY FUNCTIONS ----- # + ################################# + + /** + * Calculates a new width and height for the image based on $this->maxWidth and the provided dimensions + * + * @return array + * @param int $width + * @param int $height + */ + protected function calcWidth($width, $height) + { + $newWidthPercentage = (100 * $this->maxWidth) / $width; + $newHeight = ($height * $newWidthPercentage) / 100; + + return array( + 'newWidth' => intval($this->maxWidth), + 'newHeight' => intval($newHeight) + ); + } + + /** + * Calculates a new width and height for the image based on $this->maxWidth and the provided dimensions + * + * @return array + * @param int $width + * @param int $height + */ + protected function calcHeight($width, $height) + { + $newHeightPercentage = (100 * $this->maxHeight) / $height; + $newWidth = ($width * $newHeightPercentage) / 100; + + return array( + 'newWidth' => ceil($newWidth), + 'newHeight' => ceil($this->maxHeight) + ); + } + + /** + * Calculates a new width and height for the image based on $this->percent and the provided dimensions + * + * @return array + * @param int $width + * @param int $height + */ + protected function calcPercent($width, $height) + { + $newWidth = ($width * $this->percent) / 100; + $newHeight = ($height * $this->percent) / 100; + + return array( + 'newWidth' => ceil($newWidth), + 'newHeight' => ceil($newHeight) + ); + } + + /** + * Calculates the new image dimensions + * + * These calculations are based on both the provided dimensions and $this->maxWidth and $this->maxHeight + * + * @param int $width + * @param int $height + */ + protected function calcImageSize($width, $height) + { + $newSize = array( + 'newWidth' => $width, + 'newHeight' => $height + ); + + if ($this->maxWidth > 0) { + $newSize = $this->calcWidth($width, $height); + + if ($this->maxHeight > 0 && $newSize['newHeight'] > $this->maxHeight) { + $newSize = $this->calcHeight($newSize['newWidth'], $newSize['newHeight']); + } + } + + if ($this->maxHeight > 0) { + $newSize = $this->calcHeight($width, $height); + + if ($this->maxWidth > 0 && $newSize['newWidth'] > $this->maxWidth) { + $newSize = $this->calcWidth($newSize['newWidth'], $newSize['newHeight']); + } + } + + $this->newDimensions = $newSize; + } + + /** + * Calculates new image dimensions, not allowing the width and height to be less than either the max width or height + * + * @param int $width + * @param int $height + */ + protected function calcImageSizeStrict($width, $height) + { + // first, we need to determine what the longest resize dimension is.. + if ($this->maxWidth >= $this->maxHeight) { + // and determine the longest original dimension + if ($width > $height) { + $newDimensions = $this->calcHeight($width, $height); + + if ($newDimensions['newWidth'] < $this->maxWidth) { + $newDimensions = $this->calcWidth($width, $height); + } + } elseif ($height >= $width) { + $newDimensions = $this->calcWidth($width, $height); + + if ($newDimensions['newHeight'] < $this->maxHeight) { + $newDimensions = $this->calcHeight($width, $height); + } + } + } elseif ($this->maxHeight > $this->maxWidth) { + if ($width >= $height) { + $newDimensions = $this->calcWidth($width, $height); + + if ($newDimensions['newHeight'] < $this->maxHeight) { + $newDimensions = $this->calcHeight($width, $height); + } + } elseif ($height > $width) { + $newDimensions = $this->calcHeight($width, $height); + + if ($newDimensions['newWidth'] < $this->maxWidth) { + $newDimensions = $this->calcWidth($width, $height); + } + } + } + + $this->newDimensions = $newDimensions; + } + + /** + * Calculates new dimensions based on $this->percent and the provided dimensions + * + * @param int $width + * @param int $height + */ + protected function calcImageSizePercent($width, $height) + { + if ($this->percent > 0) { + $this->newDimensions = $this->calcPercent($width, $height); + } + } + + /** + * Determines the file format by mime-type + * + * This function will throw exceptions for invalid images / mime-types + * + */ + protected function determineFormat() + { + $formatInfo = getimagesize($this->fileName); + + // non-image files will return false + if ($formatInfo === false) { + if ($this->remoteImage) { + throw new \Exception("Could not determine format of remote image: {$this->fileName}"); + } else { + throw new \Exception("File is not a valid image: {$this->fileName}"); + } + } + + $mimeType = isset($formatInfo['mime']) ? $formatInfo['mime'] : null; + + switch ($mimeType) { + case 'image/gif': + $this->format = 'GIF'; + break; + case 'image/jpeg': + $this->format = 'JPG'; + break; + case 'image/png': + $this->format = 'PNG'; + break; + default: + throw new \Exception("Image format not supported: {$mimeType}"); + } + } + + /** + * Makes sure the correct GD implementation exists for the file type + * + */ + protected function verifyFormatCompatiblity() + { + $isCompatible = true; + $gdInfo = gd_info(); + + switch ($this->format) { + case 'GIF': + $isCompatible = $gdInfo['GIF Create Support']; + break; + case 'JPG': + $isCompatible = (isset($gdInfo['JPG Support']) || isset($gdInfo['JPEG Support'])) ? true : false; + break; + case 'PNG': + $isCompatible = $gdInfo[$this->format . ' Support']; + break; + default: + $isCompatible = false; + } + + if (!$isCompatible) { + // one last check for "JPEG" instead + $isCompatible = $gdInfo['JPEG Support']; + + if (!$isCompatible) { + throw new \Exception("Your GD installation does not support {$this->format} image types"); + } + } + } + + /** + * Preserves the alpha or transparency for PNG and GIF files + * + * Alpha / transparency will not be preserved if the appropriate options are set to false. + * Also, the GIF transparency is pretty skunky (the results aren't awesome), but it works like a + * champ... that's the nature of GIFs tho, so no huge surprise. + * + * This functionality was originally suggested by commenter Aimi (no links / site provided) - Thanks! :) + * + */ + protected function preserveAlpha() + { + if ($this->format == 'PNG' && $this->options['preserveAlpha'] === true) { + imagealphablending($this->workingImage, false); + + $colorTransparent = imagecolorallocatealpha( + $this->workingImage, + $this->options['alphaMaskColor'][0], + $this->options['alphaMaskColor'][1], + $this->options['alphaMaskColor'][2], + 0 + ); + + imagefill($this->workingImage, 0, 0, $colorTransparent); + imagesavealpha($this->workingImage, true); + } + // preserve transparency in GIFs... this is usually pretty rough tho + if ($this->format == 'GIF' && $this->options['preserveTransparency'] === true) { + $colorTransparent = imagecolorallocate( + $this->workingImage, + $this->options['transparencyMaskColor'][0], + $this->options['transparencyMaskColor'][1], + $this->options['transparencyMaskColor'][2] + ); + + imagecolortransparent($this->workingImage, $colorTransparent); + imagetruecolortopalette($this->workingImage, true, 256); + } + } +} diff --git a/rainloop/v/0.0.0/app/libraries/PHPThumb/PHPThumb.php b/rainloop/v/0.0.0/app/libraries/PHPThumb/PHPThumb.php new file mode 100644 index 000000000..7521b9d90 --- /dev/null +++ b/rainloop/v/0.0.0/app/libraries/PHPThumb/PHPThumb.php @@ -0,0 +1,149 @@ + + * Copyright (c) 2009, Ian Selby/Gen X Design + * + * Author(s): Ian Selby + * + * Licensed under the MIT License + * Redistributions of files must retain the above copyright notice. + * + * @author Ian Selby + * @copyright Copyright (c) 2009 Gen X Design + * @link http://phpthumb.gxdlabs.com + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ + +abstract class PHPThumb +{ + /** + * The name of the file we're manipulating + * This must include the path to the file (absolute paths recommended) + * + * @var string + */ + protected $fileName; + + /** + * @var \Symfony\Component\Filesystem\Filesystem + */ + protected $filesystem; + + /** + * What the file format is (mime-type) + * + * @var string + */ + protected $format; + + /** + * Whether or not the image is hosted remotely + * + * @var bool + */ + protected $remoteImage; + + /** + * An array of attached plugins to execute in order. + * @var array + */ + protected $plugins; + + /** + * @param $fileName + * @param array $options + * @param array $plugins + */ + public function __construct($fileName, array $options = array(), array $plugins = array()) + { + $this->filesystem = new \Symfony\Component\Filesystem\Filesystem(); + $this->fileName = $fileName; + $this->remoteImage = false; + + if(!$this->validateRequestedResource($fileName)) { + throw new \InvalidArgumentException("Image file not found: {$fileName}"); + } + + $this->setOptions($options); + + $this->plugins = $plugins; + } + + abstract public function setOptions(array $options = array()); + + /** + * Check the provided filename/url. If it is a url, validate that it is properly + * formatted. If it is a file, check to make sure that it actually exists on + * the filesystem. + * + * @param $filename + * @return bool + */ + protected function validateRequestedResource($filename) + { + if(false !== filter_var($filename, FILTER_VALIDATE_URL)) { + $this->remoteImage = true; + return true; + } + + if($this->filesystem->exists($filename)) { + return true; + } + + return false; + } + + /** + * Returns the filename. + * @return string + */ + public function getFileName() + { + return $this->fileName; + } + + /** + * Sets the filename. + * @param $fileName + * @return PHPThumb + */ + public function setFileName($fileName) + { + $this->fileName = $fileName; + + return $this; + } + + /** + * Returns the format. + * @return string + */ + public function getFormat() + { + return $this->format; + } + + /** + * Sets the format. + * @param $format + * @return PHPThumb + */ + public function setFormat($format) + { + $this->format = $format; + + return $this; + } + + /** + * Returns whether the image exists remotely, i.e. it was loaded via a URL. + * @return bool + */ + public function getIsRemoteImage() + { + return $this->remoteImage; + } +} diff --git a/rainloop/v/0.0.0/app/libraries/PHPThumb/PluginInterface.php b/rainloop/v/0.0.0/app/libraries/PHPThumb/PluginInterface.php new file mode 100644 index 000000000..56df9768a --- /dev/null +++ b/rainloop/v/0.0.0/app/libraries/PHPThumb/PluginInterface.php @@ -0,0 +1,12 @@ + + * Copyright (c) 2009, Ian Selby/Gen X Design + * + * Author(s): Ian Selby + * + * Licensed under the MIT License + * Redistributions of files must retain the above copyright notice. + * + * @author Ian Selby + * @copyright Copyright (c) 2009 Gen X Design + * @link http://phpthumb.gxdlabs.com + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + * @version 3.0 + * @package PhpThumb + * @filesource + */ + +/** + * GD Reflection Lib Plugin + * + * This plugin allows you to create those fun Apple(tm)-style reflections in your images + * + * @package PhpThumb + * @subpackage Plugins + */ +class Reflection implements \PHPThumb\PluginInterface +{ + protected $currentDimensions; + protected $workingImage; + protected $newImage; + protected $options; + + protected $percent; + protected $reflection; + protected $white; + protected $border; + protected $borderColor; + + public function __construct($percent, $reflection, $white, $border, $borderColor) + { + $this->percent = $percent; + $this->reflection = $reflection; + $this->white = $white; + $this->border = $border; + $this->borderColor = $borderColor; + } + + /** + * @param \PHPThumb\PHPThumb $phpthumb + * @return \PHPThumb\PHPThumb + */ + public function execute($phpthumb) + { + $this->currentDimensions = $phpthumb->getCurrentDimensions(); + $this->workingImage = $phpthumb->getWorkingImage(); + $this->newImage = $phpthumb->getOldImage(); + $this->options = $phpthumb->getOptions(); + + $width = $this->currentDimensions['width']; + $height = $this->currentDimensions['height']; + $this->reflectionHeight = intval($height * ($this->reflection / 100)); + $newHeight = $height + $this->reflectionHeight; + $reflectedPart = $height * ($this->percent / 100); + + $this->workingImage = imagecreatetruecolor($width, $newHeight); + + imagealphablending($this->workingImage, true); + + $colorToPaint = imagecolorallocatealpha( + $this->workingImage, + 255, + 255, + 255, + 0 + ); + + imagefilledrectangle( + $this->workingImage, + 0, + 0, + $width, + $newHeight, + $colorToPaint + ); + + imagecopyresampled( + $this->workingImage, + $this->newImage, + 0, + 0, + 0, + $reflectedPart, + $width, + $this->reflectionHeight, + $width, + ($height - $reflectedPart) + ); + + $this->imageFlipVertical(); + + imagecopy( + $this->workingImage, + $this->newImage, + 0, + 0, + 0, + 0, + $width, + $height + ); + + imagealphablending($this->workingImage, true); + + for ($i = 0; $i < $this->reflectionHeight; $i++) { + $colorToPaint = imagecolorallocatealpha( + $this->workingImage, + 255, + 255, + 255, + ($i / $this->reflectionHeight * -1 + 1) * $this->white + ); + + imagefilledrectangle( + $this->workingImage, + 0, + $height + $i, + $width, + $height + $i, + $colorToPaint + ); + } + + if ($this->border == true) { + $rgb = $this->hex2rgb($this->borderColor, false); + $colorToPaint = imagecolorallocate($this->workingImage, $rgb[0], $rgb[1], $rgb[2]); + + //top line + imageline( + $this->workingImage, + 0, + 0, + $width, + 0, + $colorToPaint + ); + + //bottom line + imageline( + $this->workingImage, + 0, + $height, + $width, + $height, + $colorToPaint + ); + + //left line + imageline( + $this->workingImage, + 0, + 0, + 0, + $height, + $colorToPaint + ); + + //right line + imageline( + $this->workingImage, + $width - 1, + 0, + $width - 1, + $height, + $colorToPaint + ); + } + + if ($phpthumb->getFormat() == 'PNG') { + $colorTransparent = imagecolorallocatealpha( + $this->workingImage, + $this->options['alphaMaskColor'][0], + $this->options['alphaMaskColor'][1], + $this->options['alphaMaskColor'][2], + 0 + ); + + imagefill($this->workingImage, 0, 0, $colorTransparent); + imagesavealpha($this->workingImage, true); + } + + $phpthumb->setOldImage($this->workingImage); + $this->currentDimensions['width'] = $width; + $this->currentDimensions['height'] = $newHeight; + $phpthumb->setCurrentDimensions($this->currentDimensions); + + return $phpthumb; + } + + /** + * Flips the image vertically + * + */ + protected function imageFlipVertical () + { + $x_i = imagesx($this->workingImage); + $y_i = imagesy($this->workingImage); + + for ($x = 0; $x < $x_i; $x++) { + for ($y = 0; $y < $y_i; $y++) { + imagecopy( + $this->workingImage, + $this->workingImage, + $x, + $y_i - $y - 1, + $x, + $y, + 1, + 1 + ); + } + } + } + + /** + * Converts a hex color to rgb tuples + * + * @return mixed + * @param string $hex + * @param bool $asString + */ + protected function hex2rgb ($hex, $asString = false) + { + // strip off any leading # + if (0 === strpos($hex, '#')) { + $hex = substr($hex, 1); + } elseif (0 === strpos($hex, '&H')) { + $hex = substr($hex, 2); + } + + // break into hex 3-tuple + $cutpoint = ceil(strlen($hex) / 2)-1; + $rgb = explode(':', wordwrap($hex, $cutpoint, ':', $cutpoint), 3); + + // convert each tuple to decimal + $rgb[0] = (isset($rgb[0]) ? hexdec($rgb[0]) : 0); + $rgb[1] = (isset($rgb[1]) ? hexdec($rgb[1]) : 0); + $rgb[2] = (isset($rgb[2]) ? hexdec($rgb[2]) : 0); + + return ($asString ? "{$rgb[0]} {$rgb[1]} {$rgb[2]}" : $rgb); + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/ExceptionInterface.php b/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/ExceptionInterface.php new file mode 100644 index 000000000..c212e664d --- /dev/null +++ b/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/ExceptionInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception interface for all exceptions thrown by the component. + * + * @author Romain Neutron + * + * @api + */ +interface ExceptionInterface +{ +} diff --git a/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/FileNotFoundException.php b/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/FileNotFoundException.php new file mode 100644 index 000000000..15533db40 --- /dev/null +++ b/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/FileNotFoundException.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception class thrown when a file couldn't be found + * + * @author Fabien Potencier + * @author Christian Gärtner + */ +class FileNotFoundException extends IOException +{ + public function __construct($message = null, $code = 0, \Exception $previous = null, $path = null) + { + if (null === $message) { + if (null === $path) { + $message = 'File could not be found.'; + } else { + $message = sprintf('File "%s" could not be found.', $path); + } + } + + parent::__construct($message, $code, $previous, $path); + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/IOException.php b/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/IOException.php new file mode 100644 index 000000000..4b551af71 --- /dev/null +++ b/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/IOException.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception class thrown when a filesystem operation failure happens + * + * @author Romain Neutron + * @author Christian Gärtner + * @author Fabien Potencier + * + * @api + */ +class IOException extends \RuntimeException implements IOExceptionInterface +{ + private $path; + + public function __construct($message, $code = 0, \Exception $previous = null, $path = null) + { + $this->path = $path; + + parent::__construct($message, $code, $previous); + } + + /** + * {@inheritdoc} + */ + public function getPath() + { + return $this->path; + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/IOExceptionInterface.php b/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/IOExceptionInterface.php new file mode 100644 index 000000000..de9f3e714 --- /dev/null +++ b/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Exception/IOExceptionInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * IOException interface for file and input/output stream releated exceptions thrown by the component. + * + * @author Christian Gärtner + */ +interface IOExceptionInterface extends ExceptionInterface +{ + /** + * Returns the associated path for the exception + * + * @return string The path. + */ + public function getPath(); +} diff --git a/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Filesystem.php b/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Filesystem.php new file mode 100644 index 000000000..9e45d2ba3 --- /dev/null +++ b/rainloop/v/0.0.0/app/libraries/Symfony/Component/Filesystem/Filesystem.php @@ -0,0 +1,474 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem; + +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Exception\FileNotFoundException; + +/** + * Provides basic utility to manipulate the file system. + * + * @author Fabien Potencier + */ +class Filesystem +{ + /** + * Copies a file. + * + * This method only copies the file if the origin file is newer than the target file. + * + * By default, if the target already exists, it is not overridden. + * + * @param string $originFile The original filename + * @param string $targetFile The target filename + * @param boolean $override Whether to override an existing file or not + * + * @throws FileNotFoundException When orginFile doesn't exist + * @throws IOException When copy fails + */ + public function copy($originFile, $targetFile, $override = false) + { + if (stream_is_local($originFile) && !is_file($originFile)) { + throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile); + } + + $this->mkdir(dirname($targetFile)); + + if (!$override && is_file($targetFile)) { + $doCopy = filemtime($originFile) > filemtime($targetFile); + } else { + $doCopy = true; + } + + if ($doCopy) { + // https://bugs.php.net/bug.php?id=64634 + $source = fopen($originFile, 'r'); + $target = fopen($targetFile, 'w+'); + stream_copy_to_stream($source, $target); + fclose($source); + fclose($target); + unset($source, $target); + + if (!is_file($targetFile)) { + throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile); + } + } + } + + /** + * Creates a directory recursively. + * + * @param string|array|\Traversable $dirs The directory path + * @param integer $mode The directory mode + * + * @throws IOException On any directory creation failure + */ + public function mkdir($dirs, $mode = 0777) + { + foreach ($this->toIterator($dirs) as $dir) { + if (is_dir($dir)) { + continue; + } + + if (true !== @mkdir($dir, $mode, true)) { + throw new IOException(sprintf('Failed to create "%s".', $dir), 0, null, $dir); + } + } + } + + /** + * Checks the existence of files or directories. + * + * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to check + * + * @return Boolean true if the file exists, false otherwise + */ + public function exists($files) + { + foreach ($this->toIterator($files) as $file) { + if (!file_exists($file)) { + return false; + } + } + + return true; + } + + /** + * Sets access and modification time of file. + * + * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to create + * @param integer $time The touch time as a unix timestamp + * @param integer $atime The access time as a unix timestamp + * + * @throws IOException When touch fails + */ + public function touch($files, $time = null, $atime = null) + { + foreach ($this->toIterator($files) as $file) { + $touch = $time ? @touch($file, $time, $atime) : @touch($file); + if (true !== $touch) { + throw new IOException(sprintf('Failed to touch "%s".', $file), 0, null, $file); + } + } + } + + /** + * Removes files or directories. + * + * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to remove + * + * @throws IOException When removal fails + */ + public function remove($files) + { + $files = iterator_to_array($this->toIterator($files)); + $files = array_reverse($files); + foreach ($files as $file) { + if (!file_exists($file) && !is_link($file)) { + continue; + } + + if (is_dir($file) && !is_link($file)) { + $this->remove(new \FilesystemIterator($file)); + + if (true !== @rmdir($file)) { + throw new IOException(sprintf('Failed to remove directory "%s".', $file), 0, null, $file); + } + } else { + // https://bugs.php.net/bug.php?id=52176 + if (defined('PHP_WINDOWS_VERSION_MAJOR') && is_dir($file)) { + if (true !== @rmdir($file)) { + throw new IOException(sprintf('Failed to remove file "%s".', $file), 0, null, $file); + } + } else { + if (true !== @unlink($file)) { + throw new IOException(sprintf('Failed to remove file "%s".', $file), 0, null, $file); + } + } + } + } + } + + /** + * Change mode for an array of files or directories. + * + * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change mode + * @param integer $mode The new mode (octal) + * @param integer $umask The mode mask (octal) + * @param Boolean $recursive Whether change the mod recursively or not + * + * @throws IOException When the change fail + */ + public function chmod($files, $mode, $umask = 0000, $recursive = false) + { + foreach ($this->toIterator($files) as $file) { + if ($recursive && is_dir($file) && !is_link($file)) { + $this->chmod(new \FilesystemIterator($file), $mode, $umask, true); + } + if (true !== @chmod($file, $mode & ~$umask)) { + throw new IOException(sprintf('Failed to chmod file "%s".', $file), 0, null, $file); + } + } + } + + /** + * Change the owner of an array of files or directories + * + * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change owner + * @param string $user The new owner user name + * @param Boolean $recursive Whether change the owner recursively or not + * + * @throws IOException When the change fail + */ + public function chown($files, $user, $recursive = false) + { + foreach ($this->toIterator($files) as $file) { + if ($recursive && is_dir($file) && !is_link($file)) { + $this->chown(new \FilesystemIterator($file), $user, true); + } + if (is_link($file) && function_exists('lchown')) { + if (true !== @lchown($file, $user)) { + throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file); + } + } else { + if (true !== @chown($file, $user)) { + throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file); + } + } + } + } + + /** + * Change the group of an array of files or directories + * + * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change group + * @param string $group The group name + * @param Boolean $recursive Whether change the group recursively or not + * + * @throws IOException When the change fail + */ + public function chgrp($files, $group, $recursive = false) + { + foreach ($this->toIterator($files) as $file) { + if ($recursive && is_dir($file) && !is_link($file)) { + $this->chgrp(new \FilesystemIterator($file), $group, true); + } + if (is_link($file) && function_exists('lchgrp')) { + if (true !== @lchgrp($file, $group)) { + throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file); + } + } else { + if (true !== @chgrp($file, $group)) { + throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file); + } + } + } + } + + /** + * Renames a file or a directory. + * + * @param string $origin The origin filename or directory + * @param string $target The new filename or directory + * @param Boolean $overwrite Whether to overwrite the target if it already exists + * + * @throws IOException When target file or directory already exists + * @throws IOException When origin cannot be renamed + */ + public function rename($origin, $target, $overwrite = false) + { + // we check that target does not exist + if (!$overwrite && is_readable($target)) { + throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target); + } + + if (true !== @rename($origin, $target)) { + throw new IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target), 0, null, $target); + } + } + + /** + * Creates a symbolic link or copy a directory. + * + * @param string $originDir The origin directory path + * @param string $targetDir The symbolic link name + * @param Boolean $copyOnWindows Whether to copy files if on Windows + * + * @throws IOException When symlink fails + */ + public function symlink($originDir, $targetDir, $copyOnWindows = false) + { + if (!function_exists('symlink') && $copyOnWindows) { + $this->mirror($originDir, $targetDir); + + return; + } + + $this->mkdir(dirname($targetDir)); + + $ok = false; + if (is_link($targetDir)) { + if (readlink($targetDir) != $originDir) { + $this->remove($targetDir); + } else { + $ok = true; + } + } + + if (!$ok) { + if (true !== @symlink($originDir, $targetDir)) { + $report = error_get_last(); + if (is_array($report)) { + if (defined('PHP_WINDOWS_VERSION_MAJOR') && false !== strpos($report['message'], 'error code(1314)')) { + throw new IOException('Unable to create symlink due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?'); + } + } + + throw new IOException(sprintf('Failed to create symbolic link from "%s" to "%s".', $originDir, $targetDir), 0, null, $targetDir); + } + } + } + + /** + * Given an existing path, convert it to a path relative to a given starting path + * + * @param string $endPath Absolute path of target + * @param string $startPath Absolute path where traversal begins + * + * @return string Path of target relative to starting path + */ + public function makePathRelative($endPath, $startPath) + { + // Normalize separators on windows + if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + $endPath = strtr($endPath, '\\', '/'); + $startPath = strtr($startPath, '\\', '/'); + } + + // Split the paths into arrays + $startPathArr = explode('/', trim($startPath, '/')); + $endPathArr = explode('/', trim($endPath, '/')); + + // Find for which directory the common path stops + $index = 0; + while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) { + $index++; + } + + // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels) + $depth = count($startPathArr) - $index; + + // Repeated "../" for each level need to reach the common path + $traverser = str_repeat('../', $depth); + + $endPathRemainder = implode('/', array_slice($endPathArr, $index)); + + // Construct $endPath from traversing to the common path, then to the remaining $endPath + $relativePath = $traverser.(strlen($endPathRemainder) > 0 ? $endPathRemainder.'/' : ''); + + return (strlen($relativePath) === 0) ? './' : $relativePath; + } + + /** + * Mirrors a directory to another. + * + * @param string $originDir The origin directory + * @param string $targetDir The target directory + * @param \Traversable $iterator A Traversable instance + * @param array $options An array of boolean options + * Valid options are: + * - $options['override'] Whether to override an existing file on copy or not (see copy()) + * - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink()) + * - $options['delete'] Whether to delete files that are not in the source directory (defaults to false) + * + * @throws IOException When file type is unknown + */ + public function mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array()) + { + $targetDir = rtrim($targetDir, '/\\'); + $originDir = rtrim($originDir, '/\\'); + + // Iterate in destination folder to remove obsolete entries + if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) { + $deleteIterator = $iterator; + if (null === $deleteIterator) { + $flags = \FilesystemIterator::SKIP_DOTS; + $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST); + } + foreach ($deleteIterator as $file) { + $origin = str_replace($targetDir, $originDir, $file->getPathname()); + if (!$this->exists($origin)) { + $this->remove($file); + } + } + } + + $copyOnWindows = false; + if (isset($options['copy_on_windows']) && !function_exists('symlink')) { + $copyOnWindows = $options['copy_on_windows']; + } + + if (null === $iterator) { + $flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS; + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST); + } + + foreach ($iterator as $file) { + $target = str_replace($originDir, $targetDir, $file->getPathname()); + + if ($copyOnWindows) { + if (is_link($file) || is_file($file)) { + $this->copy($file, $target, isset($options['override']) ? $options['override'] : false); + } elseif (is_dir($file)) { + $this->mkdir($target); + } else { + throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); + } + } else { + if (is_link($file)) { + $this->symlink($file, $target); + } elseif (is_dir($file)) { + $this->mkdir($target); + } elseif (is_file($file)) { + $this->copy($file, $target, isset($options['override']) ? $options['override'] : false); + } else { + throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); + } + } + } + } + + /** + * Returns whether the file path is an absolute path. + * + * @param string $file A file path + * + * @return Boolean + */ + public function isAbsolutePath($file) + { + if (strspn($file, '/\\', 0, 1) + || (strlen($file) > 3 && ctype_alpha($file[0]) + && substr($file, 1, 1) === ':' + && (strspn($file, '/\\', 2, 1)) + ) + || null !== parse_url($file, PHP_URL_SCHEME) + ) { + return true; + } + + return false; + } + + /** + * Atomically dumps content into a file. + * + * @param string $filename The file to be written to. + * @param string $content The data to write into the file. + * @param integer $mode The file mode (octal). + * @throws IOException If the file cannot be written to. + */ + public function dumpFile($filename, $content, $mode = 0666) + { + $dir = dirname($filename); + + if (!is_dir($dir)) { + $this->mkdir($dir); + } elseif (!is_writable($dir)) { + throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir); + } + + $tmpFile = tempnam($dir, basename($filename)); + + if (false === @file_put_contents($tmpFile, $content)) { + throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename); + } + + $this->rename($tmpFile, $filename, true); + $this->chmod($filename, $mode); + } + + /** + * @param mixed $files + * + * @return \Traversable + */ + private function toIterator($files) + { + if (!$files instanceof \Traversable) { + $files = new \ArrayObject(is_array($files) ? $files : array($files)); + } + + return $files; + } +} diff --git a/rainloop/v/0.0.0/app/src/RainLoop/Actions.php b/rainloop/v/0.0.0/app/src/RainLoop/Actions.php index 325fb3a08..fee0b7b92 100644 --- a/rainloop/v/0.0.0/app/src/RainLoop/Actions.php +++ b/rainloop/v/0.0.0/app/src/RainLoop/Actions.php @@ -2050,8 +2050,24 @@ class Actions */ public function DoFiltersSave() { - sleep(2); + $aIncFilters = $this->GetActionParam('Filters', array()); + + $aFilters = array(); + foreach ($aIncFilters as $aFilter) + { + if ($aFilter) + { + $oFilter = new \RainLoop\Providers\Filters\Classes\Filter(); + if ($oFilter->FromJSON($aFilter)) + { + $this->Logger()->WriteDump($oFilter); + $aFilters[] = $oFilter; + } + } + } + return $this->TrueResponse(__FUNCTION__); + return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->Save($aFilters)); } /** @@ -7001,10 +7017,11 @@ class Actions /** * @param bool $bDownload + * @param bool $bThumbnail = false * * @return bool */ - private function rawSmart($bDownload) + private function rawSmart($bDownload, $bThumbnail = false) { $sRawKey = (string) $this->GetActionParam('RawKey', ''); $aValues = $this->getDecodedRawKeyValue($sRawKey); @@ -7021,11 +7038,11 @@ class Actions $this->verifyCacheByKey($sRawKey); } - $this->initMailClientConnection(); + $oAccount = $this->initMailClientConnection(); $self = $this; return $this->MailClient()->MessageMimeStream( - function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use ($self, $sRawKey, $sContentTypeIn, $sFileNameIn, $bDownload) { + function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use ($self, $oAccount, $sRawKey, $sContentTypeIn, $sFileNameIn, $bDownload, $bThumbnail) { if (\is_resource($rResource)) { $sContentTypeOut = $sContentTypeIn; @@ -7046,16 +7063,41 @@ class Actions $sFileNameOut = $self->MainClearFileName($sFileNameOut, $sContentTypeOut, $sMimeIndex); - \header('Content-Type: '.$sContentTypeOut); - \header('Content-Disposition: '.($bDownload ? 'attachment' : 'inline').'; '. - \trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileNameOut)), true); - - \header('Accept-Ranges: none', true); - \header('Content-Transfer-Encoding: binary'); - $self->cacheByKey($sRawKey); - \MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource); + $bDone = false; + if ($bThumbnail && !$bDownload) + { + \MailSo\Base\StreamWrappers\TempFile::Reg(); + + $sFileName = 'mailsotempfile://'.\md5($sFileNameOut.\rand(1000, 9999)); + + $rTempResource = \fopen($sFileName, 'r+b'); + if (@\is_resource($rTempResource)) + { + \MailSo\Base\Utils::MultipleStreamWriter($rResource, array($rTempResource)); + @\fclose($rTempResource); + + $oThumb =@ new \PHPThumb\GD($sFileName); + if ($oThumb) + { + $oThumb->adaptiveResize(220, 80)->show(); + $bDone = true; + } + } + } + + if (!$bDone) + { + \header('Content-Type: '.$sContentTypeOut); + \header('Content-Disposition: '.($bDownload ? 'attachment' : 'inline').'; '. + \trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileNameOut)), true); + + \header('Accept-Ranges: none', true); + \header('Content-Transfer-Encoding: binary'); + + \MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource); + } } }, $sFolder, $iUid, true, $sMimeIndex); } @@ -7076,6 +7118,14 @@ class Actions return $this->rawSmart(false); } + /** + * @return bool + */ + public function RawViewThumbnail() + { + return $this->rawSmart(false, true); + } + /** * @return bool */ @@ -7126,6 +7176,42 @@ class Actions return \in_array($sExt, array('doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx')); } + /** + * @param string $sFileName + * + * @return bool + */ + public function isFileHasThumbnail($sFileName) + { + static $aCache = array(); + + $sExt = \MailSo\Base\Utils::GetFileExtension($sFileName); + if (isset($aCache[$sExt])) + { + return $aCache[$sExt]; + } + + $bResult = \function_exists('gd_info'); + if ($bResult) + { + $bResult = false; + switch ($sExt) + { + case 'png': + $bResult = \function_exists('imagecreatefrompng'); + break; + case 'jpg': + case 'jpeg': + $bResult = \function_exists('imagecreatefromjpeg'); + break; + } + } + + $aCache[$sExt] = $bResult; + + return $bResult; + } + /** * @return bool */ @@ -8185,12 +8271,18 @@ class Actions 'CID' => $mResponse->Cid(), 'ContentLocation' => $mResponse->ContentLocation(), 'IsInline' => $mResponse->IsInline(), + 'IsThumbnail' => !!$this->Config()->Get('interface', 'show_attachment_thumbnail', true), 'IsLinked' => ($mFoundedCIDs && \in_array(\trim(\trim($mResponse->Cid()), '<>'), $mFoundedCIDs)) || ($mFoundedContentLocationUrls && \in_array(\trim($mResponse->ContentLocation()), $mFoundedContentLocationUrls)) )); $mResult['Framed'] = $this->isFileHasFramedPreview($mResult['FileName']); + if ($mResult['IsThumbnail']) + { + $mResult['IsThumbnail'] = $this->isFileHasThumbnail($mResult['FileName']); + } + $mResult['Download'] = \RainLoop\Utils::EncodeKeyValues(array( 'V' => APP_VERSION, 'Account' => $oAccount ? \md5($oAccount->Hash()) : '', @@ -8201,6 +8293,8 @@ class Actions 'FileName' => $mResult['FileName'], 'Framed' => $mResult['Framed'] )); + + $this->Logger()->WriteDump($mResult); } else if ('MailSo\Mail\Folder' === $sClassName) { diff --git a/rainloop/v/0.0.0/app/src/RainLoop/Config/Application.php b/rainloop/v/0.0.0/app/src/RainLoop/Config/Application.php index a2d6d92ea..4f629d0e0 100644 --- a/rainloop/v/0.0.0/app/src/RainLoop/Config/Application.php +++ b/rainloop/v/0.0.0/app/src/RainLoop/Config/Application.php @@ -80,6 +80,10 @@ class Application extends \RainLoop\Config\AbstractConfig 0 for unlimited.') ), + 'interface' => array( + 'show_attachment_thumbnail' => array(true, '') + ), + 'branding' => array( 'login_logo' => array(''), 'login_desc' => array(''), diff --git a/rainloop/v/0.0.0/app/src/RainLoop/Providers/Filters/Classes/Filter.php b/rainloop/v/0.0.0/app/src/RainLoop/Providers/Filters/Classes/Filter.php index d2522681d..98700bdf1 100644 --- a/rainloop/v/0.0.0/app/src/RainLoop/Providers/Filters/Classes/Filter.php +++ b/rainloop/v/0.0.0/app/src/RainLoop/Providers/Filters/Classes/Filter.php @@ -4,6 +4,11 @@ namespace RainLoop\Providers\Filters\Classes; class Filter { + /** + * @var string + */ + private $sID; + /** * @var string */ @@ -41,6 +46,7 @@ class Filter public function Clear() { + $this->sID = ''; $this->sName = ''; $this->aConditions = array(); @@ -54,6 +60,14 @@ class Filter $this->bSkipOthers = false; } + /** + * @return string + */ + public function ID() + { + return $this->sID; + } + /** * @return string */ @@ -129,6 +143,16 @@ class Filter } } + /** + * @param array $aFilter + * + * @return array + */ + public function FromJSON($aFilter) + { +// + } + /** * @param bool $bAjax = false * @@ -138,6 +162,7 @@ class Filter { $aConditions = $this->Conditions(); return array( + 'ID' => $this->ID(), 'Name' => $this->Name(), 'Conditions' => $aConditions, 'FilterRulesType' => $this->FilterRulesType(), diff --git a/rainloop/v/0.0.0/app/templates/Views/User/MailMessageView.html b/rainloop/v/0.0.0/app/templates/Views/User/MailMessageView.html index 221ee8a56..b9db7c646 100644 --- a/rainloop/v/0.0.0/app/templates/Views/User/MailMessageView.html +++ b/rainloop/v/0.0.0/app/templates/Views/User/MailMessageView.html @@ -15,17 +15,6 @@ -
 
 
- -