├── .gitignore ├── .htaccess ├── .travis.yml ├── README.md ├── Tests └── PlaceholderTest.php ├── composer.json ├── i ├── OFL.txt ├── Oswald-Regular.ttf ├── index.php └── placeholder.class.php ├── nginx.conf └── phpunit.xml /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # img-src.co placeholder generator rewrites 2 | RewriteEngine On 3 | RewriteBase / 4 | RewriteRule ^(\d+)/?$ /i/index.php?w=$1&h=$1 5 | RewriteRule ^(\d+)x(\d+)/?$ /i/index.php?w=$1&h=$2 6 | RewriteRule ^(\d+)/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/?$ /i/index.php?w=$1&h=$1&bgColor=$2 7 | RewriteRule ^(\d+)x(\d+)/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/?$ /i/index.php?w=$1&h=$2&bgColor=$3 8 | RewriteRule ^(\d+)/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/?$ /i/index.php?w=$1&h=$1&bgColor=$2&textColor=$3 9 | RewriteRule ^(\d+)x(\d+)/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/?$ /i/index.php?w=$1&h=$2&bgColor=$3&textColor=$4 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.3 4 | - 5.4 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # img-src.co Placeholder Image Generator # 2 | 3 | [![Build Status](https://travis-ci.org/img-src/placeholder.png)](https://travis-ci.org/img-src/placeholder) 4 | 5 | ## About ## 6 | 7 | PHP placeholder image generator used on [http://img-src.co](http://img-src.co "img-src.co"). 8 | 9 | ## Requirements ## 10 | 11 | * PHP 5.1.0 or higher 12 | 13 | ## How to use ## 14 | 15 | ### Quick start ### 16 | 17 | 1. Copy the `/i/` folder and all of its contents to your web root directory. 18 | 2. You're now ready to make placeholder images using the following format: 19 | `` 20 | 21 | Examples: 22 | 23 | 24 | 25 | 26 | 27 | 28 | ### Advanced setup ("pretty URLs") ### 29 | 30 | 1. Copy the `/i/` folder and all of its contents to your web root directory. 31 | 2. Set up the rewrite rules for your web server (see below). 32 | 3. You're now ready to make placeholder images using the following format (background & text colors are optional): 33 | `` 34 | 35 | Examples: 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | **Apache rewrites**: This can either go in an .htaccess file or your site config file. A sample .htaccess file is included in the repository. 44 | 45 | RewriteEngine On 46 | RewriteBase / 47 | RewriteRule ^(\d+)/?$ /i/index.php?w=$1&h=$1 48 | RewriteRule ^(\d+)x(\d+)/?$ /i/index.php?w=$1&h=$2 49 | RewriteRule ^(\d+)/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/?$ /i/index.php?w=$1&h=$1&bgColor=$2 50 | RewriteRule ^(\d+)x(\d+)/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/?$ /i/index.php?w=$1&h=$2&bgColor=$3 51 | RewriteRule ^(\d+)/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/?$ /i/index.php?w=$1&h=$1&bgColor=$2&textColor=$3 52 | RewriteRule ^(\d+)x(\d+)/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/([a-fA-F0-9]{3}|[a-fA-F0-9]{6})/?$ /i/index.php?w=$1&h=$2&bgColor=$3&textColor=$4 53 | 54 | **Nginx rewrites**: This needs to go in your site config file. A sample config file is included in the repository that can be included by your site config. 55 | 56 | location ~* "^/(\d+)x(\d+)/([a-f0-9]{3}|[a-f0-9]{6})/([a-f0-9]{3}|[a-f0-9]{6})/?$" { 57 | try_files $uri $uri/ /i/index.php?w=$1&h=$2&bgColor=$3&textColor=$4; 58 | } 59 | location ~* "^/(\d+)/([a-f0-9]{3}|[a-f0-9]{6})/([a-f0-9]{3}|[a-f0-9]{6})/?$" { 60 | try_files $uri $uri/ /i/index.php?w=$1&h=$1&bgColor=$2&textColor=$3; 61 | } 62 | location ~* "^/(\d+)x(\d+)/([a-f0-9]{3}|[a-f0-9]{6})/?$" { 63 | try_files $uri $uri/ /i/index.php?w=$1&h=$2&bgColor=$3; 64 | } 65 | location ~* "^/(\d+)/([a-f0-9]{3}|[a-f0-9]{6})/?$" { 66 | try_files $uri $uri/ /i/index.php?w=$1&h=$1&bgColor=$2; 67 | } 68 | location ~ "^/(\d+)x(\d+)/?$" { 69 | try_files $uri $uri/ /i/index.php?w=$1&h=$2; 70 | } 71 | location ~ "^/(\d+)/?$" { 72 | try_files $uri $uri/ /i/index.php?w=$1&h=$1; 73 | } 74 | 75 | ## Support ## 76 | 77 | To report a bug, please use our [GitHub issue tracker](https://github.com/img-src/placeholder/issues "GitHub issue tracker"). Better yet, if you find a bug, feel free to fork our code and submit a pull request with your fix. 78 | 79 | ## License ## 80 | 81 | The img-src.co placeholder image generator is distributed under the terms of the [MIT license](http://www.opensource.org/licenses/mit-license.php). copyright © 2012-2013 -------------------------------------------------------------------------------- /Tests/PlaceholderTest.php: -------------------------------------------------------------------------------- 1 | placeholder = new Placeholder(); 11 | } 12 | 13 | public function tearDown() { 14 | unset($this->placeholder); 15 | } 16 | 17 | // Test if valid background color (full) is assigned 18 | public function testBackgroundColorValidSixHex() 19 | { 20 | $backgroundColor = '123456'; 21 | $this->placeholder->setBackgroundColor($backgroundColor); 22 | $this->assertEquals($backgroundColor, $this->placeholder->getBackgroundColor()); 23 | } 24 | 25 | // Test if valid background color (abbrev) is assigned 26 | public function testBackgroundColorValidThreeHex() 27 | { 28 | $backgroundColor = '325'; 29 | $this->placeholder->setBackgroundColor($backgroundColor); 30 | $this->assertEquals($backgroundColor, $this->placeholder->getBackgroundColor()); 31 | } 32 | 33 | // Test if invalid background hex color throws correct exception 34 | public function testBackgroundColorInvalidHex() 35 | { 36 | $this->setExpectedException('InvalidArgumentException', 'Background color must be a valid RGB hex code.'); 37 | $backgroundColor = 'xxx343'; 38 | $this->placeholder->setBackgroundColor($backgroundColor); 39 | } 40 | 41 | // Test if invalid background hex color throws correct exception 42 | public function testBackgroundColorInvalidFormat() 43 | { 44 | $this->setExpectedException('InvalidArgumentException', 'Background color must be 3 or 6 character hex code.'); 45 | $backgroundColor = 'xxx34'; 46 | $this->placeholder->setBackgroundColor($backgroundColor); 47 | } 48 | 49 | // Test if valid text color (full) is assigned 50 | public function testTextColorValidSixHex() 51 | { 52 | $textColor = '123456'; 53 | $this->placeholder->setTextColor($textColor); 54 | $this->assertEquals($textColor, $this->placeholder->getTextColor()); 55 | } 56 | 57 | // Test if valid text color (abbrev) is assigned 58 | public function testTextColorValidThreeHex() 59 | { 60 | $textColor = '325'; 61 | $this->placeholder->setTextColor($textColor); 62 | $this->assertEquals($textColor, $this->placeholder->getTextColor()); 63 | } 64 | 65 | // Test if invalid text hex color throws correct exception 66 | public function testTextColorInvalidHex() 67 | { 68 | $this->setExpectedException('InvalidArgumentException', 'Text color must be a valid RGB hex code.'); 69 | $textColor = 'xxx343'; 70 | $this->placeholder->setTextColor($textColor); 71 | } 72 | 73 | // Test if invalid text hex color throws correct exception 74 | public function testTextColorInvalidFormat() 75 | { 76 | $this->setExpectedException('InvalidArgumentException', 'Text color must be 3 or 6 character hex code.'); 77 | $textColor = 'xxx34'; 78 | $this->placeholder->setTextColor($textColor); 79 | } 80 | 81 | // Test if setting font with valid path will assign 82 | public function testValidFont() 83 | { 84 | $fontPath = dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'i' . DIRECTORY_SEPARATOR . 'Oswald-Regular.ttf'; 85 | $this->placeholder->setFont($fontPath); 86 | $this->assertEquals($fontPath, $this->placeholder->getFont()); 87 | } 88 | 89 | // Test if setting font with valid path will assign 90 | public function testInvalidFont() 91 | { 92 | $this->setExpectedException('InvalidArgumentException', 'Font file must exist and be readable by web server.'); 93 | $fontPath = './i-dont-exist.ttf'; 94 | $this->placeholder->setFont($fontPath); 95 | } 96 | 97 | // Test if valid expires header is assigned 98 | public function testValidExpires() 99 | { 100 | $expires = 50000; 101 | $this->placeholder->setExpires($expires); 102 | $this->assertEquals($expires, $this->placeholder->getExpires()); 103 | } 104 | 105 | // Test if invalid expires header throws correct exception 106 | public function testInvalidExpires() 107 | { 108 | $this->setExpectedException('InvalidArgumentException', 'Expires must be an integer.'); 109 | $expires = '10 days'; 110 | $this->placeholder->setExpires($expires); 111 | } 112 | 113 | // Test if valid max width is assigned 114 | public function testValidMaxWidth() 115 | { 116 | $maxWidth = 50000; 117 | $this->placeholder->setMaxWidth($maxWidth); 118 | $this->assertEquals($maxWidth, $this->placeholder->getMaxWidth()); 119 | } 120 | 121 | // Test if invalid max width throws correct exception 122 | public function testInvalidMaxWidth() 123 | { 124 | $this->setExpectedException('InvalidArgumentException', 'Maximum width must be an integer.'); 125 | $maxWidth = 'One million'; 126 | $this->placeholder->setMaxWidth($maxWidth); 127 | } 128 | 129 | // Test if valid max height is assigned 130 | public function testValidMaxHeight() 131 | { 132 | $maxHeight = 50000; 133 | $this->placeholder->setMaxHeight($maxHeight); 134 | $this->assertEquals($maxHeight, $this->placeholder->getMaxHeight()); 135 | } 136 | 137 | // Test if invalid max height throws correct exception 138 | public function testInvalidMaxHeight() 139 | { 140 | $this->setExpectedException('InvalidArgumentException', 'Maximum height must be an integer.'); 141 | $maxHeight = 'One million'; 142 | $this->placeholder->setMaxHeight($maxHeight); 143 | } 144 | 145 | // Test if valid cache bool is assigned 146 | public function testValidCache() 147 | { 148 | $cache = true; 149 | $this->placeholder->setCache($cache); 150 | $this->assertEquals($cache, $this->placeholder->getCache()); 151 | } 152 | 153 | // Test if invalid cache bool throws correct exception 154 | public function testInvalidCache() 155 | { 156 | $this->setExpectedException('InvalidArgumentException', 'setCache expects a boolean value.'); 157 | $cache = 1; 158 | $this->placeholder->setCache($cache); 159 | } 160 | 161 | // Test if valid cache directory is assigned 162 | public function testValidCacheDir() 163 | { 164 | $cacheDir = dirname(__FILE__); 165 | $this->placeholder->setCacheDir($cacheDir); 166 | $this->assertEquals($cacheDir, $this->placeholder->getCacheDir()); 167 | } 168 | 169 | // Test if invalid cache directory throws correct exception 170 | public function testInvalidCacheDir() 171 | { 172 | $this->setExpectedException('InvalidArgumentException', 'setCacheDir expects a directory.'); 173 | $cacheDir = __FILE__; 174 | $this->placeholder->setCacheDir($cacheDir); 175 | } 176 | 177 | // Test if valid width is assigned 178 | public function testValidWidth() 179 | { 180 | $width = 50000; 181 | $this->placeholder->setWidth($width); 182 | $this->assertEquals($width, $this->placeholder->getWidth()); 183 | } 184 | 185 | // Test if invalid width throws correct exception 186 | public function testInvalidWidthFormat() 187 | { 188 | $this->setExpectedException('InvalidArgumentException', 'Width must be an integer.'); 189 | $width = 'One million'; 190 | $this->placeholder->setWidth($width); 191 | } 192 | 193 | // Test if invalid width throws correct exception 194 | public function testInvalidWidthZero() 195 | { 196 | $this->setExpectedException('InvalidArgumentException', 'Width must be greater than zero.'); 197 | $width = 0; 198 | $this->placeholder->setWidth($width); 199 | } 200 | 201 | // Test if valid height is assigned 202 | public function testValidHeight() 203 | { 204 | $height = 50000; 205 | $this->placeholder->setHeight($height); 206 | $this->assertEquals($height, $this->placeholder->getHeight()); 207 | } 208 | 209 | // Test if invalid height throws correct exception 210 | public function testInvalidHeightFormat() 211 | { 212 | $this->setExpectedException('InvalidArgumentException', 'Height must be an integer.'); 213 | $height = 'One million'; 214 | $this->placeholder->setHeight($height); 215 | } 216 | 217 | // Test if invalid height throws correct exception 218 | public function testInvalidHeightZero() 219 | { 220 | $this->setExpectedException('InvalidArgumentException', 'Height must be greater than zero.'); 221 | $height = 0; 222 | $this->placeholder->setHeight($height); 223 | } 224 | 225 | // Test if image requested larger than max size throws correct error 226 | public function testRenderTooLarge() 227 | { 228 | $maxWidth = 400; 229 | $maxHeight = 600; 230 | $width = 500; 231 | $height = 900; 232 | $this->setExpectedException('RuntimeException', 'Placeholder size may not exceed ' . $maxWidth . 'x' . $maxHeight . ' pixels.'); 233 | $this->placeholder->setMaxWidth($maxWidth); 234 | $this->placeholder->setMaxHeight($maxHeight); 235 | $this->placeholder->setWidth($width); 236 | $this->placeholder->setHeight($height); 237 | $this->placeholder->render(); 238 | } 239 | 240 | // Test if valid image size returns correctly 241 | /** 242 | * @runInSeparateProcess 243 | */ 244 | public function testRenderValid() 245 | { 246 | $maxWidth = 1000; 247 | $maxHeight = 1000; 248 | $width = 500; 249 | $height = 900; 250 | $this->placeholder->setMaxWidth($maxWidth); 251 | $this->placeholder->setMaxHeight($maxHeight); 252 | $this->placeholder->setWidth($width); 253 | $this->placeholder->setHeight($height); 254 | ob_start(); 255 | $this->placeholder->render(); 256 | $output = ob_get_contents(); 257 | ob_clean(); 258 | $tempFilename = '/tmp/phpunit.testImage.testRenderValid'; 259 | file_put_contents($tempFilename, $output); 260 | $type = exif_imagetype($tempFilename); 261 | $this->assertEquals(IMAGETYPE_PNG, $type); 262 | $size = getimagesize($tempFilename); 263 | $this->assertEquals($size[0], $width); 264 | $this->assertEquals($size[1], $height); 265 | unlink($tempFilename); 266 | } 267 | } 268 | ?> -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require-dev": { 3 | "phpunit/phpunit": "*" 4 | } 5 | } -------------------------------------------------------------------------------- /i/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Vernon Adams (vern@newtypography.co.uk), 2 | with Reserved Font Name Oswald 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /i/Oswald-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/img-src/placeholder/3915741e868d1f6c375f58bc0262f98a125cc637/i/Oswald-Regular.ttf -------------------------------------------------------------------------------- /i/index.php: -------------------------------------------------------------------------------- 1 | setWidth($width); 14 | $placeholder->setHeight($height); 15 | if ($backgroundColor) $placeholder->setBackgroundColor($backgroundColor); 16 | if ($textColor) $placeholder->setTextColor($textColor); 17 | $placeholder->render(); 18 | } catch (Exception $e){ 19 | die($e->getMessage()); 20 | } 21 | -------------------------------------------------------------------------------- /i/placeholder.class.php: -------------------------------------------------------------------------------- 1 | backgroundColor = 'dddddd'; 16 | $this->cache = false; 17 | $this->cacheDir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'cache'; 18 | $this->expires = 604800; 19 | $this->font = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Oswald-Regular.ttf'; 20 | $this->maxHeight = 2000; 21 | $this->maxWidth = 2000; 22 | $this->textColor = '000000'; 23 | } 24 | 25 | /** 26 | * Sets background color 27 | * 28 | * @param string $hex Hex code value 29 | * @throws InvalidArgumentException 30 | */ 31 | function setBackgroundColor($hex) 32 | { 33 | if (strlen($hex) === 3 || strlen($hex) === 6) { 34 | if (preg_match('/^[a-f0-9]{3}$|^[a-f0-9]{6}$/i', $hex)) { 35 | $this->backgroundColor = $hex; 36 | } else { 37 | throw new InvalidArgumentException('Background color must be a valid RGB hex code.'); 38 | } 39 | } else { 40 | throw new InvalidArgumentException('Background color must be 3 or 6 character hex code.'); 41 | } 42 | } 43 | 44 | /** 45 | * Gets background color 46 | */ 47 | function getBackgroundColor() 48 | { 49 | return $this->backgroundColor; 50 | } 51 | 52 | /** 53 | * Sets text color 54 | * 55 | * @param string $hex Hex code value 56 | * @throws InvalidArgumentException 57 | */ 58 | function setTextColor($hex) 59 | { 60 | if (strlen($hex) === 3 || strlen($hex) === 6) { 61 | if (preg_match('/^[a-f0-9]{3}$|^[a-f0-9]{6}$/i', $hex)) { 62 | $this->textColor = $hex; 63 | } else { 64 | throw new InvalidArgumentException('Text color must be a valid RGB hex code.'); 65 | } 66 | } else { 67 | throw new InvalidArgumentException('Text color must be 3 or 6 character hex code.'); 68 | } 69 | } 70 | 71 | /** 72 | * Gets text color 73 | */ 74 | function getTextColor() 75 | { 76 | return $this->textColor; 77 | } 78 | 79 | /** 80 | * Sets location of TTF font 81 | * 82 | * @param string $fontPath Location of TTF font 83 | * @throws InvalidArgumentException 84 | */ 85 | function setFont($fontPath) 86 | { 87 | if (is_readable($fontPath)) { 88 | $this->font = $fontPath; 89 | } else { 90 | throw new InvalidArgumentException('Font file must exist and be readable by web server.'); 91 | } 92 | } 93 | 94 | /** 95 | * Gets location of font 96 | */ 97 | function getFont() 98 | { 99 | return $this->font; 100 | } 101 | 102 | /** 103 | * Set expires header value 104 | * 105 | * @param int $expires Seconds used in expires HTTP header 106 | * @throws InvalidArgumentException 107 | */ 108 | function setExpires($expires) 109 | { 110 | if (preg_match('/^\d+$/', $expires)) { 111 | $this->expires = $expires; 112 | } else { 113 | throw new InvalidArgumentException('Expires must be an integer.'); 114 | } 115 | } 116 | 117 | /** 118 | * Get expires header value 119 | */ 120 | function getExpires() 121 | { 122 | return $this->expires; 123 | } 124 | 125 | /** 126 | * Set maximum width allowed for placeholder image 127 | * 128 | * @param int $maxWidth Maximum width of generated image 129 | * @throws InvalidArgumentException 130 | */ 131 | function setMaxWidth($maxWidth) 132 | { 133 | if (preg_match('/^\d+$/', $maxWidth)) { 134 | $this->maxWidth = $maxWidth; 135 | } else { 136 | throw new InvalidArgumentException('Maximum width must be an integer.'); 137 | } 138 | } 139 | 140 | /** 141 | * Get max width value 142 | */ 143 | function getMaxWidth() 144 | { 145 | return $this->maxWidth; 146 | } 147 | 148 | /** 149 | * Set maximum height allowed for placeholder image 150 | * 151 | * @param int $maxHeight Maximum height of generated image 152 | * @throws InvalidArgumentException 153 | */ 154 | function setMaxHeight($maxHeight) 155 | { 156 | if (preg_match('/^\d+$/', $maxHeight)) { 157 | $this->maxHeight = $maxHeight; 158 | } else { 159 | throw new InvalidArgumentException('Maximum height must be an integer.'); 160 | } 161 | } 162 | 163 | /** 164 | * Get max height value 165 | */ 166 | function getMaxHeight() 167 | { 168 | return $this->maxHeight; 169 | } 170 | 171 | /** 172 | * Enable or disable cache 173 | * 174 | * @param bool $cache Whether or not to cache 175 | * @throws InvalidArgumentException 176 | */ 177 | function setCache($cache) 178 | { 179 | if (is_bool($cache)) { 180 | $this->cache = $cache; 181 | } else { 182 | throw new InvalidArgumentException('setCache expects a boolean value.'); 183 | } 184 | } 185 | 186 | /** 187 | * Get cache value 188 | */ 189 | function getCache() 190 | { 191 | return $this->cache; 192 | } 193 | 194 | /** 195 | * Sets caching path 196 | * 197 | * @param string $cacheDir Path to cache folder, must be writable by web server 198 | * @throws InvalidArgumentException 199 | */ 200 | function setCacheDir($cacheDir) 201 | { 202 | if (is_dir($cacheDir)) { 203 | $this->cacheDir = $cacheDir; 204 | } else { 205 | throw new InvalidArgumentException('setCacheDir expects a directory.'); 206 | } 207 | } 208 | 209 | /** 210 | * Get cache directory value 211 | */ 212 | function getCacheDir() 213 | { 214 | return $this->cacheDir; 215 | } 216 | 217 | /** 218 | * Set width of image to render 219 | * 220 | * @param int $width Width of generated image 221 | * @throws InvalidArgumentException 222 | */ 223 | function setWidth($width) 224 | { 225 | if (preg_match('/^\d+$/', $width)) { 226 | if ($width > 0) { 227 | $this->width = $width; 228 | } else { 229 | throw new InvalidArgumentException('Width must be greater than zero.'); 230 | } 231 | } else { 232 | throw new InvalidArgumentException('Width must be an integer.'); 233 | } 234 | } 235 | 236 | /** 237 | * Get width value 238 | */ 239 | function getWidth() 240 | { 241 | return $this->width; 242 | } 243 | 244 | /** 245 | * Set height of image to render 246 | * 247 | * @param int $height Height of generated image 248 | * @throws InvalidArgumentException 249 | */ 250 | function setHeight($height) 251 | { 252 | if (preg_match('/^\d+$/', $height)) { 253 | if ($height > 0) { 254 | $this->height = $height; 255 | } else { 256 | throw new InvalidArgumentException('Height must be greater than zero.'); 257 | } 258 | } else { 259 | throw new InvalidArgumentException('Height must be an integer.'); 260 | } 261 | } 262 | 263 | /** 264 | * Get height value 265 | */ 266 | function getHeight() 267 | { 268 | return $this->height; 269 | } 270 | 271 | /** 272 | * Display image and cache (if enabled) 273 | * 274 | * @throws RuntimeException 275 | */ 276 | function render() 277 | { 278 | if ($this->width <= $this->maxWidth && $this->height <= $this->maxHeight) { 279 | $cachePath = $this->cacheDir . '/' . $this->width . '_' . $this->height . '_' . (strlen($this->backgroundColor) === 3 ? $this->backgroundColor[0] . $this->backgroundColor[0] . $this->backgroundColor[1] . $this->backgroundColor[1] . $this->backgroundColor[2] . $this->backgroundColor[2] : $this->backgroundColor) . '_' . (strlen($this->textColor) === 3 ? $this->textColor[0] . $this->textColor[0] . $this->textColor[1] . $this->textColor[1] . $this->textColor[2] . $this->textColor[2] : $this->textColor) . '.png'; 280 | header('Content-type: image/png'); 281 | header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', time() + $this->expires)); 282 | header('Cache-Control: public'); 283 | if ($this->cache === true && is_readable($cachePath)) { 284 | // send header identifying cache hit & send cached image 285 | header('img-src-cache: hit'); 286 | print file_get_contents($cachePath); 287 | } else { 288 | // cache disabled or no cached copy exists 289 | // send header identifying cache miss if cache enabled 290 | if ($this->cache === true) header('img-src-cache: miss'); 291 | 292 | $image = $this->createImage(); 293 | 294 | imagepng($image); 295 | // write cache 296 | if ($this->cache === true && is_writable($this->cacheDir)) { 297 | imagepng($image, $cachePath); 298 | } 299 | imagedestroy($image); 300 | } 301 | } else { 302 | throw new RuntimeException('Placeholder size may not exceed ' . $this->maxWidth . 'x' . $this->maxHeight . ' pixels.'); 303 | } 304 | } 305 | 306 | private function createImage() 307 | { 308 | $image = imagecreate($this->width, $this->height); 309 | // convert backgroundColor hex to RGB values 310 | list($bgR, $bgG, $bgB) = $this->hexToDec($this->backgroundColor); 311 | $backgroundColor = imagecolorallocate($image, $bgR, $bgG, $bgB); 312 | // convert textColor hex to RGB values 313 | list($textR, $textG, $textB) = $this->hexToDec($this->textColor); 314 | $textColor = imagecolorallocate($image, $textR, $textG, $textB); 315 | $text = $this->width . 'x' . $this->height; 316 | imagefilledrectangle($image, 0, 0, $this->width, $this->height, $backgroundColor); 317 | $fontSize = 26; 318 | $textBoundingBox = imagettfbbox($fontSize, 0, $this->font, $text); 319 | // decrease the default font size until it fits nicely within the image 320 | while (((($this->width - ($textBoundingBox[2] - $textBoundingBox[0])) < 10) || (($this->height - ($textBoundingBox[1] - $textBoundingBox[7])) < 10)) && ($fontSize > 1)) { 321 | $fontSize--; 322 | $textBoundingBox = imagettfbbox($fontSize, 0, $this->font, $text); 323 | } 324 | imagettftext($image, $fontSize, 0, ($this->width / 2) - (($textBoundingBox[2] - $textBoundingBox[0]) / 2), ($this->height / 2) + (($textBoundingBox[1] - $textBoundingBox[7]) / 2), $textColor, $this->font, $text); 325 | 326 | return $image; 327 | } 328 | 329 | function renderToFile($file) 330 | { 331 | if (!file_exists($file)) { 332 | touch($file); 333 | } 334 | $image = $this->createImage(); 335 | imagepng($image, $file); 336 | imagedestroy($image); 337 | } 338 | 339 | /** 340 | * Convert hex code to array of RGB decimal values 341 | * 342 | * @param string $hex Hex code to convert to dec 343 | * @return array 344 | * @throws InvalidArgumentException 345 | */ 346 | private function hexToDec($hex) 347 | { 348 | if (strlen($hex) === 3) { 349 | $rgbArray = array(hexdec($hex[0] . $hex[0]), hexdec($hex[1] . $hex[1]), hexdec($hex[2] . $hex[2])); 350 | } else if (strlen($hex) === 6) { 351 | $rgbArray = array(hexdec($hex[0] . $hex[1]), hexdec($hex[2] . $hex[3]), hexdec($hex[4] . $hex[5])); 352 | } else { 353 | throw new InvalidArgumentException('Could not convert hex value to decimal.'); 354 | } 355 | return $rgbArray; 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | # img-src.co placeholder generator rewrites 2 | location ~* "^/(\d+)x(\d+)/([a-f0-9]{3}|[a-f0-9]{6})/([a-f0-9]{3}|[a-f0-9]{6})/?$" { 3 | try_files $uri $uri/ /i/index.php?w=$1&h=$2&bgColor=$3&textColor=$4; 4 | } 5 | location ~* "^/(\d+)/([a-f0-9]{3}|[a-f0-9]{6})/([a-f0-9]{3}|[a-f0-9]{6})/?$" { 6 | try_files $uri $uri/ /i/index.php?w=$1&h=$1&bgColor=$2&textColor=$3; 7 | } 8 | location ~* "^/(\d+)x(\d+)/([a-f0-9]{3}|[a-f0-9]{6})/?$" { 9 | try_files $uri $uri/ /i/index.php?w=$1&h=$2&bgColor=$3; 10 | } 11 | location ~* "^/(\d+)/([a-f0-9]{3}|[a-f0-9]{6})/?$" { 12 | try_files $uri $uri/ /i/index.php?w=$1&h=$1&bgColor=$2; 13 | } 14 | location ~ "^/(\d+)x(\d+)/?$" { 15 | try_files $uri $uri/ /i/index.php?w=$1&h=$2; 16 | } 17 | location ~ "^/(\d+)/?$" { 18 | try_files $uri $uri/ /i/index.php?w=$1&h=$1; 19 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./Tests/ 6 | 7 | 8 | --------------------------------------------------------------------------------