├── CloudfrontImageService.php ├── README.txt ├── classes ├── Image.php ├── ImageDimensions.php └── ImageDimensionsMap.php ├── cloudfrontimageservice.sql ├── config.samp.php └── examples ├── cloudfrontImageTest.php ├── configureImageObjects.php └── flower.jpg /CloudfrontImageService.php: -------------------------------------------------------------------------------- 1 | bucket = CDN_BUCKET; 51 | $this->cdnDomain = CDN_DOMAIN_NAME; 52 | $this->imagesRoot = IMAGES_ROOT; 53 | $this->s3 = new S3Service(); 54 | $this->s3->setAuth(AWS_ACCESS_KEY, AWS_SECRET_KEY); 55 | } 56 | 57 | 58 | /** 59 | * Returns the URL to the image requested 60 | * 61 | * Example Usage: echo($imgSvc->getUrlFromFilePath('wwwimages/logo.jpg', 'originial')); 62 | * 63 | * @param string $filePath The relative path to the image on the filesystem. 64 | * @param string $dimensionsKeyname A string that identifies the dimensions of the resulting image (must be in tbl_imageDimensions.keyname) 65 | * @return string A URL. 66 | */ 67 | public function getUrlFromFilePath($filePath, $dimensionsKeyname) 68 | { 69 | $img = Image::findByFilePath($filePath); 70 | if (!isset($img)) 71 | { 72 | throw new Exception("Could not find Image object by path $filePath"); 73 | } 74 | return $this->getURL($img, $dimensionsKeyname); 75 | } 76 | 77 | 78 | /** 79 | * Returns the URL to the image requested 80 | * 81 | * @param ing $imageId An image id from tbl_images 82 | * @param string $dimensionsKeyname A string that identifies the dimensions of the resulting image (must be in tbl_imageDimensions.keyname) 83 | * @return string A URL. 84 | */ 85 | public function getUrlFromImageId($imageId, $dimensionsKeyname) 86 | { 87 | $img = Image::findById($imageId); 88 | if (!isset($img)) 89 | { 90 | throw new Exception("Could not find Image object by id $imageId"); 91 | } 92 | return $this->getURL($img, $dimensionsKeyname); 93 | } 94 | 95 | 96 | /** 97 | * Returns the URL to the image requested 98 | * 99 | * @param $image and Image object 100 | * @param $dimensionsKeyname A string that identifies the dimensions of the resulting image (must be in tbl_imageDimensions.keyname) 101 | * @return string A URL. 102 | */ 103 | public function getURL(Image $image, $dimensionsKeyname) 104 | { 105 | $idim = $this->getDimensionByKeyname($dimensionsKeyname); 106 | $imageDimensionMap = ImageDimensionsMap::findByImageIdAndDimensionsId($image->Id, $idim->Id); 107 | if (!isset($imageDimensionMap)) 108 | { 109 | $imageDimensionMap = $this->createAndUploadImageDimensionMap($image, $dimensionsKeyname); 110 | } 111 | if ($imageDimensionMap->Version < $image->Version) 112 | { 113 | 114 | $imageDimensionMap = $this->createAndUploadImageDimensionMap($image, $dimensionsKeyname); 115 | } 116 | $pth = $this->getPath($image, $imageDimensionMap, $idim); 117 | return "http://{$this->cdnDomain}/{$pth}"; 118 | } 119 | 120 | 121 | 122 | /** 123 | * This is a utility function for adding images to your system. This can be used in 124 | * a console application to pick up an image located at $fsPath and copy it to 125 | * your disk-based data store and create a record in the Images table. 126 | * 127 | * @param string $fsPath The full path to the image you want to add to the system 128 | * @param string $filePath The relative path and file name that you want to use for the resulting Image 129 | * @return string An Image object. 130 | */ 131 | public function createImageObjectFromFileSystemPath($fsPath, $filePath) 132 | { 133 | if (!file_exists($fsPath)) 134 | { 135 | throw new Exception("File $fsPath does not exist"); 136 | } 137 | $magick_wand=NewMagickWand(); 138 | MagickReadImage($magick_wand,$fsPath); 139 | 140 | //check the image type 141 | $mimeType = MagickGetImageMimeType($magick_wand); 142 | $typeStr = ''; 143 | switch($mimeType) 144 | { 145 | case 'image/png': 146 | $typeStr = 'PNG'; 147 | break; 148 | case 'image/jpg': 149 | case 'image/jpeg': 150 | $typeStr = 'JPG'; 151 | break; 152 | case 'image/gif': 153 | $typeStr = 'GIF'; 154 | break; 155 | default: 156 | throw new Exception("Unknown image mime-type $mimeType"); 157 | break; 158 | } 159 | 160 | //Ensure that $filePath is not the path of another Image already in the system. If 161 | //it is a duplicate, append an interger to the file name to make it unique. 162 | $unique = false; //initialze the unique flag to false 163 | $parts = pathinfo($filePath); //get the path parts 164 | $baseName = "{$parts['dirname']}/{$parts['filename']}"; //the full path minus file extension 165 | $ext = $parts['extension']; //the extention 166 | $ct = 0; //the integer we will append to the filename, if necessary 167 | $testFile = "{$baseName}.{$ext}"; 168 | while(!$unique) 169 | { 170 | $existImg = Image::findByFilePath($testFile); //check the database to see if this path already exists 171 | if (!isset($existImg)) 172 | { 173 | $unique = true; //it doesn't exist. $unique==true will break the while 174 | } 175 | else 176 | { 177 | $ct++; //it does exist - increment the counter, change the $testFile, then loop to test the file name 178 | $testFile = "{$baseName}{$ct}.{$ext}"; 179 | } 180 | } 181 | $finalFilePath = $testFile; 182 | $this->ensureImagePath($finalFilePath); 183 | $fullImagePath = $this->imagesRoot . $finalFilePath; 184 | copy($fsPath, $fullImagePath); //copy the file to it's official location 185 | $img = new Image(); //create new Image Object 186 | $img->FilePath = $finalFilePath; 187 | $img->ImageType = $typeStr; 188 | $img->Version = 1; 189 | $img->save(); //commit to database 190 | return $img; 191 | } 192 | 193 | //Recursively makes sure the directories that make up $filePath exist 194 | private function ensureImagePath($filePath) //$filePath is the path relative to the base image path 195 | { 196 | $fullPath = dirname(IMAGES_ROOT . $filePath); 197 | if (!file_exists($fullPath)) //only enter the crawl if the final result doesn't exist 198 | { 199 | $parts = explode('/', $fullPath); 200 | $subPath = '/'; 201 | for($i=1;$igetDimensionByKeyname($dimensionsKeyname); 219 | $srcPth = $this->imagesRoot . $image->FilePath; 220 | if(file_exists($srcPth)) 221 | { 222 | $tempPath = tempnam(sys_get_temp_dir(), 'img_'); 223 | if ($dimensionsKeyname == 'original') //dimenisons "original" means the original size is requsted 224 | { 225 | $s3Path = $srcPth; 226 | $pts = getimagesize($s3Path); 227 | $width = $pts[0]; 228 | $height = $pts[1]; 229 | $newDims = array( 230 | 'x'=>$width, 231 | 'y'=>$height 232 | ); 233 | $s3Path = $srcPth; 234 | } 235 | else 236 | { 237 | //need to redimension this image, using MagickWand 238 | $magick_wand=NewMagickWand(); 239 | MagickReadImage($magick_wand,$srcPth); 240 | $width = MagickGetImageWidth($magick_wand); 241 | $height = MagickGetImageHeight($magick_wand); 242 | $newDims = $this->getNewDimensionsPreservingAspectRatio($width, $height, $imd->Width, $imd->Height); 243 | MagickScaleImage($magick_wand, $newDims['x'], $newDims['y']); 244 | MagickWriteImage($magick_wand, $tempPath); 245 | $s3Path = $tempPath; 246 | } 247 | $headers = array(); //This holds the http headers for our S3 object. 248 | switch($image->ImageType) 249 | { 250 | case 'GIF': 251 | $headers['Content-Type'] = 'image/gif'; 252 | break; 253 | case 'JPG': 254 | $headers['Content-Type'] = 'image/jpeg'; 255 | break; 256 | case 'PNG': 257 | $headers['Content-Type'] = 'image/png'; 258 | break; 259 | default: 260 | throw new Exception("Unrecognized type {$image->getType()}"); 261 | break; 262 | } 263 | 264 | //A "Far Future" expiration - maximizing the chance that the user's web browser will use 265 | //a cached version rather than requesting the file from Cloudfront. 266 | //Also set to public (as recommended by Google Speed Tracer), so that caching will work when 267 | //using SSL as well. Even though Cloudfont doesn't support ssl today, someday it will 268 | //and we will be prepared! 269 | $headers['Cache-Control'] = 'public, max-age=315360000'; //10 years 270 | 271 | $imDimMap = new ImageDimensionsMap(); 272 | $imDimMap->ImageId = $image->Id; 273 | $imDimMap->ImageDimensionsId = $imd->Id; 274 | $imDimMap->Width = $newDims['x']; 275 | $imDimMap->Height = $newDims['y']; 276 | $imDimMap->Version = $image->Version; 277 | $imDimMap->save(); 278 | 279 | 280 | //upload the new file to S3 281 | try 282 | { 283 | $acl = S3Service::ACL_PUBLIC_READ; 284 | if (!file_exists($tempPath)) 285 | { 286 | throw new Exception("$tempPath dosn't exist"); 287 | } 288 | 289 | $res = $this->s3->putObject(S3Service::inputFile($s3Path, true), $this->bucket, $this->getPath($image, $imDimMap, $imd),$acl, array(), $headers); 290 | 291 | if ($res) 292 | { 293 | unlink($tempPath); 294 | } 295 | else 296 | { 297 | //something's wrong. Fail silently and just leave the old version if it exists. 298 | //Don't throw an exception or raise a failure, because there's a user on the other side 299 | //of this request waiting to see this image. 300 | $imDimMap->Version = $imDimMap->Version-1; 301 | $imDimMap->save(); 302 | } 303 | } 304 | catch (Exception $e) 305 | { 306 | //something's wrong. Fail silently and just leave the old version if it exist. 307 | //Don't throw an exception or raise a failure, because there's a user on the other side 308 | //of this request waiting to see this image. 309 | $imDimMap->Version = $imDimMap->Version-1; 310 | $imDimMap->save(); 311 | } 312 | } 313 | return $imDimMap; 314 | } 315 | 316 | 317 | //Get the new dimensions that will fit in the ImageDimensions "box" preserving aspect ratio. 318 | private function getNewDimensionsPreservingAspectRatio($oldX, $oldY, $targetX, $targetY) 319 | { 320 | $oldAspectRatio = $oldX/$oldY; 321 | $targAspectRatio = $targetX/$targetY; 322 | $res = array(); 323 | if ($targAspectRatio < $oldAspectRatio) 324 | { 325 | //bound to width(X) 326 | $res['x'] = $targetX; 327 | $res['y'] = ceil($targetX*$oldY/$oldX); 328 | 329 | } 330 | else 331 | { 332 | //bound to height(Y) 333 | $res['y'] = $targetY; 334 | $res['x'] = ceil($targetY*$oldX/$oldY); 335 | } 336 | return $res; 337 | } 338 | 339 | //Takes all three objects and returns the actual path which look something like this: 340 | // previews/0000/0079/00000838/1_1_book_thumb_130x168.png 341 | private function getPath(Image $image, ImageDimensionsMap $imgDimMap, ImageDimensions $imageDimensions) 342 | { 343 | $parts = pathinfo($image->FilePath); 344 | return "{$parts['dirname']}/{$parts['filename']}{$this->getVersionDimsSlug($imgDimMap, $imageDimensions)}.{$parts['extension']}"; 345 | } 346 | 347 | //Returns the part of the url made up of the version, dimension key and the actual width and 348 | //height of hte image. Something like this: 349 | // _1_book_thumb_130x168 350 | private function getVersionDimsSlug(ImageDimensionsMap $imageDimensionMap, ImageDimensions $imageDimensions) 351 | { 352 | return "_{$imageDimensionMap->Version}_{$imageDimensions->Keyname}_{$imageDimensionMap->Width}x{$imageDimensionMap->Height}"; 353 | } 354 | 355 | //get the ImageDimension object by it's keyname 356 | private function getDimensionByKeyname($dimensionsKeyname) 357 | { 358 | $idim = ImageDimensions::findByKeyname($dimensionsKeyname); 359 | if (!isset($idim)) 360 | { 361 | throw new Exception("Could not find ImageDimensions object by keyname $dimensionsKeyname"); 362 | } 363 | return $idim; 364 | } 365 | 366 | } 367 | 368 | ?> -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | 2 | CloudfrontService - A PHP Cloudfront Framework - Version 0.0 3 | 4 | INTRODUCTION 5 | ------------ 6 | For details visit the related article on the AWS Ninja blog: 7 | http://wp.me/pWVsZ-4 8 | 9 | REQUIREMENTS 10 | ------------ 11 | 12 | PHP 5.0+ 13 | MagickWand - http://www.magickwand.org 14 | awsninja_core - Core components for all AWSNinja libraries. 15 | 16 | 17 | INSTALLATION 18 | ------------ 19 | 20 | 1. Copy the awsninja_cloudfrontimageservice folder to the same location 21 | that contains the awsninja_core directory. 22 | 23 | 2. Move the CloudfrontService-related entries in the config.samp.php to 24 | the config.php or config.samp.php (depends if you renamed that file) 25 | and modify them to suit your needs. 26 | 27 | 3. Run the command in cloudfrontimageservice.sql on your MySQL database 28 | to create the three needed tables. 29 | 30 | 4. From the command line, enter the examples subdriectory and run the 31 | configureImageObjects.php script. You should see the message "Image 32 | Id 1 created." 33 | 34 | 5. Then run the cloudfrontImageText.php script. It should print out a 35 | working url of the image on your Cloudfront distribution. This 36 | indicates that everything is working correctly. 37 | 38 | 39 | -------------------------------------------------------------------------------- /classes/Image.php: -------------------------------------------------------------------------------- 1 | Id)) 21 | { 22 | self::update($this); 23 | } 24 | else 25 | { 26 | $id = self::insert($this); 27 | $this->Id = $id; 28 | } 29 | return true; 30 | } 31 | 32 | 33 | public static function insert(Image $img) 34 | { 35 | $db = Db::instance(); 36 | $sql = "INSERT INTO tbl_image (filePath, imageType, version, filePath_hashIndex) VALUES (:filePath, :imageType, :version, crc32(:filePath))"; 37 | $vals = array( 38 | ':filePath'=>$img->FilePath, 39 | ':imageType'=>$img->ImageType, 40 | ':version'=>$img->Version 41 | ); 42 | $id = $db->executeInsertStatement($sql, $vals); 43 | return $id; 44 | } 45 | 46 | public static function findById($imageId) 47 | { 48 | $db = Db::instance(); 49 | $sql = "SELECT id, filePath, imageType, version FROM tbl_image WHERE id=:id"; 50 | $vals = array( 51 | ':id'=>$imageId 52 | ); 53 | $res = $db->executeSelectStatement($sql, $vals); 54 | if(isset($res[0])) 55 | { 56 | $r = $res[0]; 57 | $img = new Image(); 58 | $img->Id = $r['id']; 59 | $img->FilePath = $r['filePath']; 60 | $img->Version = $r['version']; 61 | $img->ImageType = $r['imageType']; 62 | return $img; 63 | } 64 | else 65 | { 66 | return null; 67 | } 68 | } 69 | 70 | public static function findByFilePath($filePath) 71 | { 72 | $db = Db::instance(); 73 | $sql = "SELECT id, filePath, imageType, version FROM tbl_image WHERE filePath=:filePath and filePath_hashIndex=crc32(:filePath)"; 74 | $vals = array( 75 | ':filePath'=>$filePath 76 | ); 77 | $res = $db->executeSelectStatement($sql, $vals); 78 | if(isset($res[0])) 79 | { 80 | $r = $res[0]; 81 | $img = new Image(); 82 | $img->Id = $r['id']; 83 | $img->FilePath = $r['filePath']; 84 | $img->Version = $r['version']; 85 | $img->ImageType = $r['imageType']; 86 | return $img; 87 | } 88 | else 89 | { 90 | return null; 91 | } 92 | } 93 | } 94 | 95 | 96 | ?> -------------------------------------------------------------------------------- /classes/ImageDimensions.php: -------------------------------------------------------------------------------- 1 | $keyName 25 | ); 26 | $res = $db->executeSelectStatement($sql, $vals); 27 | if(isset($res[0])) 28 | { 29 | $r = $res[0]; 30 | 31 | $imDem = new ImageDimensions(); 32 | $imDem->Id = $r['id']; 33 | $imDem->Keyname = $r['keyname']; 34 | $imDem->Description = $r['description']; 35 | $imDem->Width = $r['width']; 36 | $imDem->Height = $r['height']; 37 | return $imDem; 38 | } 39 | else 40 | { 41 | return null; 42 | } 43 | } 44 | } 45 | 46 | 47 | ?> -------------------------------------------------------------------------------- /classes/ImageDimensionsMap.php: -------------------------------------------------------------------------------- 1 | Id = $id; 24 | return true; 25 | } 26 | 27 | public static function replaceInto(ImageDimensionsMap $idm) 28 | { 29 | $db = Db::instance(); 30 | $sql = "REPLACE INTO tbl_imageDimensionsMap (imageId, imageDimensionsId, width, height, version) VALUES (:imageId, :imageDimensionsId, :width, :height, :version)"; 31 | $vals = array( 32 | ':imageId'=>$idm->ImageId, 33 | ':imageDimensionsId'=>$idm->ImageDimensionsId, 34 | ':width'=>$idm->Width, 35 | ':height'=>$idm->Height, 36 | ':version'=>$idm->Version 37 | ); 38 | $id = $db->executeInsertStatement($sql, $vals); 39 | return $id; 40 | } 41 | 42 | public static function findByImageIdAndDimensionsId($imageId, $imageDimensionsId) 43 | { 44 | $db = Db::instance(); 45 | $sql = "SELECT id, imageId, imageDimensionsId, width, height, version FROM tbl_imageDimensionsMap WHERE imageId=:imageId AND imageDimensionsId=:imageDimensionsId"; 46 | $vals = array( 47 | ':imageId'=>$imageId, 48 | ':imageDimensionsId'=>$imageDimensionsId 49 | ); 50 | $res = $db->executeSelectStatement($sql, $vals); 51 | if(isset($res[0])) 52 | { 53 | $r = $res[0]; 54 | $idm = new ImageDimensionsMap(); 55 | $idm->Id = $r['id']; 56 | $idm->ImageId = $r['imageId']; 57 | $idm->ImageDimensionsId = $r['imageDimensionsId']; 58 | $idm->Width = $r['width']; 59 | $idm->Height = $r['height']; 60 | $idm->Version = $r['version']; 61 | return $idm; 62 | } 63 | else 64 | { 65 | return null; 66 | } 67 | } 68 | } 69 | 70 | 71 | ?> -------------------------------------------------------------------------------- /cloudfrontimageservice.sql: -------------------------------------------------------------------------------- 1 | -- phpMyAdmin SQL Dump 2 | -- version 3.3.3 3 | -- http://www.phpmyadmin.net 4 | -- 5 | -- Host: localhost 6 | -- Generation Time: Jun 11, 2010 at 12:13 AM 7 | -- Server version: 5.1.41 8 | -- PHP Version: 5.3.2-1ubuntu4.2 9 | 10 | SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; 11 | 12 | 13 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 14 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 15 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 16 | /*!40101 SET NAMES utf8 */; 17 | 18 | -- 19 | -- Database: `ninja_cloudfrontimages` 20 | -- 21 | 22 | -- -------------------------------------------------------- 23 | 24 | -- 25 | -- Table structure for table `tbl_image` 26 | -- 27 | 28 | CREATE TABLE IF NOT EXISTS `tbl_image` ( 29 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 30 | `filePath` varchar(255) NOT NULL, 31 | `imageType` enum('GIF','JPG','PNG') NOT NULL, 32 | `version` tinyint(3) unsigned NOT NULL, 33 | `filePath_hashIndex` int(10) unsigned NOT NULL, 34 | PRIMARY KEY (`id`), 35 | KEY `fileNameCRC` (`filePath_hashIndex`) 36 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ; 37 | 38 | -- 39 | -- Dumping data for table `tbl_image` 40 | -- 41 | 42 | 43 | -- -------------------------------------------------------- 44 | 45 | -- 46 | -- Table structure for table `tbl_imageDimensions` 47 | -- 48 | 49 | CREATE TABLE IF NOT EXISTS `tbl_imageDimensions` ( 50 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 51 | `keyname` varchar(50) NOT NULL, 52 | `description` varchar(500) NOT NULL, 53 | `width` smallint(11) unsigned NOT NULL, 54 | `height` smallint(11) unsigned NOT NULL, 55 | PRIMARY KEY (`id`) 56 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=10 ; 57 | 58 | -- 59 | -- Dumping data for table `tbl_imageDimensions` 60 | -- 61 | 62 | INSERT INTO `tbl_imageDimensions` (`id`, `keyname`, `description`, `width`, `height`) VALUES 63 | (1, 'thumb_small', 'Small Thumbnail', 78, 100), 64 | (2, 'thumb_large', 'Large Thumbnail', 100, 100), 65 | (3, 'xlarge', 'Very large', 700, 700), 66 | (4, 'marketplace_listing', '', 200, 260), 67 | (5, 'original', 'The original dimensions', 0, 0), 68 | (6, 'bookthumb_carousel', 'For carousel on homepage', 130, 168), 69 | (7, 'browsebox_thumb', 'for browsebox', 104, 134), 70 | (8, 'thumbnail', 'for the create page upload widgets', 136, 174), 71 | (9, 'tinythumb', 'recently viewed', 20, 30); 72 | 73 | -- -------------------------------------------------------- 74 | 75 | -- 76 | -- Table structure for table `tbl_imageDimensionsMap` 77 | -- 78 | 79 | CREATE TABLE IF NOT EXISTS `tbl_imageDimensionsMap` ( 80 | `id` int(11) NOT NULL AUTO_INCREMENT, 81 | `imageId` int(11) unsigned NOT NULL, 82 | `imageDimensionsId` int(11) unsigned NOT NULL, 83 | `width` smallint(5) unsigned NOT NULL, 84 | `height` smallint(5) unsigned NOT NULL, 85 | `version` tinyint(11) unsigned NOT NULL, 86 | PRIMARY KEY (`imageId`,`imageDimensionsId`), 87 | UNIQUE KEY `id` (`id`), 88 | KEY `imageDimensionsId` (`imageDimensionsId`) 89 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ; 90 | 91 | -- 92 | -- Dumping data for table `tbl_imageDimensionsMap` 93 | -- 94 | 95 | -------------------------------------------------------------------------------- /config.samp.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/cloudfrontImageTest.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php -q 2 | getUrlFromFilePath($destination, $dimensionkey); 16 | 17 | echo($url); 18 | 19 | 20 | ?> 21 | 22 | -------------------------------------------------------------------------------- /examples/configureImageObjects.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php -q 2 | createImageObjectFromFileSystemPath($originSystemPath, $destination); 13 | 14 | 15 | echo("Image Id {$imgObj->Id} created.\n"); 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ?> -------------------------------------------------------------------------------- /examples/flower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awsninja/awsninja_cloudfrontimageservice/11ce094634092f581c3a5e525ddbf8e40466f781/examples/flower.jpg --------------------------------------------------------------------------------