      1: <?php
      2:
      3: /**
      4:  * A widget that can generate an image which is scaled to the screen resolution of the
      5:  * user, and can be made secured to prevent hot-linking from remote sites.  Note that by
      6:  * default, a jpg file will be returned (the source file can be jpg, png, or gif).
      7:  *
      8:  * @package alpha::view::widgets
      9:  * @since 1.0
     10:  * @author John Collins <dev@alphaframework.org>
     11:  * @version $Id: Image.inc 1568 2012-08-05 19:59:16Z alphadevx $
     12:  * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
     13:  * @copyright Copyright (c) 2012, John Collins (founder of Alpha Framework).
     14:  * All rights reserved.
     15:  *
     16:  * <pre>
     17:  * Redistribution and use in source and binary forms, with or
     18:  * without modification, are permitted provided that the
     19:  * following conditions are met:
     20:  *
     21:  * * Redistributions of source code must retain the above
     22:  *   copyright notice, this list of conditions and the
     23:  *   following disclaimer.
     24:  * * Redistributions in binary form must reproduce the above
     25:  *   copyright notice, this list of conditions and the
     26:  *   following disclaimer in the documentation and/or other
     27:  *   materials provided with the distribution.
     28:  * * Neither the name of the Alpha Framework nor the names
     29:  *   of its contributors may be used to endorse or promote
     30:  *   products derived from this software without specific
     31:  *   prior written permission.
     32:  *
     33:  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
     34:  * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
     35:  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
     36:  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     37:  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
     38:  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     39:  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
     40:  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     41:  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     42:  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
     43:  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
     44:  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     45:  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     46:  * </pre>
     47:  *
     48:  */
     49: class Image {
     50:     /**
     51:      * The title of the image for alt text (optional).
     52:      *
     53:      * @var string
     54:      * @since 1.0
     55:      */
     56:     private $title;
     57:
     58:     /**
     59:      * The absolute path to the source image.
     60:      *
     61:      * @var string
     62:      * @since 1.0
     63:      */
     64:     private $source;
     65:
     66:     /**
     67:      * The width of the image (can differ from the source file when scale=true).
     68:      *
     69:      * @var Integer
     70:      * @since 1.0
     71:      */
     72:     private $width;
     73:
     74:     /**
     75:      * The height of the image (can differ from the source file when scale=true).
     76:      *
     77:      * @var Integer
     78:      * @since 1.0
     79:      */
     80:     private $height;
     81:
     82:     /**
     83:      * The file type of the source image (gif, jpg, or png supported)
     84:      *
     85:      * @var Enum
     86:      * @since 1.0
     87:      */
     88:     private $sourceType;
     89:
     90:     /**
     91:      * The quality of the jpg image generated (0.00 to 1.00, 0.75 by default).
     92:      *
     93:      * @var Double
     94:      * @since 1.0
     95:      */
     96:     private $quality;
     97:
     98:     /**
     99:      * Flag to determine if the image will scale to match the target resolution (false
    100:      * by default).
    101:      *
    102:      * @var Boolean
    103:      * @since 1.0
    104:      */
    105:     private $scale;
    106:
    107:     /**
    108:      * Flag to determine if the link to the image will change every 24hrs, making hot-linking
    109:      * to the image difficult (false by default).
    110:      *
    111:      * @var Boolean
    112:      * @since 1.0
    113:      */
    114:     private $secure;
    115:
    116:     /**
    117:      * The auto-generated name of the cache file for the image.
    118:      *
    119:      * @var string
    120:      * @since 1.0
    121:      */
    122:     private $filename;
    123:
    124:     /**
    125:      * Trace logger
    126:      *
    127:      * @var Logger
    128:      * @since 1.0
    129:      */
    130:     private static $logger = null;
    131:
    132:     /**
    133:      * The constructor.
    134:      *
    135:      * @param $source
    136:      * @param $width
    137:      * @param $height
    138:      * @param $sourceType
    139:      * @param $quality
    140:      * @param $scale
    141:      * @throws IllegalArguementException
    142:      * @since 1.0
    143:      */
    144:     public function __construct($source, $width, $height, $sourceType, $quality=0.75, $scale=false, $secure=false) {
    145:         self::$logger = new Logger('Image');
    146:         self::$logger->debug('>>__construct(source=['.$source.'], width=['.$width.'], height=['.$height.'], sourceType=['.$sourceType.'], quality=['.$quality.'], scale=['.$scale.'], secure=['.$secure.'])');
    147:
    148:         global $config;
    149:
    150:         if(file_exists($source))
    151:             $this->source = $source;
    152:         else
    153:             throw new IllegalArguementException('The source file for the Image widget ['.$source.'] cannot be found!');
    154:
    155:         $this->sourceType = new Enum();
    156:         $this->sourceType->setOptions(array('jpg','png','gif'));
    157:         $this->sourceType->setValue($sourceType);
    158:
    159:         if($quality < 0.0 || $quality > 1.0)
    160:             throw new IllegalArguementException('The quality setting of ['.$quality.'] is outside of the allowable range of 0.0 to 1.0');
    161:
    162:         $this->quality = new Double($quality);
    163:         $this->scale = new Boolean($scale);
    164:         $this->secure = new Boolean($secure);
    165:
    166:         $this->width = new Integer($width);
    167:         $this->height = new Integer($height);
    168:
    169:         $this->setFilename();
    170:
    171:         self::$logger->debug('<<__construct');
    172:     }
    173:
    174:     /**
    175:      * Renders the HTML <img> tag to the ViewImage controller, with all of the correct params to render the source
    176:      * image in the desired resolution.
    177:      *
    178:      * @param $altText Set this value to render alternate text as part of the HTML link (defaults to no alternate text)
    179:      * @return string
    180:      * @since 1.0
    181:      */
    182:     public function renderHTMLLink($altText='') {
    183:         global $config;
    184:
    185:         if($this->secure->getBooleanValue()) {
    186:             $params = AlphaController::generateSecurityFields();
    187:             return '<img src="'.FrontController::generateSecureURL('act=ViewImage&s='.$this->source.'&w='.$this->width->getValue().'&h='.$this->height->getValue().'&t='.$this->sourceType->getValue().'&q='.$this->quality->getValue().'&sc='.$this->scale->getValue().'&se='.$this->secure->getValue().'&var1='.$params[0].'&var2='.$params[1]).'"'.(empty($altText) ? '' : ' alt="'.$altText.'"').'/>';
    188:         }else{
    189:             return '<img src="'.FrontController::generateSecureURL('act=ViewImage&s='.$this->source.'&w='.$this->width->getValue().'&h='.$this->height->getValue().'&t='.$this->sourceType->getValue().'&q='.$this->quality->getValue().'&sc='.$this->scale->getValue().'&se='.$this->secure->getValue()).'"'.(empty($altText) ? '' : ' alt="'.$altText.'"').'/>';
    190:         }
    191:     }
    192:
    193:     /**
    194:      * Setter for the filename, which also creates a sub-directory under /cache for images when required
    195:      *
    196:      * @since 1.0
    197:      * @throws AlphaException
    198:      */
    199:     private function setFilename() {
    200:         global $config;
    201:
    202:         if(!strpos($this->source, 'attachments/article_')) {
    203:             // checking to see if we will write a jpg or png to the cache
    204:             if($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png'))
    205:                 $this->filename = $config->get('app.file.store.dir').'cache/images/'.basename($this->source, ".".$this->sourceType->getValue()).'_'.$this->width->getValue().'x'.$this->height->getValue().'.png';
    206:             else
    207:                 $this->filename = $config->get('app.file.store.dir').'cache/images/'.basename($this->source, ".".$this->sourceType->getValue()).'_'.$this->width->getValue().'x'.$this->height->getValue().'.jpg';
    208:         }else{
    209:             // make a cache dir for the article
    210:             $cacheDir = $config->get('app.file.store.dir').'cache/images/article_'.substr($this->source, strpos($this->source, 'attachments/article_')+20, 11);
    211:             if(!file_exists($cacheDir)) {
    212:                 $success = mkdir($cacheDir);
    213:
    214:                 if (!$success) {
    215:                     throw new AlphaException('Unable to create the folder '.$cacheDir.' for the cache image, source file is '.$this->source);
    216:                 }
    217:
    218:                 // ...and set write permissions on the folder
    219:                 $success = chmod($cacheDir, 0777);
    220:
    221:                 if (!$success) {
    222:                     throw new AlphaException('Unable to set write permissions on the folder ['.$cacheDir.'].');
    223:                 }
    224:             }
    225:
    226:             // now set the filename to include the new cache directory
    227:             if($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png'))
    228:                 $this->filename = $cacheDir.'/'.basename($this->source, ".".$this->sourceType->getValue()).'_'.$this->width->getValue().'x'.$this->height->getValue().'.png';
    229:             else
    230:                 $this->filename = $cacheDir.'/'.basename($this->source, ".".$this->sourceType->getValue()).'_'.$this->width->getValue().'x'.$this->height->getValue().'.jpg';
    231:         }
    232:
    233:         self::$logger->debug('Image filename is ['.$this->filename.']');
    234:     }
    235:
    236:     /**
    237:      * Gets the auto-generated filename for the image in the /cache directory
    238:      *
    239:      * @since 1.0
    240:      */
    241:     public function getFilename() {
    242:         return $this->filename;
    243:     }
    244:
    245:     /**
    246:      * Renders the actual binary image using GD library calls.
    247:      *
    248:      *  @since 1.0
    249:      */
    250:     public function renderImage() {
    251:         global $config;
    252:
    253:         /*
    254:          * Handle secure image validation (if enabled in config).
    255:          * Not required for the view_article_pdf.php controller.
    256:          */
    257:         if($config->get('cms.images.widget.secure') && basename($_SERVER["PHP_SELF"]) != 'ViewArticlePDF.php') {
    258:             $valid = AlphaController::checkSecurityFields();
    259:
    260:             // if not valid, just return a blank black image of the same dimensions
    261:             if(!$valid) {
    262:                 $im  = imagecreatetruecolor($this->width->getValue(), $this->height->getValue()); /* Create a blank image */
    263:                 $bgc = imagecolorallocate($im, 0, 0, 0);
    264:                 imagefilledrectangle($im, 0, 0, $this->width->getValue(), $this->height->getValue(), $bgc);
    265:
    266:                 if($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png')) {
    267:                     header("Content-Type: image/png");
    268:                     imagepng($im);
    269:                 }else{
    270:                     header("Content-Type: image/jpeg");
    271:                     imagejpeg($im);
    272:                 }
    273:
    274:                 imagedestroy($im);
    275:
    276:                 self::$logger->warn('The client ['.$_SERVER['HTTP_USER_AGENT'].'] was blocked from accessing the file ['.$this->filename.'] due to bad security tokens being provided');
    277:             }
    278:         }
    279:
    280:         // if scaled, we need to compute the target image size
    281:         if($this->scale->getBooleanValue() && isset($_COOKIE['screenSize'])) {
    282:             $originalScreenResolution = explode('x', $config->get('sysCMSImagesWidgetScreenResolution'));
    283:             $originalScreenX = $originalScreenResolution[0];
    284:             $originalScreenY = $originalScreenResolution[1];
    285:
    286:             $targetScreenResolution = explode('x', $_COOKIE['screenSize']);
    287:             $targetScreenX = $targetScreenResolution[0];
    288:             $targetScreenY = $targetScreenResolution[1];
    289:
    290:             // calculate the new units we will scale by
    291:             $xu = $targetScreenX/$originalScreenX;
    292:             $yu = $targetScreenY/$originalScreenY;
    293:
    294:             $this->width = new Integer(intval($this->width->getValue()*$xu));
    295:             $this->height = new Integer(intval($this->height->getValue()*$yu));
    296:
    297:             // need to update the cache filename as the dimensions have changed
    298:             $this->setFilename();
    299:         }
    300:
    301:         // check the image cache first before we proceed
    302:         if ($this->checkCache()) {
    303:             $this->loadCache();
    304:         }else{
    305:             // now get the old image
    306:             switch ($this->sourceType->getValue()) {
    307:                 case "gif":
    308:                     $old_image = imagecreatefromgif($this->source);
    309:                 break;
    310:                 case "jpg":
    311:                     $old_image = imagecreatefromjpeg($this->source);
    312:                 break;
    313:                 case "png":
    314:                     $old_image = imagecreatefrompng($this->source);
    315:                 break;
    316:             }
    317:
    318:             if (!$old_image) { /* See if it failed */
    319:                 $im  = imagecreatetruecolor($this->width->getValue(), $this->height->getValue()); /* Create a blank image */
    320:                 $bgc = imagecolorallocate($im, 255, 255, 255);
    321:                 $tc  = imagecolorallocate($im, 0, 0, 0);
    322:                 imagefilledrectangle($im, 0, 0, $this->width->getValue(), $this->height->getValue(), $bgc);
    323:
    324:                 imagestring($im, 1, 5, 5, "Error loading $this->source", $tc);
    325:                 if($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png')) {
    326:                     header("Content-Type: image/png");
    327:                     imagepng($im);
    328:                 }else{
    329:                     header("Content-Type: image/jpeg");
    330:                     imagejpeg($im);
    331:                 }
    332:                 imagedestroy($im);
    333:             }else{
    334:                 // the dimensions of the source image
    335:                 $oldWidth = imagesx($old_image);
    336:                 $oldHeight = imagesy($old_image);
    337:
    338:                 // now create the new image
    339:                 $new_image = imagecreatetruecolor($this->width->getValue(), $this->height->getValue());
    340:
    341:                 // set a transparent background for PNGs
    342:                 if($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png')) {
    343:                     // Turn off transparency blending (temporarily)
    344:                     imagealphablending($new_image, false);
    345:
    346:                     // Create a new transparent color for image
    347:                     $color = imagecolorallocatealpha($new_image, 255, 0, 0, 0);
    348:
    349:                     // Completely fill the background of the new image with allocated color.
    350:                     imagefill($new_image, 0, 0, $color);
    351:
    352:                     // Restore transparency blending
    353:                     imagesavealpha($new_image, true);
    354:                 }
    355:                 // copy the old image to the new image (in memory, not the file!)
    356:                 imagecopyresampled($new_image, $old_image, 0, 0, 0, 0, $this->width->getValue(), $this->height->getValue(), $oldWidth, $oldHeight);
    357:
    358:                 if($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png')) {
    359:                     header("Content-Type: image/png");
    360:                     imagepng($new_image, basename($this->filename));
    361:                 }else{
    362:                     header("Content-Type: image/jpeg");
    363:                     imagejpeg($new_image, basename($this->filename), 100*$this->quality->getValue());
    364:                 }
    365:
    366:                 $this->cache($new_image);
    367:                 imagedestroy($old_image);
    368:                 imagedestroy($new_image);
    369:             }
    370:         }
    371:     }
    372:
    373:     /**
    374:      * Caches the image to the cache directory
    375:      *
    376:      * @param image $image the binary GD image stream to save
    377:      * @since 1.0
    378:      */
    379:     private function cache($image) {
    380:         global $config;
    381:
    382:         if($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png'))
    383:             imagepng($image, $this->filename);
    384:         else
    385:             imagejpeg($image, $this->filename, 100*$this->quality->getValue());
    386:     }
    387:
    388:     /**
    389:      * Used to check the image cache for the image jpeg cache file.
    390:      *
    391:      * @return bool
    392:      * @since 1.0
    393:      */
    394:     private function checkCache() {
    395:         return file_exists($this->filename);
    396:     }
    397:
    398:     /**
    399:      * Method to load the content of the image cache file to the standard output stream (the browser)
    400:      *
    401:      * @since 1.0
    402:      */
    403:     private function loadCache() {
    404:         readfile($this->filename);
    405:     }
    406:
    407:     /**
    408:      * Converts a URL for an image to a relative file system path for the image, assuming it is
    409:      * hosted on the same server as the application.
    410:      *
    411:      * @param string $imgURL
    412:      * @return string the path of the image
    413:      * @since 1.0
    414:      */
    415:     public static function convertImageURLToPath($imgURL) {
    416:         global $config;
    417:
    418:         $imgPath = str_replace($config->get('app.url'), '', $imgURL);
    419:
    420:         return $imgPath;
    421:     }
    422: }
    423:
    424: ?>