├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .php_cs ├── .styleci.yml ├── Adapter ├── Adapter.php ├── AdapterInterface.php ├── Common.php ├── GD.php └── Imagick.php ├── Exceptions └── GenerationError.php ├── GarbageCollect.php ├── Image.php ├── ImageColor.php ├── LICENSE ├── Makefile ├── README.md ├── Source ├── Create.php ├── Data.php ├── File.php ├── Resource.php └── Source.php ├── Utils └── FileUtils.php ├── autoload.php ├── composer.json ├── demo ├── basic.php ├── cache.php ├── cacheCreate.php ├── cacheName.php ├── crop.php ├── data.php ├── fallback.php ├── fonts │ └── CaviarDreams.ttf ├── gc.php ├── get.php ├── guess.php ├── img │ ├── mona.jpg │ ├── test.png │ ├── test2.jpg │ └── vinci.png ├── inline.php ├── merge.php ├── percent.php ├── resource.php ├── watermark.php └── write.php ├── doc ├── cropResize.jpg ├── forceResize.jpg ├── generate.php ├── mona.jpg ├── resize.jpg ├── scaleResize.jpg ├── zoomCrop.jpg └── zoomCropTop.jpg ├── images └── error.jpg ├── phpunit.xml.dist └── tests ├── .gitignore ├── ImageTests.php ├── bootstrap.php └── files ├── monalisa.gif ├── monalisa.jpg └── monalisa.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [composer.json] 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [Makefile] 12 | indent_style = tab 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: PHP Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | build: 8 | strategy: 9 | matrix: 10 | operating-system: [ubuntu-22.04] 11 | php-versions: ['7.3', '7.4', '8.0', '8.1'] 12 | fail-fast: false 13 | runs-on: ${{ matrix.operating-system }} 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php-versions }} 21 | 22 | - name: Validate composer.json and composer.lock 23 | run: composer validate 24 | 25 | - name: Install dependencies 26 | run: composer install --prefer-dist --no-progress --no-suggest 27 | 28 | - name: Run test suite 29 | run: composer run-script test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | demo/cache 2 | demo/out.jpg 3 | **.swp 4 | vendor 5 | composer.lock 6 | /phpunit.xml 7 | /.php_cs.cache 8 | .idea 9 | .phpunit.result.cache 10 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setUsingCache(true) 9 | ; 10 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: symfony 2 | 3 | enabled: 4 | - align_double_arrow 5 | - newline_after_open_tag 6 | - ordered_use 7 | - long_array_syntax 8 | - php_unit_construct 9 | - php_unit_strict 10 | 11 | disabled: 12 | - unalign_double_arrow 13 | - unalign_equals 14 | -------------------------------------------------------------------------------- /Adapter/Adapter.php: -------------------------------------------------------------------------------- 1 | source = $source; 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function getResource() 40 | { 41 | return $this->resource; 42 | } 43 | 44 | /** 45 | * Does this adapter supports the given type ? 46 | */ 47 | protected function supports($type) 48 | { 49 | return false; 50 | } 51 | 52 | /** 53 | * Converts the image to true color. 54 | */ 55 | protected function convertToTrueColor() 56 | { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Adapter/AdapterInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface AdapterInterface 16 | { 17 | /** 18 | * set the image source for the adapter. 19 | * 20 | * @param Source $source 21 | * 22 | * @return $this 23 | */ 24 | public function setSource(Source $source); 25 | 26 | /** 27 | * get the raw resource. 28 | * 29 | * @return resource 30 | */ 31 | public function getResource(); 32 | 33 | /** 34 | * Gets the name of the adapter. 35 | * 36 | * @return string 37 | */ 38 | public function getName(); 39 | 40 | /** 41 | * Image width. 42 | * 43 | * @return int 44 | */ 45 | public function width(); 46 | 47 | /** 48 | * Image height. 49 | * 50 | * @return int 51 | */ 52 | public function height(); 53 | 54 | /** 55 | * Init the resource. 56 | * 57 | * @return $this 58 | */ 59 | public function init(); 60 | 61 | /** 62 | * Unload the resource 63 | */ 64 | public function deinit(); 65 | 66 | /** 67 | * Save the image as a gif. 68 | * 69 | * @return $this 70 | */ 71 | public function saveGif($file); 72 | 73 | /** 74 | * Save the image as a png. 75 | * 76 | * @return $this 77 | */ 78 | public function savePng($file); 79 | 80 | /** 81 | * Save the image as a Webp. 82 | * 83 | * @return $this 84 | */ 85 | public function saveWebp($file, $quality); 86 | 87 | /** 88 | * Save the image as a jpeg. 89 | * 90 | * @return $this 91 | */ 92 | public function saveJpeg($file, $quality); 93 | 94 | /** 95 | * Works as resize() excepts that the layout will be cropped. 96 | * 97 | * @param int $width the width 98 | * @param int $height the height 99 | * @param int $background the background 100 | * 101 | * @return $this 102 | */ 103 | public function cropResize($width = null, $height = null, $background = 0xffffff); 104 | 105 | /** 106 | * Resize the image preserving scale. Can enlarge it. 107 | * 108 | * @param int $width the width 109 | * @param int $height the height 110 | * @param int $background the background 111 | * @param bool $crop 112 | * 113 | * @return $this 114 | */ 115 | public function scaleResize($width = null, $height = null, $background = 0xffffff, $crop = false); 116 | 117 | /** 118 | * Resizes the image. It will never be enlarged. 119 | * 120 | * @param int $width the width 121 | * @param int $height the height 122 | * @param int $background the background 123 | * @param bool $force 124 | * @param bool $rescale 125 | * @param bool $crop 126 | * 127 | * @return $this 128 | */ 129 | public function resize($width = null, $height = null, $background = 0xffffff, $force = false, $rescale = false, $crop = false); 130 | 131 | /** 132 | * Crops the image. 133 | * 134 | * @param int $x the top-left x position of the crop box 135 | * @param int $y the top-left y position of the crop box 136 | * @param int $width the width of the crop box 137 | * @param int $height the height of the crop box 138 | * 139 | * @return $this 140 | */ 141 | public function crop($x, $y, $width, $height); 142 | 143 | /** 144 | * enable progressive image loading. 145 | * 146 | * @return $this 147 | */ 148 | public function enableProgressive(); 149 | 150 | /** 151 | * Resizes the image forcing the destination to have exactly the 152 | * given width and the height. 153 | * 154 | * @param int $width the width 155 | * @param int $height the height 156 | * @param int $background the background 157 | * 158 | * @return $this 159 | */ 160 | public function forceResize($width = null, $height = null, $background = 0xffffff); 161 | 162 | /** 163 | * Perform a zoom crop of the image to desired width and height. 164 | * 165 | * @param int $width Desired width 166 | * @param int $height Desired height 167 | * @param int $background 168 | * 169 | * @return $this 170 | */ 171 | public function zoomCrop($width, $height, $background = 0xffffff); 172 | 173 | /** 174 | * Fills the image background to $bg if the image is transparent. 175 | * 176 | * @param int $background background color 177 | * 178 | * @return $this 179 | */ 180 | public function fillBackground($background = 0xffffff); 181 | 182 | /** 183 | * Negates the image. 184 | * 185 | * @return $this 186 | */ 187 | public function negate(); 188 | 189 | /** 190 | * Changes the brightness of the image. 191 | * 192 | * @param int $brightness the brightness 193 | * 194 | * @return $this 195 | */ 196 | public function brightness($brightness); 197 | 198 | /** 199 | * Contrasts the image. 200 | * 201 | * @param int $contrast the contrast [-100, 100] 202 | * 203 | * @return $this 204 | */ 205 | public function contrast($contrast); 206 | 207 | /** 208 | * Apply a grayscale level effect on the image. 209 | * 210 | * @return $this 211 | */ 212 | public function grayscale(); 213 | 214 | /** 215 | * Emboss the image. 216 | * 217 | * @return $this 218 | */ 219 | public function emboss(); 220 | 221 | /** 222 | * Smooth the image. 223 | * 224 | * @param int $p value between [-10,10] 225 | * 226 | * @return $this 227 | */ 228 | public function smooth($p); 229 | 230 | /** 231 | * Sharps the image. 232 | * 233 | * @return $this 234 | */ 235 | public function sharp(); 236 | 237 | /** 238 | * Edges the image. 239 | * 240 | * @return $this 241 | */ 242 | public function edge(); 243 | 244 | /** 245 | * Colorize the image. 246 | * 247 | * @param int $red value in range [-255, 255] 248 | * @param int $green value in range [-255, 255] 249 | * @param int $blue value in range [-255, 255] 250 | * 251 | * @return $this 252 | */ 253 | public function colorize($red, $green, $blue); 254 | 255 | /** 256 | * apply sepia to the image. 257 | * 258 | * @return $this 259 | */ 260 | public function sepia(); 261 | 262 | /** 263 | * Merge with another image. 264 | * 265 | * @param Image $other 266 | * @param int $x 267 | * @param int $y 268 | * @param int $width 269 | * @param int $height 270 | * 271 | * @return $this 272 | */ 273 | public function merge(Image $other, $x = 0, $y = 0, $width = null, $height = null); 274 | 275 | /** 276 | * Rotate the image. 277 | * 278 | * @param float $angle 279 | * @param int $background 280 | * 281 | * @return $this 282 | */ 283 | public function rotate($angle, $background = 0xffffff); 284 | 285 | /** 286 | * Fills the image. 287 | * 288 | * @param int $color 289 | * @param int $x 290 | * @param int $y 291 | * 292 | * @return $this 293 | */ 294 | public function fill($color = 0xffffff, $x = 0, $y = 0); 295 | 296 | /** 297 | * write text to the image. 298 | * 299 | * @param string $font 300 | * @param string $text 301 | * @param int $x 302 | * @param int $y 303 | * @param int $size 304 | * @param int $angle 305 | * @param int $color 306 | * @param string $align 307 | */ 308 | public function write($font, $text, $x = 0, $y = 0, $size = 12, $angle = 0, $color = 0x000000, $align = 'left'); 309 | 310 | /** 311 | * Draws a rectangle. 312 | * 313 | * @param int $x1 314 | * @param int $y1 315 | * @param int $x2 316 | * @param int $y2 317 | * @param int $color 318 | * @param bool $filled 319 | * 320 | * @return $this 321 | */ 322 | public function rectangle($x1, $y1, $x2, $y2, $color, $filled = false); 323 | 324 | /** 325 | * Draws a rounded rectangle. 326 | * 327 | * @param int $x1 328 | * @param int $y1 329 | * @param int $x2 330 | * @param int $y2 331 | * @param int $radius 332 | * @param int $color 333 | * @param bool $filled 334 | * 335 | * @return $this 336 | */ 337 | public function roundedRectangle($x1, $y1, $x2, $y2, $radius, $color, $filled = false); 338 | 339 | /** 340 | * Draws a line. 341 | * 342 | * @param int $x1 343 | * @param int $y1 344 | * @param int $x2 345 | * @param int $y2 346 | * @param int $color 347 | * 348 | * @return $this 349 | */ 350 | public function line($x1, $y1, $x2, $y2, $color = 0x000000); 351 | 352 | /** 353 | * Draws an ellipse. 354 | * 355 | * @param int $cx 356 | * @param int $cy 357 | * @param int $width 358 | * @param int $height 359 | * @param int $color 360 | * @param bool $filled 361 | * 362 | * @return $this 363 | */ 364 | public function ellipse($cx, $cy, $width, $height, $color = 0x000000, $filled = false); 365 | 366 | /** 367 | * Draws a circle. 368 | * 369 | * @param int $cx 370 | * @param int $cy 371 | * @param int $r 372 | * @param int $color 373 | * @param bool $filled 374 | * 375 | * @return $this 376 | */ 377 | public function circle($cx, $cy, $r, $color = 0x000000, $filled = false); 378 | 379 | /** 380 | * Draws a polygon. 381 | * 382 | * @param array $points 383 | * @param int $color 384 | * @param bool $filled 385 | * 386 | * @return $this 387 | */ 388 | public function polygon(array $points, $color, $filled = false); 389 | 390 | /** 391 | * Flips the image. 392 | * 393 | * @param int $flipVertical 394 | * @param int $flipHorizontal 395 | * 396 | * @return $this 397 | */ 398 | public function flip($flipVertical, $flipHorizontal); 399 | } 400 | -------------------------------------------------------------------------------- /Adapter/Common.php: -------------------------------------------------------------------------------- 1 | width(); 13 | $originalHeight = $this->height(); 14 | 15 | // Calculate the different ratios 16 | $originalRatio = $originalWidth / $originalHeight; 17 | $newRatio = $width / $height; 18 | 19 | // Compare ratios 20 | if ($originalRatio > $newRatio) { 21 | // Original image is wider 22 | $newHeight = $height; 23 | $newWidth = (int) $height * $originalRatio; 24 | } else { 25 | // Equal width or smaller 26 | $newHeight = (int) $width / $originalRatio; 27 | $newWidth = $width; 28 | } 29 | 30 | // Perform resize 31 | $this->resize($newWidth, $newHeight, $background, true); 32 | 33 | // Define x position 34 | switch ($xPosLetter) { 35 | case 'L': 36 | case 'left': 37 | $xPos = 0; 38 | break; 39 | case 'R': 40 | case 'right': 41 | $xPos = (int) $newWidth - $width; 42 | break; 43 | case 'center': 44 | $xPos = (int) ($newWidth - $width) / 2; 45 | break; 46 | default: 47 | $factorW = $newWidth / $originalWidth; 48 | $xPos = $xPosLetter * $factorW; 49 | 50 | // If the desired cropping position goes beyond the width then 51 | // set the crop to be within the correct bounds. 52 | if ($xPos + $width > $newWidth) { 53 | $xPos = (int) $newWidth - $width; 54 | } 55 | } 56 | 57 | // Define y position 58 | switch ($yPosLetter) { 59 | case 'T': 60 | case 'top': 61 | $yPos = 0; 62 | break; 63 | case 'B': 64 | case 'bottom': 65 | $yPos = (int) $newHeight - $height; 66 | break; 67 | case 'center': 68 | $yPos = (int) ($newHeight - $height) / 2; 69 | break; 70 | default: 71 | $factorH = $newHeight / $originalHeight; 72 | $yPos = $yPosLetter * $factorH; 73 | 74 | // If the desired cropping position goes beyond the height then 75 | // set the crop to be within the correct bounds. 76 | if ($yPos + $height > $newHeight) { 77 | $yPos = (int) $newHeight - $height; 78 | } 79 | } 80 | 81 | // Crop image to reach desired size 82 | $this->crop($xPos, $yPos, $width, $height); 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Resizes the image forcing the destination to have exactly the 89 | * given width and the height. 90 | * 91 | * @param int $w the width 92 | * @param int $h the height 93 | * @param int $bg the background 94 | */ 95 | public function forceResize($width = null, $height = null, $background = 'transparent') 96 | { 97 | return $this->resize($width, $height, $background, true); 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function scaleResize($width = null, $height = null, $background = 'transparent', $crop = false) 104 | { 105 | return $this->resize($width, $height, $background, false, true, $crop); 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function cropResize($width = null, $height = null, $background = 'transparent') 112 | { 113 | return $this->resize($width, $height, $background, false, false, true); 114 | } 115 | 116 | /** 117 | * Read exif rotation from file and apply it. 118 | */ 119 | public function fixOrientation() 120 | { 121 | if (!in_array(exif_imagetype($this->source->getInfos()), array( 122 | IMAGETYPE_JPEG, 123 | IMAGETYPE_TIFF_II, 124 | IMAGETYPE_TIFF_MM, 125 | ))) { 126 | return $this; 127 | } 128 | 129 | if (!extension_loaded('exif')) { 130 | throw new \RuntimeException('You need to EXIF PHP Extension to use this function'); 131 | } 132 | 133 | $exif = @exif_read_data($this->source->getInfos()); 134 | 135 | if ($exif === false || !array_key_exists('Orientation', $exif)) { 136 | return $this; 137 | } 138 | 139 | return $this->applyExifOrientation($exif['Orientation']); 140 | } 141 | 142 | /** 143 | * Apply orientation using Exif orientation value. 144 | */ 145 | public function applyExifOrientation($exif_orienation) 146 | { 147 | switch ($exif_orienation) { 148 | case 1: 149 | break; 150 | 151 | case 2: 152 | $this->flip(false, true); 153 | break; 154 | 155 | case 3: // 180 rotate left 156 | $this->rotate(180); 157 | break; 158 | 159 | case 4: // vertical flip 160 | $this->flip(true, false); 161 | break; 162 | 163 | case 5: // vertical flip + 90 rotate right 164 | $this->flip(true, false); 165 | $this->rotate(-90); 166 | break; 167 | 168 | case 6: // 90 rotate right 169 | $this->rotate(-90); 170 | break; 171 | 172 | case 7: // horizontal flip + 90 rotate right 173 | $this->flip(false, true); 174 | $this->rotate(-90); 175 | break; 176 | 177 | case 8: // 90 rotate left 178 | $this->rotate(90); 179 | break; 180 | } 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Opens the image. 187 | */ 188 | abstract protected function openGif($file); 189 | 190 | abstract protected function openJpeg($file); 191 | 192 | abstract protected function openPng($file); 193 | 194 | abstract protected function openWebp($file); 195 | 196 | /** 197 | * Creates an image. 198 | */ 199 | abstract protected function createImage($width, $height); 200 | 201 | /** 202 | * Creating an image using $data. 203 | */ 204 | abstract protected function createImageFromData($data); 205 | 206 | /** 207 | * Loading image from $resource. 208 | */ 209 | protected function loadResource($resource) 210 | { 211 | $this->resource = $resource; 212 | } 213 | 214 | protected function loadFile($file, $type) 215 | { 216 | if (!$this->supports($type)) { 217 | throw new \RuntimeException('Type '.$type.' is not supported by GD'); 218 | } 219 | 220 | if ($type == 'jpeg') { 221 | $this->openJpeg($file); 222 | } 223 | 224 | if ($type == 'gif') { 225 | $this->openGif($file); 226 | } 227 | 228 | if ($type == 'png') { 229 | $this->openPng($file); 230 | } 231 | 232 | if ($type == 'webp') { 233 | $this->openWebp($file); 234 | } 235 | 236 | if (false === $this->resource) { 237 | throw new \UnexpectedValueException('Unable to open file ('.$file.')'); 238 | } else { 239 | $this->convertToTrueColor(); 240 | } 241 | } 242 | 243 | /** 244 | * {@inheritdoc} 245 | */ 246 | public function init() 247 | { 248 | $source = $this->source; 249 | 250 | if ($source instanceof \Gregwar\Image\Source\File) { 251 | $this->loadFile($source->getFile(), $source->guessType()); 252 | } elseif ($source instanceof \Gregwar\Image\Source\Create) { 253 | $this->createImage($source->getWidth(), $source->getHeight()); 254 | } elseif ($source instanceof \Gregwar\Image\Source\Data) { 255 | $this->createImageFromData($source->getData()); 256 | } elseif ($source instanceof \Gregwar\Image\Source\Resource) { 257 | $this->loadResource($source->getResource()); 258 | } else { 259 | throw new \Exception('Unsupported image source type '.get_class($source)); 260 | } 261 | 262 | return $this; 263 | } 264 | 265 | /** 266 | * {@inheritdoc} 267 | */ 268 | public function deinit() 269 | { 270 | $this->resource = null; 271 | } 272 | 273 | /** 274 | * {@inheritdoc} 275 | */ 276 | public function resize($width = null, $height = null, $background = 'transparent', $force = false, $rescale = false, $crop = false) 277 | { 278 | $current_width = $this->width(); 279 | $current_height = $this->height(); 280 | $new_width = 0; 281 | $new_height = 0; 282 | $scale = 1.0; 283 | 284 | if ($height === null && preg_match('#^(.+)%$#mUsi', $width, $matches)) { 285 | $width = round($current_width * ((float) $matches[1] / 100.0)); 286 | $height = round($current_height * ((float) $matches[1] / 100.0)); 287 | } 288 | 289 | if (!$rescale && (!$force || $crop)) { 290 | if ($width != null && $current_width > $width) { 291 | $scale = $current_width / $width; 292 | } 293 | 294 | if ($height != null && $current_height > $height) { 295 | if ($current_height / $height > $scale) { 296 | $scale = $current_height / $height; 297 | } 298 | } 299 | } else { 300 | if ($width != null) { 301 | $scale = $current_width / $width; 302 | $new_width = $width; 303 | } 304 | 305 | if ($height != null) { 306 | if ($width != null && $rescale) { 307 | $scale = max($scale, $current_height / $height); 308 | } else { 309 | $scale = $current_height / $height; 310 | } 311 | $new_height = $height; 312 | } 313 | } 314 | 315 | if (!$force || $width == null || $rescale) { 316 | $new_width = round($current_width / $scale); 317 | } 318 | 319 | if (!$force || $height == null || $rescale) { 320 | $new_height = round($current_height / $scale); 321 | } 322 | 323 | if ($width == null || $crop) { 324 | $width = $new_width; 325 | } 326 | 327 | if ($height == null || $crop) { 328 | $height = $new_height; 329 | } 330 | 331 | $this->doResize($background, (int) $width, (int) $height, (int) $new_width, (int) $new_height); 332 | } 333 | 334 | /** 335 | * Trim background color arround the image. 336 | * 337 | * @param int $bg the background 338 | */ 339 | protected function _trimColor($background = 'transparent') 340 | { 341 | $width = $this->width(); 342 | $height = $this->height(); 343 | 344 | $b_top = 0; 345 | $b_lft = 0; 346 | $b_btm = $height - 1; 347 | $b_rt = $width - 1; 348 | 349 | //top 350 | for (; $b_top < $height; ++$b_top) { 351 | for ($x = 0; $x < $width; ++$x) { 352 | if ($this->getColor($x, $b_top) != $background) { 353 | break 2; 354 | } 355 | } 356 | } 357 | 358 | // bottom 359 | for (; $b_btm >= 0; --$b_btm) { 360 | for ($x = 0; $x < $width; ++$x) { 361 | if ($this->getColor($x, $b_btm) != $background) { 362 | break 2; 363 | } 364 | } 365 | } 366 | 367 | // left 368 | for (; $b_lft < $width; ++$b_lft) { 369 | for ($y = $b_top; $y <= $b_btm; ++$y) { 370 | if ($this->getColor($b_lft, $y) != $background) { 371 | break 2; 372 | } 373 | } 374 | } 375 | 376 | // right 377 | for (; $b_rt >= 0; --$b_rt) { 378 | for ($y = $b_top; $y <= $b_btm; ++$y) { 379 | if ($this->getColor($b_rt, $y) != $background) { 380 | break 2; 381 | } 382 | } 383 | } 384 | 385 | ++$b_btm; 386 | ++$b_rt; 387 | 388 | $this->crop($b_lft, $b_top, $b_rt - $b_lft, $b_btm - $b_top); 389 | } 390 | 391 | /** 392 | * Resizes the image to an image having size of $target_width, $target_height, using 393 | * $new_width and $new_height and padding with $bg color. 394 | */ 395 | abstract protected function doResize($bg, int $target_width, int $target_height, int $new_width, int $new_height); 396 | 397 | /** 398 | * Gets the color of the $x, $y pixel. 399 | */ 400 | abstract protected function getColor($x, $y); 401 | 402 | /** 403 | * {@inheritdoc} 404 | */ 405 | public function enableProgressive() 406 | { 407 | throw new \Exception('The Adapter '.$this->getName().' does not support Progressive Image loading'); 408 | } 409 | 410 | /** 411 | * This does nothing, but can be used to tag a ressource for instance (having a final image hash 412 | * for the cache different depending on the tag) 413 | */ 414 | public function tag($tag) 415 | { 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /Adapter/GD.php: -------------------------------------------------------------------------------- 1 | \IMG_JPG, 13 | 'gif' => \IMG_GIF, 14 | 'png' => \IMG_PNG, 15 | 'webp' => \IMG_WEBP 16 | ); 17 | 18 | protected function loadResource($resource) 19 | { 20 | parent::loadResource($resource); 21 | imagesavealpha($this->resource, true); 22 | } 23 | 24 | /** 25 | * Gets the width and the height for writing some text. 26 | */ 27 | public static function TTFBox($font, $text, $size, $angle = 0) 28 | { 29 | $box = imagettfbbox($size, $angle, $font, $text); 30 | 31 | return array( 32 | 'width' => abs($box[2] - $box[0]), 33 | 'height' => abs($box[3] - $box[5]), 34 | ); 35 | } 36 | 37 | public function __construct() 38 | { 39 | parent::__construct(); 40 | 41 | if (!(extension_loaded('gd') && function_exists('gd_info'))) { 42 | throw new \RuntimeException('You need to install GD PHP Extension to use this library'); 43 | } 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getName() 50 | { 51 | return 'GD'; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function fillBackground($background = 0xffffff) 58 | { 59 | $w = $this->width(); 60 | $h = $this->height(); 61 | $n = imagecreatetruecolor($w, $h); 62 | imagefill($n, 0, 0, ImageColor::gdAllocate($this->resource, $background)); 63 | imagecopyresampled($n, $this->resource, 0, 0, 0, 0, $w, $h, $w, $h); 64 | imagedestroy($this->resource); 65 | $this->resource = $n; 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Do the image resize. 72 | * 73 | * @return $this 74 | */ 75 | protected function doResize($bg, int $target_width, int $target_height, int $new_width, int $new_height) 76 | { 77 | $width = $this->width(); 78 | $height = $this->height(); 79 | $n = imagecreatetruecolor($target_width, $target_height); 80 | 81 | if ($bg != 'transparent') { 82 | imagefill($n, 0, 0, ImageColor::gdAllocate($this->resource, $bg)); 83 | } else { 84 | imagealphablending($n, false); 85 | $color = ImageColor::gdAllocate($this->resource, 'transparent'); 86 | 87 | imagefill($n, 0, 0, $color); 88 | imagesavealpha($n, true); 89 | } 90 | 91 | imagecopyresampled( 92 | $n, 93 | $this->resource, 94 | (int) (($target_width - $new_width) / 2), 95 | (int) (($target_height - $new_height) / 2), 96 | 0, 97 | 0, 98 | $new_width, 99 | $new_height, 100 | $width, 101 | $height 102 | ); 103 | 104 | imagedestroy($this->resource); 105 | 106 | $this->resource = $n; 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | public function crop($x, $y, $width, $height) 115 | { 116 | $destination = imagecreatetruecolor($width, $height); 117 | imagealphablending($destination, false); 118 | imagesavealpha($destination, true); 119 | imagecopy($destination, $this->resource, 0, 0, (int) $x, (int) $y, $this->width(), $this->height()); 120 | imagedestroy($this->resource); 121 | $this->resource = $destination; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function negate() 130 | { 131 | imagefilter($this->resource, IMG_FILTER_NEGATE); 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * {@inheritdoc} 138 | */ 139 | public function brightness($brightness) 140 | { 141 | imagefilter($this->resource, IMG_FILTER_BRIGHTNESS, $brightness); 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * {@inheritdoc} 148 | */ 149 | public function contrast($contrast) 150 | { 151 | imagefilter($this->resource, IMG_FILTER_CONTRAST, $contrast); 152 | 153 | return $this; 154 | } 155 | 156 | /** 157 | * {@inheritdoc} 158 | */ 159 | public function grayscale() 160 | { 161 | imagefilter($this->resource, IMG_FILTER_GRAYSCALE); 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * {@inheritdoc} 168 | */ 169 | public function emboss() 170 | { 171 | imagefilter($this->resource, IMG_FILTER_EMBOSS); 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * {@inheritdoc} 178 | */ 179 | public function smooth($p) 180 | { 181 | imagefilter($this->resource, IMG_FILTER_SMOOTH, $p); 182 | 183 | return $this; 184 | } 185 | 186 | /** 187 | * {@inheritdoc} 188 | */ 189 | public function sharp() 190 | { 191 | imagefilter($this->resource, IMG_FILTER_MEAN_REMOVAL); 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * {@inheritdoc} 198 | */ 199 | public function edge() 200 | { 201 | imagefilter($this->resource, IMG_FILTER_EDGEDETECT); 202 | 203 | return $this; 204 | } 205 | 206 | /** 207 | * {@inheritdoc} 208 | */ 209 | public function colorize($red, $green, $blue) 210 | { 211 | imagefilter($this->resource, IMG_FILTER_COLORIZE, $red, $green, $blue); 212 | 213 | return $this; 214 | } 215 | 216 | /** 217 | * {@inheritdoc} 218 | */ 219 | public function sepia() 220 | { 221 | imagefilter($this->resource, IMG_FILTER_GRAYSCALE); 222 | imagefilter($this->resource, IMG_FILTER_COLORIZE, 100, 50, 0); 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * {@inheritdoc} 229 | */ 230 | public function gaussianBlur($blurFactor = 1) 231 | { 232 | $blurFactor = round($blurFactor); // blurFactor has to be an integer 233 | 234 | $originalWidth = $this->width(); 235 | $originalHeight = $this->height(); 236 | 237 | $smallestWidth = ceil($originalWidth * pow(0.5, $blurFactor)); 238 | $smallestHeight = ceil($originalHeight * pow(0.5, $blurFactor)); 239 | 240 | // for the first run, the previous image is the original input 241 | $prevImage = $this->resource; 242 | $prevWidth = $originalWidth; 243 | $prevHeight = $originalHeight; 244 | 245 | // scale way down and gradually scale back up, blurring all the way 246 | for ($i = 0; $i < $blurFactor; ++$i) { 247 | // determine dimensions of next image 248 | $nextWidth = $smallestWidth * pow(2, $i); 249 | $nextHeight = $smallestHeight * pow(2, $i); 250 | 251 | // resize previous image to next size 252 | $nextImage = imagecreatetruecolor($nextWidth, $nextHeight); 253 | imagecopyresized($nextImage, $prevImage, 0, 0, 0, 0, 254 | $nextWidth, $nextHeight, $prevWidth, $prevHeight); 255 | 256 | // apply blur filter 257 | imagefilter($nextImage, IMG_FILTER_GAUSSIAN_BLUR); 258 | 259 | // now the new image becomes the previous image for the next step 260 | $prevImage = $nextImage; 261 | $prevWidth = $nextWidth; 262 | $prevHeight = $nextHeight; 263 | } 264 | 265 | // scale back to original size and blur one more time 266 | imagecopyresized($this->resource, $nextImage, 267 | 0, 0, 0, 0, $originalWidth, $originalHeight, $nextWidth, $nextHeight); 268 | imagefilter($this->resource, IMG_FILTER_GAUSSIAN_BLUR); 269 | 270 | // clean up 271 | imagedestroy($prevImage); 272 | 273 | return $this; 274 | } 275 | 276 | /** 277 | * {@inheritdoc} 278 | */ 279 | public function merge(Image $other, $x = 0, $y = 0, $width = null, $height = null) 280 | { 281 | $other = clone $other; 282 | $other->init(); 283 | $other->applyOperations(); 284 | 285 | imagealphablending($this->resource, true); 286 | 287 | if (null == $width) { 288 | $width = $other->width(); 289 | } 290 | 291 | if (null == $height) { 292 | $height = $other->height(); 293 | } 294 | 295 | imagecopyresampled($this->resource, $other->getAdapter()->getResource(), $x, $y, 0, 0, $width, $height, $width, $height); 296 | 297 | return $this; 298 | } 299 | 300 | /** 301 | * {@inheritdoc} 302 | */ 303 | public function rotate($angle, $background = 0xffffff) 304 | { 305 | $this->resource = imagerotate($this->resource, $angle, ImageColor::gdAllocate($this->resource, $background)); 306 | imagealphablending($this->resource, true); 307 | imagesavealpha($this->resource, true); 308 | 309 | return $this; 310 | } 311 | 312 | /** 313 | * {@inheritdoc} 314 | */ 315 | public function fill($color = 0xffffff, $x = 0, $y = 0) 316 | { 317 | imagealphablending($this->resource, false); 318 | imagefill($this->resource, $x, $y, ImageColor::gdAllocate($this->resource, $color)); 319 | 320 | return $this; 321 | } 322 | 323 | /** 324 | * {@inheritdoc} 325 | */ 326 | public function write($font, $text, $x = 0, $y = 0, $size = 12, $angle = 0, $color = 0x000000, $align = 'left') 327 | { 328 | imagealphablending($this->resource, true); 329 | 330 | if ($align != 'left') { 331 | $sim_size = self::TTFBox($font, $text, $size, $angle); 332 | 333 | if ($align == 'center') { 334 | $x -= $sim_size['width'] / 2; 335 | } 336 | 337 | if ($align == 'right') { 338 | $x -= $sim_size['width']; 339 | } 340 | } 341 | 342 | imagettftext($this->resource, $size, $angle, $x, $y, ImageColor::gdAllocate($this->resource, $color), $font, $text); 343 | 344 | return $this; 345 | } 346 | 347 | /** 348 | * {@inheritdoc} 349 | */ 350 | public function rectangle($x1, $y1, $x2, $y2, $color, $filled = false) 351 | { 352 | if ($filled) { 353 | imagefilledrectangle($this->resource, $x1, $y1, $x2, $y2, ImageColor::gdAllocate($this->resource, $color)); 354 | } else { 355 | imagerectangle($this->resource, $x1, $y1, $x2, $y2, ImageColor::gdAllocate($this->resource, $color)); 356 | } 357 | 358 | return $this; 359 | } 360 | 361 | /** 362 | * {@inheritdoc} 363 | */ 364 | public function roundedRectangle($x1, $y1, $x2, $y2, $radius, $color, $filled = false) 365 | { 366 | if ($color) { 367 | $color = ImageColor::gdAllocate($this->resource, $color); 368 | } 369 | 370 | if ($filled == true) { 371 | imagefilledrectangle($this->resource, $x1 + $radius, $y1, $x2 - $radius, $y2, $color); 372 | imagefilledrectangle($this->resource, $x1, $y1 + $radius, $x1 + $radius - 1, $y2 - $radius, $color); 373 | imagefilledrectangle($this->resource, $x2 - $radius + 1, $y1 + $radius, $x2, $y2 - $radius, $color); 374 | 375 | imagefilledarc($this->resource, $x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $color, IMG_ARC_PIE); 376 | imagefilledarc($this->resource, $x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $color, IMG_ARC_PIE); 377 | imagefilledarc($this->resource, $x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $color, IMG_ARC_PIE); 378 | imagefilledarc($this->resource, $x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $color, IMG_ARC_PIE); 379 | } else { 380 | imageline($this->resource, $x1 + $radius, $y1, $x2 - $radius, $y1, $color); 381 | imageline($this->resource, $x1 + $radius, $y2, $x2 - $radius, $y2, $color); 382 | imageline($this->resource, $x1, $y1 + $radius, $x1, $y2 - $radius, $color); 383 | imageline($this->resource, $x2, $y1 + $radius, $x2, $y2 - $radius, $color); 384 | 385 | imagearc($this->resource, $x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $color); 386 | imagearc($this->resource, $x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $color); 387 | imagearc($this->resource, $x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $color); 388 | imagearc($this->resource, $x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $color); 389 | } 390 | 391 | return $this; 392 | } 393 | 394 | /** 395 | * {@inheritdoc} 396 | */ 397 | public function line($x1, $y1, $x2, $y2, $color = 0x000000) 398 | { 399 | imageline($this->resource, $x1, $y1, $x2, $y2, ImageColor::gdAllocate($this->resource, $color)); 400 | 401 | return $this; 402 | } 403 | 404 | /** 405 | * {@inheritdoc} 406 | */ 407 | public function ellipse($cx, $cy, $width, $height, $color = 0x000000, $filled = false) 408 | { 409 | if ($filled) { 410 | imagefilledellipse($this->resource, $cx, $cy, $width, $height, ImageColor::gdAllocate($this->resource, $color)); 411 | } else { 412 | imageellipse($this->resource, $cx, $cy, $width, $height, ImageColor::gdAllocate($this->resource, $color)); 413 | } 414 | 415 | return $this; 416 | } 417 | 418 | /** 419 | * {@inheritdoc} 420 | */ 421 | public function circle($cx, $cy, $r, $color = 0x000000, $filled = false) 422 | { 423 | return $this->ellipse($cx, $cy, $r, $r, ImageColor::gdAllocate($this->resource, $color), $filled); 424 | } 425 | 426 | /** 427 | * {@inheritdoc} 428 | */ 429 | public function polygon(array $points, $color, $filled = false) 430 | { 431 | if ($filled) { 432 | imagefilledpolygon($this->resource, $points, count($points) / 2, ImageColor::gdAllocate($this->resource, $color)); 433 | } else { 434 | imagepolygon($this->resource, $points, count($points) / 2, ImageColor::gdAllocate($this->resource, $color)); 435 | } 436 | 437 | return $this; 438 | } 439 | 440 | /** 441 | * {@inheritdoc} 442 | */ 443 | public function flip($flipVertical, $flipHorizontal) 444 | { 445 | if (!$flipVertical && !$flipHorizontal) { 446 | return $this; 447 | } 448 | 449 | if (function_exists('imageflip')) { 450 | if ($flipVertical && $flipHorizontal) { 451 | $flipMode = \IMG_FLIP_BOTH; 452 | } elseif ($flipVertical && !$flipHorizontal) { 453 | $flipMode = \IMG_FLIP_VERTICAL; 454 | } elseif (!$flipVertical && $flipHorizontal) { 455 | $flipMode = \IMG_FLIP_HORIZONTAL; 456 | } 457 | 458 | imageflip($this->resource, $flipMode); 459 | } else { 460 | $width = $this->width(); 461 | $height = $this->height(); 462 | 463 | $src_x = 0; 464 | $src_y = 0; 465 | $src_width = $width; 466 | $src_height = $height; 467 | 468 | if ($flipVertical) { 469 | $src_y = $height - 1; 470 | $src_height = -$height; 471 | } 472 | 473 | if ($flipHorizontal) { 474 | $src_x = $width - 1; 475 | $src_width = -$width; 476 | } 477 | 478 | $imgdest = imagecreatetruecolor($width, $height); 479 | imagealphablending($imgdest, false); 480 | imagesavealpha($imgdest, true); 481 | 482 | if (imagecopyresampled($imgdest, $this->resource, 0, 0, $src_x, $src_y, $width, $height, $src_width, $src_height)) { 483 | imagedestroy($this->resource); 484 | $this->resource = $imgdest; 485 | } 486 | } 487 | 488 | return $this; 489 | } 490 | 491 | /** 492 | * {@inheritdoc} 493 | */ 494 | public function width() 495 | { 496 | if (null === $this->resource) { 497 | $this->init(); 498 | } 499 | 500 | return imagesx($this->resource); 501 | } 502 | 503 | /** 504 | * {@inheritdoc} 505 | */ 506 | public function height() 507 | { 508 | if (null === $this->resource) { 509 | $this->init(); 510 | } 511 | 512 | return imagesy($this->resource); 513 | } 514 | 515 | protected function createImage($width, $height) 516 | { 517 | $this->resource = imagecreatetruecolor($width, $height); 518 | } 519 | 520 | protected function createImageFromData($data) 521 | { 522 | $this->resource = @imagecreatefromstring($data); 523 | } 524 | 525 | /** 526 | * Converts the image to true color. 527 | */ 528 | protected function convertToTrueColor() 529 | { 530 | if (!imageistruecolor($this->resource)) { 531 | if (function_exists('imagepalettetotruecolor')) { 532 | // Available in PHP 5.5 533 | imagepalettetotruecolor($this->resource); 534 | } else { 535 | $transparentIndex = imagecolortransparent($this->resource); 536 | 537 | $w = $this->width(); 538 | $h = $this->height(); 539 | 540 | $img = imagecreatetruecolor($w, $h); 541 | imagecopy($img, $this->resource, 0, 0, 0, 0, $w, $h); 542 | 543 | if ($transparentIndex != -1) { 544 | $width = $this->width(); 545 | $height = $this->height(); 546 | 547 | imagealphablending($img, false); 548 | imagesavealpha($img, true); 549 | 550 | for ($x = 0; $x < $width; ++$x) { 551 | for ($y = 0; $y < $height; ++$y) { 552 | if (imagecolorat($this->resource, $x, $y) == $transparentIndex) { 553 | imagesetpixel($img, $x, $y, 127 << 24); 554 | } 555 | } 556 | } 557 | } 558 | 559 | $this->resource = $img; 560 | } 561 | } 562 | 563 | imagesavealpha($this->resource, true); 564 | } 565 | 566 | /** 567 | * {@inheritdoc} 568 | */ 569 | public function saveGif($file) 570 | { 571 | $transColor = imagecolorallocatealpha($this->resource, 255, 255, 255, 127); 572 | imagecolortransparent($this->resource, $transColor); 573 | imagegif($this->resource, $file); 574 | 575 | return $this; 576 | } 577 | 578 | /** 579 | * {@inheritdoc} 580 | */ 581 | public function savePng($file) 582 | { 583 | imagepng($this->resource, $file); 584 | 585 | return $this; 586 | } 587 | 588 | /** 589 | * {@inheritdoc} 590 | */ 591 | public function saveWebp($file, $quality) 592 | { 593 | imagewebp($this->resource, $file, $quality); 594 | 595 | return $this; 596 | } 597 | 598 | /** 599 | * {@inheritdoc} 600 | */ 601 | public function saveJpeg($file, $quality) 602 | { 603 | imagejpeg($this->resource, $file, $quality); 604 | 605 | return $this; 606 | } 607 | 608 | /** 609 | * Try to open the file using jpeg. 610 | */ 611 | protected function openJpeg($file) 612 | { 613 | if (FileUtils::safeExists($file) && filesize($file)) { 614 | $this->resource = @imagecreatefromjpeg($file); 615 | } else { 616 | $this->resource = false; 617 | } 618 | } 619 | 620 | /** 621 | * Try to open the file using gif. 622 | */ 623 | protected function openGif($file) 624 | { 625 | if (FileUtils::safeExists($file) && filesize($file)) { 626 | $this->resource = @imagecreatefromgif($file); 627 | } else { 628 | $this->resource = false; 629 | } 630 | } 631 | 632 | /** 633 | * Try to open the file using PNG. 634 | */ 635 | protected function openPng($file) 636 | { 637 | if (FileUtils::safeExists($file) && filesize($file)) { 638 | $this->resource = @imagecreatefrompng($file); 639 | } else { 640 | $this->resource = false; 641 | } 642 | } 643 | 644 | /** 645 | * Try to open the file using WEBP. 646 | */ 647 | protected function openWebp($file) 648 | { 649 | if (FileUtils::safeExists($file) && filesize($file)) { 650 | $this->resource = @imagecreatefromwebp($file); 651 | } else { 652 | $this->resource = false; 653 | } 654 | } 655 | 656 | /** 657 | * Does this adapter supports type ? 658 | */ 659 | protected function supports($type) 660 | { 661 | return imagetypes() & self::$gdTypes[$type]; 662 | } 663 | 664 | protected function getColor($x, $y) 665 | { 666 | return imagecolorat($this->resource, $x, $y); 667 | } 668 | 669 | /** 670 | * {@inheritdoc} 671 | */ 672 | public function enableProgressive() 673 | { 674 | imageinterlace($this->resource, 1); 675 | 676 | return $this; 677 | } 678 | } 679 | -------------------------------------------------------------------------------- /Adapter/Imagick.php: -------------------------------------------------------------------------------- 1 | newNewFile = $newNewFile; 10 | } 11 | 12 | public function getNewFile() 13 | { 14 | return $this->newNewFile; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /GarbageCollect.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class GarbageCollect 12 | { 13 | /** 14 | * Drops old files of a directory. 15 | * 16 | * @param string $directory the name of the target directory 17 | * @param int $days the number of days to consider a file old 18 | * @param bool $verbose enable verbose output 19 | * 20 | * @return true if all the files/directories of a directory was wiped 21 | */ 22 | public static function dropOldFiles($directory, $days = 30, $verbose = false) 23 | { 24 | $allDropped = true; 25 | $now = time(); 26 | 27 | $dir = opendir($directory); 28 | 29 | if (!$dir) { 30 | if ($verbose) { 31 | echo "! Unable to open $directory\n"; 32 | } 33 | 34 | return false; 35 | } 36 | 37 | while ($file = readdir($dir)) { 38 | if ($file == '.' || $file == '..') { 39 | continue; 40 | } 41 | 42 | $fullName = $directory.'/'.$file; 43 | 44 | $old = $now - filemtime($fullName); 45 | 46 | if (is_dir($fullName)) { 47 | // Directories are recursively crawled 48 | if (static::dropOldFiles($fullName, $days, $verbose)) { 49 | self::drop($fullName, $verbose); 50 | } else { 51 | $allDropped = false; 52 | } 53 | } else { 54 | if ($old > (24 * 60 * 60 * $days)) { 55 | self::drop($fullName, $verbose); 56 | } else { 57 | $allDropped = false; 58 | } 59 | } 60 | } 61 | 62 | closedir($dir); 63 | 64 | return $allDropped; 65 | } 66 | 67 | /** 68 | * Drops a file or an empty directory. 69 | */ 70 | public static function drop($file, $verbose = false) 71 | { 72 | if (is_dir($file)) { 73 | @rmdir($file); 74 | } else { 75 | @unlink($file); 76 | } 77 | 78 | if ($verbose) { 79 | echo "> Dropping $file...\n"; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Image.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * @method Image saveGif($file) 15 | * @method Image savePng($file) 16 | * @method Image saveJpeg($file, $quality) 17 | * @method Image resize($width = null, $height = null, $background = 'transparent', $force = false, $rescale = false, $crop = false) 18 | * @method Image forceResize($width = null, $height = null, $background = 'transparent') 19 | * @method Image scaleResize($width = null, $height = null, $background = 'transparent', $crop = false) 20 | * @method Image cropResize($width = null, $height = null, $background=0xffffff) 21 | * @method Image scale($width = null, $height = null, $background=0xffffff, $crop = false) 22 | * @method Image ($width = null, $height = null, $background = 0xffffff, $force = false, $rescale = false, $crop = false) 23 | * @method Image crop($x, $y, $width, $height) 24 | * @method Image enableProgressive() 25 | * @method Image force($width = null, $height = null, $background = 0xffffff) 26 | * @method Image zoomCrop($width, $height, $background = 0xffffff, $xPos, $yPos) 27 | * @method Image fillBackground($background = 0xffffff) 28 | * @method Image negate() 29 | * @method Image brightness($brightness) 30 | * @method Image contrast($contrast) 31 | * @method Image grayscale() 32 | * @method Image emboss() 33 | * @method Image smooth($p) 34 | * @method Image sharp() 35 | * @method Image edge() 36 | * @method Image colorize($red, $green, $blue) 37 | * @method Image sepia() 38 | * @method Image merge(Image $other, $x = 0, $y = 0, $width = null, $height = null) 39 | * @method Image rotate($angle, $background = 0xffffff) 40 | * @method Image fill($color = 0xffffff, $x = 0, $y = 0) 41 | * @method Image write($font, $text, $x = 0, $y = 0, $size = 12, $angle = 0, $color = 0x000000, $align = 'left') 42 | * @method Image rectangle($x1, $y1, $x2, $y2, $color, $filled = false) 43 | * @method Image roundedRectangle($x1, $y1, $x2, $y2, $radius, $color, $filled = false) 44 | * @method Image line($x1, $y1, $x2, $y2, $color = 0x000000) 45 | * @method Image ellipse($cx, $cy, $width, $height, $color = 0x000000, $filled = false) 46 | * @method Image circle($cx, $cy, $r, $color = 0x000000, $filled = false) 47 | * @method Image polygon(array $points, $color, $filled = false) 48 | * @method Image flip($flipVertical, $flipHorizontal) 49 | */ 50 | class Image 51 | { 52 | /** 53 | * Directory to use for file caching. 54 | */ 55 | protected $cacheDir = 'cache/images'; 56 | 57 | /** 58 | * Directory cache mode. 59 | */ 60 | protected $cacheMode = null; 61 | 62 | /** 63 | * Internal adapter. 64 | * 65 | * @var AdapterInterface 66 | */ 67 | protected $adapter = null; 68 | 69 | /** 70 | * Pretty name for the image. 71 | */ 72 | protected $prettyName = ''; 73 | protected $prettyPrefix; 74 | 75 | /** 76 | * Transformations hash. 77 | */ 78 | protected $hash = null; 79 | 80 | /** 81 | * The image source. 82 | */ 83 | protected $source = null; 84 | 85 | /** 86 | * Force image caching, even if there is no operation applied. 87 | */ 88 | protected $forceCache = true; 89 | 90 | /** 91 | * Supported types. 92 | */ 93 | public static $types = array( 94 | 'jpg' => 'jpeg', 95 | 'jpeg' => 'jpeg', 96 | 'webp' => 'webp', 97 | 'png' => 'png', 98 | 'gif' => 'gif', 99 | ); 100 | 101 | /** 102 | * Fallback image. 103 | */ 104 | protected $fallback; 105 | 106 | /** 107 | * Use fallback image. 108 | */ 109 | protected $useFallbackImage = true; 110 | 111 | /** 112 | * Cache system. 113 | * 114 | * @var \Gregwar\Cache\CacheInterface 115 | */ 116 | protected $cache; 117 | 118 | /** 119 | * Get the cache system. 120 | * 121 | * @return \Gregwar\Cache\CacheInterface 122 | */ 123 | public function getCacheSystem() 124 | { 125 | if (is_null($this->cache)) { 126 | $this->cache = new \Gregwar\Cache\Cache(); 127 | $this->cache->setCacheDirectory($this->cacheDir); 128 | } 129 | 130 | return $this->cache; 131 | } 132 | 133 | /** 134 | * Set the cache system. 135 | * 136 | * @param \Gregwar\Cache\CacheInterface $cache 137 | */ 138 | public function setCacheSystem(CacheInterface $cache) 139 | { 140 | $this->cache = $cache; 141 | } 142 | 143 | /** 144 | * Change the caching directory. 145 | */ 146 | public function setCacheDir($cacheDir) 147 | { 148 | $this->getCacheSystem()->setCacheDirectory($cacheDir); 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * @param int $dirMode 155 | */ 156 | public function setCacheDirMode($dirMode) 157 | { 158 | $this->cache->setDirectoryMode($dirMode); 159 | } 160 | 161 | /** 162 | * Enable or disable to force cache even if the file is unchanged. 163 | */ 164 | public function setForceCache($forceCache = true) 165 | { 166 | $this->forceCache = $forceCache; 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * The actual cache dir. 173 | */ 174 | public function setActualCacheDir($actualCacheDir) 175 | { 176 | $this->getCacheSystem()->setActualCacheDirectory($actualCacheDir); 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * Sets the pretty name of the image. 183 | */ 184 | public function setPrettyName($name, $prefix = true) 185 | { 186 | if (empty($name)) { 187 | return $this; 188 | } 189 | 190 | $this->prettyName = $this->urlize($name); 191 | $this->prettyPrefix = $prefix; 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * Urlizes the prettyName. 198 | */ 199 | protected function urlize($name) 200 | { 201 | $transliterator = '\Behat\Transliterator\Transliterator'; 202 | 203 | if (class_exists($transliterator)) { 204 | $name = $transliterator::transliterate($name); 205 | $name = $transliterator::urlize($name); 206 | } else { 207 | $name = strtolower($name); 208 | $name = str_replace(' ', '-', $name); 209 | $name = preg_replace('/([^a-z0-9\-]+)/m', '', $name); 210 | } 211 | 212 | return $name; 213 | } 214 | 215 | /** 216 | * Operations array. 217 | */ 218 | protected $operations = array(); 219 | 220 | public function __construct($originalFile = null, $width = null, $height = null) 221 | { 222 | $this->setFallback(null); 223 | 224 | if ($originalFile) { 225 | $this->source = new Source\File($originalFile); 226 | } else { 227 | $this->source = new Source\Create($width, $height); 228 | } 229 | } 230 | 231 | /** 232 | * Sets the image data. 233 | */ 234 | public function setData($data) 235 | { 236 | $this->source = new Source\Data($data); 237 | } 238 | 239 | /** 240 | * Sets the resource. 241 | */ 242 | public function setResource($resource) 243 | { 244 | $this->source = new Source\Resource($resource); 245 | } 246 | 247 | /** 248 | * Use the fallback image or not. 249 | */ 250 | public function useFallback($useFallbackImage = true) 251 | { 252 | $this->useFallbackImage = $useFallbackImage; 253 | 254 | return $this; 255 | } 256 | 257 | /** 258 | * Sets the fallback image to use. 259 | */ 260 | public function setFallback($fallback = null) 261 | { 262 | if ($fallback === null) { 263 | $this->fallback = __DIR__.'/images/error.jpg'; 264 | } else { 265 | $this->fallback = $fallback; 266 | } 267 | 268 | return $this; 269 | } 270 | 271 | /** 272 | * Gets the fallack image path. 273 | */ 274 | public function getFallback() 275 | { 276 | return $this->fallback; 277 | } 278 | 279 | /** 280 | * Gets the fallback into the cache dir. 281 | */ 282 | public function getCacheFallback() 283 | { 284 | $fallback = $this->fallback; 285 | 286 | return $this->getCacheSystem()->getOrCreateFile('fallback.jpg', array(), function ($target) use ($fallback) { 287 | copy($fallback, $target); 288 | }); 289 | } 290 | 291 | /** 292 | * @return AdapterInterface 293 | */ 294 | public function getAdapter() 295 | { 296 | if (null === $this->adapter) { 297 | // Defaults to GD 298 | $this->setAdapter('gd'); 299 | } 300 | 301 | return $this->adapter; 302 | } 303 | 304 | public function setAdapter($adapter) 305 | { 306 | if ($adapter instanceof Adapter\Adapter) { 307 | $this->adapter = $adapter; 308 | } else { 309 | if (is_string($adapter)) { 310 | $adapter = strtolower($adapter); 311 | 312 | switch ($adapter) { 313 | case 'gd': 314 | $this->adapter = new Adapter\GD(); 315 | break; 316 | case 'imagemagick': 317 | case 'imagick': 318 | $this->adapter = new Adapter\Imagick(); 319 | break; 320 | default: 321 | throw new \Exception('Unknown adapter: '.$adapter); 322 | break; 323 | } 324 | } else { 325 | throw new \Exception('Unable to load the given adapter (not string or Adapter)'); 326 | } 327 | } 328 | 329 | $this->adapter->setSource($this->source); 330 | } 331 | 332 | /** 333 | * Get the file path. 334 | * 335 | * @return mixed a string with the filen name, null if the image 336 | * does not depends on a file 337 | */ 338 | public function getFilePath() 339 | { 340 | if ($this->source instanceof Source\File) { 341 | return $this->source->getFile(); 342 | } else { 343 | return; 344 | } 345 | } 346 | 347 | /** 348 | * Defines the file only after instantiation. 349 | * 350 | * @param string $originalFile the file path 351 | */ 352 | public function fromFile($originalFile) 353 | { 354 | $this->source = new Source\File($originalFile); 355 | 356 | return $this; 357 | } 358 | 359 | /** 360 | * Tells if the image is correct. 361 | */ 362 | public function correct() 363 | { 364 | return $this->source->correct(); 365 | } 366 | 367 | /** 368 | * Guess the file type. 369 | */ 370 | public function guessType() 371 | { 372 | return $this->source->guessType(); 373 | } 374 | 375 | /** 376 | * Adds an operation. 377 | */ 378 | protected function addOperation($method, $args) 379 | { 380 | $this->operations[] = array($method, $args); 381 | } 382 | 383 | /** 384 | * Generic function. 385 | */ 386 | public function __call($methodName, $args) 387 | { 388 | $adapter = $this->getAdapter(); 389 | $reflection = new \ReflectionClass(get_class($adapter)); 390 | 391 | if ($reflection->hasMethod($methodName)) { 392 | $method = $reflection->getMethod($methodName); 393 | 394 | if ($method->getNumberOfRequiredParameters() > count($args)) { 395 | throw new \InvalidArgumentException('Not enough arguments given for '.$methodName); 396 | } 397 | 398 | $this->addOperation($methodName, $args); 399 | 400 | return $this; 401 | } 402 | 403 | throw new \BadFunctionCallException('Invalid method: '.$methodName); 404 | } 405 | 406 | /** 407 | * Serialization of operations. 408 | */ 409 | public function serializeOperations() 410 | { 411 | $datas = array(); 412 | 413 | foreach ($this->operations as $operation) { 414 | $method = $operation[0]; 415 | $args = $operation[1]; 416 | 417 | foreach ($args as &$arg) { 418 | if ($arg instanceof self) { 419 | $arg = $arg->getHash(); 420 | } 421 | } 422 | 423 | $datas[] = array($method, $args); 424 | } 425 | 426 | return serialize($datas); 427 | } 428 | 429 | /** 430 | * Generates the hash. 431 | */ 432 | public function generateHash($type = 'guess', $quality = 80) 433 | { 434 | $inputInfos = $this->source->getInfos(); 435 | 436 | $datas = array( 437 | $inputInfos, 438 | $this->serializeOperations(), 439 | $type, 440 | $quality, 441 | ); 442 | 443 | $this->hash = sha1(serialize($datas)); 444 | } 445 | 446 | /** 447 | * Gets the hash. 448 | */ 449 | public function getHash($type = 'guess', $quality = 80) 450 | { 451 | if (null === $this->hash) { 452 | $this->generateHash($type, $quality); 453 | } 454 | 455 | return $this->hash; 456 | } 457 | 458 | /** 459 | * Gets the cache file name and generate it if it does not exists. 460 | * Note that if it exists, all the image computation process will 461 | * not be done. 462 | * 463 | * @param string $type the image type 464 | * @param int $quality the quality (for JPEG) 465 | */ 466 | public function cacheFile($type = 'jpg', $quality = 80, $actual = false) 467 | { 468 | if ($type == 'guess') { 469 | $type = $this->guessType(); 470 | } 471 | 472 | if (!count($this->operations) && $type == $this->guessType() && !$this->forceCache) { 473 | return $this->getFilename($this->getFilePath()); 474 | } 475 | 476 | // Computes the hash 477 | $this->hash = $this->getHash($type, $quality); 478 | 479 | // Generates the cache file 480 | $cacheFile = ''; 481 | 482 | if (!$this->prettyName || $this->prettyPrefix) { 483 | $cacheFile .= $this->hash; 484 | } 485 | 486 | if ($this->prettyPrefix) { 487 | $cacheFile .= '-'; 488 | } 489 | 490 | if ($this->prettyName) { 491 | $cacheFile .= $this->prettyName; 492 | } 493 | 494 | $cacheFile .= '.'.$type; 495 | 496 | // If the files does not exists, save it 497 | $image = $this; 498 | 499 | // Target file should be younger than all the current image 500 | // dependencies 501 | $conditions = array( 502 | 'younger-than' => $this->getDependencies(), 503 | ); 504 | 505 | // The generating function 506 | $generate = function ($target) use ($image, $type, $quality) { 507 | $result = $image->save($target, $type, $quality); 508 | 509 | if ($result != $target) { 510 | throw new GenerationError($result); 511 | } 512 | }; 513 | 514 | // Asking the cache for the cacheFile 515 | try { 516 | $file = $this->getCacheSystem()->getOrCreateFile($cacheFile, $conditions, $generate, $actual); 517 | } catch (GenerationError $e) { 518 | $file = $e->getNewFile(); 519 | } 520 | 521 | // Nulling the resource 522 | $this->getAdapter()->setSource(new Source\File($file)); 523 | $this->getAdapter()->deinit(); 524 | 525 | if ($actual) { 526 | return $file; 527 | } else { 528 | return $this->getFilename($file); 529 | } 530 | } 531 | 532 | /** 533 | * Get cache data (to render the image). 534 | * 535 | * @param string $type the image type 536 | * @param int $quality the quality (for JPEG) 537 | */ 538 | public function cacheData($type = 'jpg', $quality = 80) 539 | { 540 | return file_get_contents($this->cacheFile($type, $quality)); 541 | } 542 | 543 | /** 544 | * Hook to helps to extends and enhance this class. 545 | */ 546 | protected function getFilename($filename) 547 | { 548 | return $filename; 549 | } 550 | 551 | /** 552 | * Generates and output a jpeg cached file. 553 | */ 554 | public function jpeg($quality = 80) 555 | { 556 | return $this->cacheFile('jpg', $quality); 557 | } 558 | 559 | /** 560 | * Generates and output a gif cached file. 561 | */ 562 | public function gif() 563 | { 564 | return $this->cacheFile('gif'); 565 | } 566 | 567 | /** 568 | * Generates and output a png cached file. 569 | */ 570 | public function png() 571 | { 572 | return $this->cacheFile('png'); 573 | } 574 | 575 | /** 576 | * Generates and output a webp cached file. 577 | */ 578 | public function webp($quality = 80) 579 | { 580 | return $this->cacheFile('webp', $quality); 581 | } 582 | 583 | /** 584 | * Generates and output an image using the same type as input. 585 | */ 586 | public function guess($quality = 80) 587 | { 588 | return $this->cacheFile('guess', $quality); 589 | } 590 | 591 | /** 592 | * Get all the files that this image depends on. 593 | * 594 | * @return string[] this is an array of strings containing all the files that the 595 | * current Image depends on 596 | */ 597 | public function getDependencies() 598 | { 599 | $dependencies = array(); 600 | 601 | $file = $this->getFilePath(); 602 | if ($file) { 603 | $dependencies[] = $file; 604 | } 605 | 606 | foreach ($this->operations as $operation) { 607 | foreach ($operation[1] as $argument) { 608 | if ($argument instanceof self) { 609 | $dependencies = array_merge($dependencies, $argument->getDependencies()); 610 | } 611 | } 612 | } 613 | 614 | return $dependencies; 615 | } 616 | 617 | /** 618 | * Applies the operations. 619 | */ 620 | public function applyOperations() 621 | { 622 | // Renders the effects 623 | foreach ($this->operations as $operation) { 624 | call_user_func_array(array($this->adapter, $operation[0]), $operation[1]); 625 | } 626 | } 627 | 628 | /** 629 | * Initialize the adapter. 630 | */ 631 | public function init() 632 | { 633 | $this->getAdapter()->init(); 634 | } 635 | 636 | /** 637 | * Save the file to a given output. 638 | */ 639 | public function save($file, $type = 'guess', $quality = 80) 640 | { 641 | if ($file) { 642 | $directory = dirname($file); 643 | 644 | if (!is_dir($directory)) { 645 | @mkdir($directory, 0777, true); 646 | } 647 | } 648 | 649 | if (is_int($type)) { 650 | $quality = $type; 651 | $type = 'jpeg'; 652 | } 653 | 654 | if ($type == 'guess') { 655 | $type = $this->guessType(); 656 | } 657 | 658 | if (!isset(self::$types[$type])) { 659 | throw new \InvalidArgumentException('Given type ('.$type.') is not valid'); 660 | } 661 | 662 | $type = self::$types[$type]; 663 | 664 | try { 665 | $this->init(); 666 | $this->applyOperations(); 667 | 668 | $success = false; 669 | 670 | if (null == $file) { 671 | ob_start(); 672 | } 673 | 674 | if ($type == 'jpeg') { 675 | $success = $this->getAdapter()->saveJpeg($file, $quality); 676 | } 677 | 678 | if ($type == 'gif') { 679 | $success = $this->getAdapter()->saveGif($file); 680 | } 681 | 682 | if ($type == 'png') { 683 | $success = $this->getAdapter()->savePng($file); 684 | } 685 | 686 | if ($type == 'webp') { 687 | $success = $this->getAdapter()->saveWebP($file, $quality); 688 | } 689 | 690 | if (!$success) { 691 | return false; 692 | } 693 | 694 | return null === $file ? ob_get_clean() : $file; 695 | } catch (\Exception $e) { 696 | if ($this->useFallbackImage) { 697 | return null === $file ? file_get_contents($this->fallback) : $this->getCacheFallback(); 698 | } else { 699 | throw $e; 700 | } 701 | } 702 | } 703 | 704 | /** 705 | * Get the contents of the image. 706 | */ 707 | public function get($type = 'guess', $quality = 80) 708 | { 709 | return $this->save(null, $type, $quality); 710 | } 711 | 712 | /* Image API */ 713 | 714 | /** 715 | * Image width. 716 | */ 717 | public function width() 718 | { 719 | return $this->getAdapter()->width(); 720 | } 721 | 722 | /** 723 | * Image height. 724 | */ 725 | public function height() 726 | { 727 | return $this->getAdapter()->height(); 728 | } 729 | 730 | /** 731 | * Tostring defaults to jpeg. 732 | */ 733 | public function __toString() 734 | { 735 | return $this->guess(); 736 | } 737 | 738 | /** 739 | * Returning basic html code for this image. 740 | */ 741 | public function html($title = '', $type = 'jpg', $quality = 80) 742 | { 743 | return ''; 744 | } 745 | 746 | /** 747 | * Returns the Base64 inlinable representation. 748 | */ 749 | public function inline($type = 'jpg', $quality = 80) 750 | { 751 | $mime = $type; 752 | if ($mime == 'jpg') { 753 | $mime = 'jpeg'; 754 | } 755 | 756 | return 'data:image/'.$mime.';base64,'.base64_encode(file_get_contents($this->cacheFile($type, $quality, true))); 757 | } 758 | 759 | /** 760 | * Creates an instance, usefull for one-line chaining. 761 | */ 762 | public static function open($file = '') 763 | { 764 | return new static($file); 765 | } 766 | 767 | /** 768 | * Creates an instance of a new resource. 769 | */ 770 | public static function create($width, $height) 771 | { 772 | return new static(null, $width, $height); 773 | } 774 | 775 | /** 776 | * Creates an instance of image from its data. 777 | */ 778 | public static function fromData($data) 779 | { 780 | $image = new static(); 781 | $image->setData($data); 782 | 783 | return $image; 784 | } 785 | 786 | /** 787 | * Creates an instance of image from resource. 788 | */ 789 | public static function fromResource($resource) 790 | { 791 | $image = new static(); 792 | $image->setResource($resource); 793 | 794 | return $image; 795 | } 796 | } 797 | -------------------------------------------------------------------------------- /ImageColor.php: -------------------------------------------------------------------------------- 1 | 0x000000, 12 | 'silver' => 0xc0c0c0, 13 | 'gray' => 0x808080, 14 | 'teal' => 0x008080, 15 | 'aqua' => 0x00ffff, 16 | 'blue' => 0x0000ff, 17 | 'navy' => 0x000080, 18 | 'green' => 0x008000, 19 | 'lime' => 0x00ff00, 20 | 'white' => 0xffffff, 21 | 'fuschia' => 0xff00ff, 22 | 'purple' => 0x800080, 23 | 'olive' => 0x808000, 24 | 'yellow' => 0xffff00, 25 | 'orange' => 0xffA500, 26 | 'red' => 0xff0000, 27 | 'maroon' => 0x800000, 28 | 'transparent' => 0x7fffffff, 29 | ); 30 | 31 | public static function gdAllocate($image, $color) 32 | { 33 | $colorRGBA = self::parse($color); 34 | 35 | $b = ($colorRGBA) & 0xff; 36 | $colorRGBA >>= 8; 37 | $g = ($colorRGBA) & 0xff; 38 | $colorRGBA >>= 8; 39 | $r = ($colorRGBA) & 0xff; 40 | $colorRGBA >>= 8; 41 | $a = ($colorRGBA) & 0xff; 42 | 43 | $c = imagecolorallocatealpha($image, $r, $g, $b, $a); 44 | 45 | if ($color == 'transparent') { 46 | imagecolortransparent($image, $c); 47 | } 48 | 49 | return $c; 50 | } 51 | 52 | public static function parse($color) 53 | { 54 | // Direct color representation (ex: 0xff0000) 55 | if (!is_string($color) && is_numeric($color)) { 56 | return $color; 57 | } 58 | 59 | // Color name (ex: "red") 60 | if (isset(self::$colors[$color])) { 61 | return self::$colors[$color]; 62 | } 63 | 64 | if (is_string($color)) { 65 | $color_string = str_replace(' ', '', $color); 66 | 67 | // Color string (ex: "ff0000", "#ff0000" or "0xfff") 68 | if (preg_match('/^(#|0x|)([0-9a-f]{3,6})/i', $color_string, $matches)) { 69 | $col = $matches[2]; 70 | 71 | if (strlen($col) == 6) { 72 | return hexdec($col); 73 | } 74 | 75 | if (strlen($col) == 3) { 76 | $r = ''; 77 | for ($i = 0; $i < 3; ++$i) { 78 | $r .= $col[$i].$col[$i]; 79 | } 80 | 81 | return hexdec($r); 82 | } 83 | } 84 | 85 | // Colors like "rgb(255, 0, 0)" 86 | if (preg_match('/^rgb\(([0-9]+),([0-9]+),([0-9]+)\)/i', $color_string, $matches)) { 87 | $r = $matches[1]; 88 | $g = $matches[2]; 89 | $b = $matches[3]; 90 | if ($r >= 0 && $r <= 0xff && $g >= 0 && $g <= 0xff && $b >= 0 && $b <= 0xff) { 91 | return ($r << 16) | ($g << 8) | ($b); 92 | } 93 | } 94 | } 95 | 96 | throw new \InvalidArgumentException('Invalid color: '.$color); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) <2012-2017> Grégoire Passault 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | cs: 2 | php-cs-fixer fix --verbose 3 | 4 | test: 5 | phpunit -c phpunit.xml.dist 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gregwar's Image class 2 | 3 | [![Build status](https://github.com/Gregwar/Image/actions/workflows/test.yml/badge.svg)](https://github.com/Gregwar/Image/actions/workflows/test.yml) 4 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YUXRLWHQSWS6L) 5 | 6 | The `Gregwar\Image` class purpose is to provide a simple object-oriented images handling and caching API. 7 | 8 | # Installation 9 | 10 | With composer : 11 | 12 | ``` json 13 | { 14 | ... 15 | "require": { 16 | "gregwar/image": "2.*" 17 | } 18 | } 19 | ``` 20 | 21 | # Usage 22 | 23 | ## Basic handling 24 | 25 | Using methods chaining, you can open, transform and save a file in a single line: 26 | 27 | ```php 28 | resize(100, 100) 32 | ->negate() 33 | ->save('out.jpg'); 34 | ``` 35 | 36 | Here are the resize methods: 37 | 38 | * `resize($width, $height, $background)`: resizes the image, will preserve scale and never 39 | enlarge it (background is `red` in order to understand what happens): 40 | 41 | ![resize()](doc/resize.jpg) 42 | 43 | * `scaleResize($width, $height, $background)`: resizes the image, will preserve scale, can enlarge 44 | it (background is `red` in order to understand what happens): 45 | 46 | ![scaleResize()](doc/scaleResize.jpg) 47 | 48 | * `forceResize($width, $height, $background)`: resizes the image forcing it to 49 | be exactly `$width` by `$height` 50 | 51 | ![forceResize()](doc/forceResize.jpg) 52 | 53 | * `cropResize($width, $height, $background)`: resizes the image preserving scale (just like `resize()`) 54 | and croping the whitespaces: 55 | 56 | ![cropResize()](doc/cropResize.jpg) 57 | 58 | * `zoomCrop($width, $height, $background, $xPos, $yPos)`: resize and crop the image to fit to given dimensions: 59 | 60 | ![zoomCrop()](doc/zoomCrop.jpg) 61 | 62 | * In `zoomCrop()`, You can change the position of the resized image using the `$xPos` (center, left or right) and `$yPos` (center, 63 | top or bottom): 64 | 65 | ![zoomCrop() with yPos=top](doc/zoomCropTop.jpg) 66 | 67 | The other methods available are: 68 | 69 | * `crop($x, $y, $w, $h)`: crops the image to a box located on coordinates $x,y and 70 | which size is $w by $h 71 | 72 | * `negate()`: negates the image colors 73 | 74 | * `brighness($b)`: applies a brightness effect to the image (from -255 to +255) 75 | 76 | * `contrast($c)`: applies a contrast effect to the image (from -100 to +100) 77 | 78 | * `grayscale()`: converts the image to grayscale 79 | 80 | * `emboss()`: emboss the image 81 | 82 | * `smooth($p)`: smooth the image 83 | 84 | * `sharp()`: applies a mean removal filter on the image 85 | 86 | * `edge()`: applies an edge effect on the image 87 | 88 | * `colorize($red, $green, $blue)`: colorize the image (from -255 to +255 for each color) 89 | 90 | * `sepia()`: applies a sepia effect 91 | 92 | * `merge($image, $x, $y, $width, $height)`: merges two images 93 | 94 | * `fill($color, $x, $y)`: fills the image with the given color 95 | 96 | * `write($font, $text, $x, $y, $size, $angle, $color, $position)`: writes text over image, $position can be any of 'left', 'right', or 'center' 97 | 98 | * `rectangle($x1, $y1, $x2, $y2, $color, $filled=false)`: draws a rectangle 99 | 100 | * `rotate($angle, $background = 0xffffff)` : rotate the image to given angle 101 | 102 | * `roundedRectangle($x1, $y1, $x2, $y2, $radius, $color, $filled=false)`: draws a rounded rectangle ($radius can be anything from 0) 103 | 104 | * `line($x1, $y1, $x2, $y2, $color)`: draws a line 105 | 106 | * `ellipse($cx, $cy, $width, $height, $color, $filled=false)`: draws an ellipse 107 | 108 | * `circle($cx, $cy, $r, $color, $filled=false)`: draws a circle 109 | 110 | * `fillBackground($bg=0xffffff)`: fills the background of a transparent image to the 'bg' color 111 | 112 | * `fixOrientation()`: return the image rotated and flipped using image exif information 113 | 114 | * `applyExifOrientation(int $exif_rotation_value)`: return the image rotated and flipped using an exif rotation value 115 | 116 | * `html($title = '', $type = 'jpg')`: return the `` tag with the cache image 117 | 118 | * `flip($flipVertical, $flipHorizontal)`: flips the image in the given directions. Both params are boolean and at least one must be true. 119 | 120 | * `inline($type = 'jpg')`: returns the HTML inlinable base64 string (see `demo/inline.php`) 121 | 122 | You can also create image from scratch using: 123 | 124 | ```php 125 | save('output.jpg', 'jpg', 85); 139 | ``` 140 | 141 | You can also get the contents of the image using `get($type = 'jpg', $quality = 80)`, which will return the binary contents of the image 142 | 143 | ## Using cache 144 | 145 | Each operation above is not actually applied on the opened image, but added in an operations 146 | array. This operation array, the name, type and modification time of file are hashed using 147 | `sha1()` and the hash is used to look up for a cache file. 148 | 149 | Once the cache directory configured, you can call the following methods: 150 | 151 | * `jpeg($quality = 80)`: lookup or create a jpeg cache file on-the-fly 152 | 153 | * `gif()`: lookup or create a gif cache file on-the-fly 154 | 155 | * `png()`: lookup or create a png cache file on-the-fly 156 | 157 | * `guess($quality = 80)`: guesses the type (use the same as input) and lookup or create a 158 | cache file on-the-fly 159 | 160 | * `setPrettyName($prettyName, $prefix = true)`: sets a "pretty" name suffix for the file, if you want it to be more SEO-friendly. 161 | for instance, if you call it "Fancy Image", the cache will look like something/something-fancy-image.jpg. 162 | If `$prefix` is passed to `false` (default `true`), the pretty name won't have any hash prefix. 163 | If you want to use non-latin1 pretty names, **behat/transliterator** package must be installed. 164 | 165 | For instance: 166 | 167 | ```php 168 | sepia() 172 | ->jpeg(); 173 | 174 | //Outputs: cache/images/1/8/6/9/c/86e4532dbd9c073075ef08e9751fc9bc0f4.jpg 175 | ``` 176 | 177 | If the original file and operations do not change, the hashed value will be the same and the 178 | cache will not be generated again. 179 | 180 | You can use this directly in an HTML document: 181 | 182 | 183 | ```php 184 | resize(150, 150)->jpeg(); ?>" /> 188 | // ... 189 | ``` 190 | 191 | This is powerful since if you change the original image or any of your code the cached hash 192 | will change and the file will be regenerated. 193 | 194 | Writing image 195 | ------------- 196 | 197 | You can also create your own image on-the-fly using drawing functions: 198 | 199 | 200 | ```php 201 | fill(0xffaaaa) // Filling with a light red 204 | ->rectangle(0xff3333, 0, 100, 300, 200, true) // Drawing a red rectangle 205 | // Writing "Hello $username !" on the picture using a custom TTF font file 206 | ->write('./fonts/CaviarDreams.ttf', 'Hello '.$username.'!', 150, 150, 20, 0, 'white', 'center') 207 | ->jpeg(); 208 | ?> 209 | 210 | ``` 211 | 212 | ## Using fallback image 213 | 214 | If the image file doesn't exist, you can configure a fallback image that will be used 215 | by the class (note that this requires the cache directory to be available). 216 | 217 | A default "error" image which is used is in `images/error.jpg`, you can change it with: 218 | 219 | ```php 220 | setFallback('/path/to/my/fallback.jpg'); 222 | ``` 223 | 224 | ## Garbage Collect 225 | 226 | To prevent the cache from growing forever, you can use the provided GarbageCollect class as below: 227 | 228 | ```php 229 | negate(); 257 | $this->sepia(); 258 | } 259 | ``` 260 | 261 | Which could be used on the Image 262 | 263 | ```php 264 | myFilter(); 266 | ``` 267 | 268 | You can also write your own adapter which could extend one of this repository and use it by calling `setAdapter()`: 269 | 270 | ```php 271 | setAdapter(new MyCustomAdapter); 273 | ``` 274 | 275 | # License 276 | 277 | `Gregwar\Image` is under MIT License, please read the LICENSE file for further details. 278 | Do not hesitate to fork this repository and customize it ! 279 | -------------------------------------------------------------------------------- /Source/Create.php: -------------------------------------------------------------------------------- 1 | width = $width; 16 | $this->height = $height; 17 | } 18 | 19 | public function getWidth() 20 | { 21 | return $this->width; 22 | } 23 | 24 | public function getHeight() 25 | { 26 | return $this->height; 27 | } 28 | 29 | public function getInfos() 30 | { 31 | return array($this->width, $this->height); 32 | } 33 | 34 | public function correct() 35 | { 36 | return $this->width > 0 && $this->height > 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Source/Data.php: -------------------------------------------------------------------------------- 1 | data = $data; 15 | } 16 | 17 | public function getData() 18 | { 19 | return $this->data; 20 | } 21 | 22 | public function getInfos() 23 | { 24 | return sha1($this->data); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/File.php: -------------------------------------------------------------------------------- 1 | file = $file; 18 | } 19 | 20 | public function getFile() 21 | { 22 | return $this->file; 23 | } 24 | 25 | public function correct() 26 | { 27 | return false !== @exif_imagetype($this->file); 28 | } 29 | 30 | public function guessType() 31 | { 32 | if (function_exists('exif_imagetype') && FileUtils::safeExists($this->file)) { 33 | $type = @exif_imagetype($this->file); 34 | 35 | if (false !== $type) { 36 | if ($type == IMAGETYPE_JPEG) { 37 | return 'jpeg'; 38 | } 39 | 40 | if ($type == IMAGETYPE_GIF) { 41 | return 'gif'; 42 | } 43 | 44 | if ($type == IMAGETYPE_PNG) { 45 | return 'png'; 46 | } 47 | 48 | if ($type == IMAGETYPE_WEBP) { 49 | return 'webp'; 50 | } 51 | } 52 | } 53 | 54 | $parts = explode('.', $this->file); 55 | $ext = strtolower($parts[count($parts) - 1]); 56 | 57 | if (isset(Image::$types[$ext])) { 58 | return Image::$types[$ext]; 59 | } 60 | 61 | return 'jpeg'; 62 | } 63 | 64 | public function getInfos() 65 | { 66 | return $this->file; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Source/Resource.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 15 | } 16 | 17 | public function getResource() 18 | { 19 | return $this->resource; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Source.php: -------------------------------------------------------------------------------- 1 | resize(500, 500) 9 | ->save('out.png', 'png'); 10 | -------------------------------------------------------------------------------- /demo/cache.php: -------------------------------------------------------------------------------- 1 | sepia(); 10 | echo "\n"; 11 | -------------------------------------------------------------------------------- /demo/cacheCreate.php: -------------------------------------------------------------------------------- 1 | fill('rgb(255, 150, 150)') 14 | ->circle(150, 150, 200, 'red', true) 15 | ->write('./fonts/CaviarDreams.ttf', 'Hello world!', 150, 150, 20, 0, 'white', 'center') 16 | ->jpeg(); 17 | 18 | echo "\n"; 19 | -------------------------------------------------------------------------------- /demo/cacheName.php: -------------------------------------------------------------------------------- 1 | sepia() 11 | ->setPrettyName('Some FanCY TestING!') 12 | ->jpeg(); 13 | 14 | echo "\n"; 15 | -------------------------------------------------------------------------------- /demo/crop.php: -------------------------------------------------------------------------------- 1 | cropResize(500, 150) 9 | ->save('out.jpg'); 10 | -------------------------------------------------------------------------------- /demo/data.php: -------------------------------------------------------------------------------- 1 | save('out.jpg'); 15 | -------------------------------------------------------------------------------- /demo/fallback.php: -------------------------------------------------------------------------------- 1 | resize(100, 0) 9 | ->negate() 10 | ->jpeg() 11 | ; 12 | -------------------------------------------------------------------------------- /demo/fonts/CaviarDreams.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/demo/fonts/CaviarDreams.ttf -------------------------------------------------------------------------------- /demo/gc.php: -------------------------------------------------------------------------------- 1 | resize(100, 100) 9 | ->negate() 10 | ->get('jpeg'); 11 | 12 | echo $image; 13 | -------------------------------------------------------------------------------- /demo/guess.php: -------------------------------------------------------------------------------- 1 | resize(100, 100) 9 | ->negate() 10 | ->guess(55); 11 | -------------------------------------------------------------------------------- /demo/img/mona.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/demo/img/mona.jpg -------------------------------------------------------------------------------- /demo/img/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/demo/img/test.png -------------------------------------------------------------------------------- /demo/img/test2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/demo/img/test2.jpg -------------------------------------------------------------------------------- /demo/img/vinci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/demo/img/vinci.png -------------------------------------------------------------------------------- /demo/inline.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /demo/merge.php: -------------------------------------------------------------------------------- 1 | merge(Image::open('img/test2.jpg')->cropResize(100, 100)) 9 | ->save('out.jpg', 'jpg'); 10 | -------------------------------------------------------------------------------- /demo/percent.php: -------------------------------------------------------------------------------- 1 | resize('26%') 9 | ->negate() 10 | ->save('out.jpg', 'jpg'); 11 | -------------------------------------------------------------------------------- /demo/resource.php: -------------------------------------------------------------------------------- 1 | save('out.jpg'); 17 | -------------------------------------------------------------------------------- /demo/watermark.php: -------------------------------------------------------------------------------- 1 | merge($watermark, $img->width()-$watermark->width(), 15 | $img->height()-$watermark->height()) 16 | ->save('out.jpg', 'jpg'); 17 | -------------------------------------------------------------------------------- /demo/write.php: -------------------------------------------------------------------------------- 1 | fill('rgb(255, 150, 150)') 9 | ->circle(150, 150, 200, 'red', true) 10 | ->write('./fonts/CaviarDreams.ttf', 'Hello world!', 150, 150, 20, 0, 'white', 'center') 11 | ->save('out.jpg', 'jpeg', 95); 12 | -------------------------------------------------------------------------------- /doc/cropResize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/doc/cropResize.jpg -------------------------------------------------------------------------------- /doc/forceResize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/doc/forceResize.jpg -------------------------------------------------------------------------------- /doc/generate.php: -------------------------------------------------------------------------------- 1 | resize(250, 250, 'red') 10 | ->save('resize.jpg'); 11 | 12 | // scaleResize() will also preserve the scale, but won't 13 | // enlage the image 14 | Image::open('mona.jpg') 15 | ->scaleResize(250, 250, 'red') 16 | ->save('scaleResize.jpg'); 17 | 18 | // forceResize() will resize matching the *exact* given size 19 | Image::open('mona.jpg') 20 | ->forceResize(250, 250) 21 | ->save('forceResize.jpg'); 22 | 23 | // cropResize() preserves scale just like resize() but will 24 | // trim the whitespace (if any) in the resulting image 25 | Image::open('mona.jpg') 26 | ->cropResize(250, 250) 27 | ->save('cropResize.jpg'); 28 | 29 | // zoomCrop() resizes the image so that a part of it appear in 30 | // the given area 31 | Image::open('mona.jpg') 32 | ->zoomCrop(200, 200) 33 | ->save('zoomCrop.jpg'); 34 | 35 | // You can specify the position using the xPos and yPos arguments 36 | Image::open('mona.jpg') 37 | ->zoomCrop(200, 200, 'transparent', 'center', 'top') 38 | ->save('zoomCropTop.jpg'); 39 | -------------------------------------------------------------------------------- /doc/mona.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/doc/mona.jpg -------------------------------------------------------------------------------- /doc/resize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/doc/resize.jpg -------------------------------------------------------------------------------- /doc/scaleResize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/doc/scaleResize.jpg -------------------------------------------------------------------------------- /doc/zoomCrop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/doc/zoomCrop.jpg -------------------------------------------------------------------------------- /doc/zoomCropTop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/doc/zoomCropTop.jpg -------------------------------------------------------------------------------- /images/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gregwar/Image/cf21cce93e7733e6b37313f103159994419a5f8a/images/error.jpg -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | ./tests/ImageTests.php 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | output 2 | -------------------------------------------------------------------------------- /tests/ImageTests.php: -------------------------------------------------------------------------------- 1 | open('monalisa.jpg'); 19 | 20 | self::assertSame($image->width(), 771); 21 | self::assertSame($image->height(), 961); 22 | } 23 | 24 | /** 25 | * Testing the resize. 26 | */ 27 | public function testResize(): void 28 | { 29 | $image = $this->open('monalisa.jpg'); 30 | 31 | $out = $this->output('monalisa_small.jpg'); 32 | $image 33 | ->resize(300, 200) 34 | ->save($out) 35 | ; 36 | 37 | self::assertFileExists($out); 38 | 39 | $i = imagecreatefromjpeg($out); 40 | self::assertSame(300, imagesx($i)); 41 | self::assertSame(200, imagesy($i)); 42 | } 43 | 44 | /** 45 | * Testing the resize %. 46 | */ 47 | public function testResizePercent(): void 48 | { 49 | $image = $this->open('monalisa.jpg'); 50 | 51 | $out = $this->output('monalisa_small.jpg'); 52 | $image 53 | ->resize('50%') 54 | ->save($out) 55 | ; 56 | 57 | self::assertFileExists($out); 58 | 59 | $i = imagecreatefromjpeg($out); 60 | self::assertSame(386, imagesx($i)); 61 | self::assertSame(481, imagesy($i)); 62 | } 63 | 64 | /** 65 | * Testing to create an image, jpeg, gif and png. 66 | */ 67 | public function testCreateImage(): void 68 | { 69 | $black = $this->output('black.jpg'); 70 | 71 | Image::create(150, 200) 72 | ->fill('black') 73 | ->save($black, 100); 74 | 75 | $i = imagecreatefromjpeg($black); 76 | self::assertFileExists($black); 77 | self::assertSame(150, imagesx($i)); 78 | self::assertSame(200, imagesy($i)); 79 | 80 | $j = imagecolorat($i, 40, 40); 81 | self::assertSame(0, $j); 82 | 83 | $black = $this->output('black.png'); 84 | Image::create(150, 200) 85 | ->fill('black') 86 | ->save($black, 'png'); 87 | 88 | $i = imagecreatefrompng($black); 89 | self::assertFileExists($black); 90 | self::assertSame(150, imagesx($i)); 91 | self::assertSame(200, imagesy($i)); 92 | 93 | $black = $this->output('black.gif'); 94 | Image::create(150, 200) 95 | ->fill('black') 96 | ->save($black, 'gif'); 97 | 98 | $i = imagecreatefromgif($black); 99 | self::assertFileExists($black); 100 | self::assertSame(150, imagesx($i)); 101 | self::assertSame(200, imagesy($i)); 102 | } 103 | 104 | /** 105 | * Testing type guess. 106 | */ 107 | public function testGuess(): void 108 | { 109 | $image = $this->open('monalisa.jpg'); 110 | self::assertSame('jpeg', $image->guessType()); 111 | $image = $this->open('monalisa.png'); 112 | self::assertSame('png', $image->guessType()); 113 | $image = $this->open('monalisa.gif'); 114 | self::assertSame('gif', $image->guessType()); 115 | } 116 | 117 | public function testDefaultCacheSystem(): void 118 | { 119 | $image = $this->open('monalisa.jpg'); 120 | self::assertInstanceOf('Gregwar\Cache\Cache', $image->getCacheSystem()); 121 | } 122 | 123 | public function testCustomCacheSystem(): void 124 | { 125 | $image = $this->open('monalisa.jpg'); 126 | $cache = new Cache(); 127 | $image->setCacheSystem($cache); 128 | self::assertInstanceOf(Gregwar\Cache\CacheInterface::class, $image->getCacheSystem()); 129 | } 130 | 131 | /** 132 | * Testing that caching an image without operations will result 133 | * in the original image when force cache is disabled. 134 | */ 135 | public function testNoCache(): void 136 | { 137 | $monalisa = __DIR__.'/files/monalisa.jpg'; 138 | $image = $this->open('monalisa.jpg')->setForceCache(false); 139 | self::assertSame($monalisa, $image->guess()); 140 | $image = $this->open('monalisa.jpg'); 141 | self::assertNotSame($monalisa, $image->guess()); 142 | $image = $this->open('monalisa.jpg')->setForceCache(true); 143 | self::assertNotSame($monalisa, $image->guess()); 144 | } 145 | 146 | public function testActualCache(): void 147 | { 148 | $output = $this->open('monalisa.jpg') 149 | ->setCacheDir('/magic/path/to/cache/') 150 | ->resize(100, 50)->negate() 151 | ->guess(); 152 | 153 | self::assertStringContainsString('/magic/path/to/cache', $output); 154 | $file = str_replace('/magic/path/to', __DIR__.'/output/', $output); 155 | self::assertFileExists($file); 156 | } 157 | 158 | public function testCacheData(): void 159 | { 160 | $output = $this->open('monalisa.jpg') 161 | ->resize(300, 200) 162 | ->cacheData(); 163 | 164 | $jpg = imagecreatefromstring($output); 165 | self::assertSame(300, imagesx($jpg)); 166 | self::assertSame(200, imagesy($jpg)); 167 | } 168 | 169 | /** 170 | * Testing using cache. 171 | */ 172 | public function testCache(): void 173 | { 174 | $output = $this->open('monalisa.jpg') 175 | ->resize(100, 50)->negate() 176 | ->guess(); 177 | 178 | self::assertFileExists($output); 179 | $i = imagecreatefromjpeg($output); 180 | self::assertSame(100, imagesx($i)); 181 | self::assertSame(50, imagesy($i)); 182 | 183 | $output2 = $this->open('monalisa.jpg') 184 | ->resize(100, 50)->negate() 185 | ->guess(); 186 | 187 | self::assertSame($output, $output2); 188 | 189 | $output3 = $this->open('monalisa.jpg') 190 | ->resize(100, 50)->negate() 191 | ->png(); 192 | self::assertFileExists($output); 193 | $i = imagecreatefrompng($output3); 194 | self::assertSame(100, imagesx($i)); 195 | self::assertSame(50, imagesy($i)); 196 | 197 | $output4 = $this->open('monalisa.jpg') 198 | ->resize(100, 50)->negate() 199 | ->gif(); 200 | self::assertFileExists($output); 201 | $i = imagecreatefromgif($output4); 202 | self::assertSame(100, imagesx($i)); 203 | self::assertSame(50, imagesy($i)); 204 | } 205 | 206 | /** 207 | * Testing Gaussian blur filter. 208 | */ 209 | public function testGaussianBlur(): void 210 | { 211 | $image = $this->open('monalisa.jpg') 212 | ->gaussianBlur(); 213 | $secondImage = $this->open('monalisa.jpg') 214 | ->gaussianBlur(5); 215 | 216 | self::assertFileExists($image); 217 | self::assertFileExists($secondImage); 218 | } 219 | 220 | /** 221 | * Testing creating image from data. 222 | */ 223 | public function testData(): void 224 | { 225 | $data = file_get_contents(__DIR__.'/files/monalisa.jpg'); 226 | 227 | $output = $this->output('mona.jpg'); 228 | $image = Image::fromData($data); 229 | $image->save($output); 230 | 231 | self::assertFileExists($output); 232 | $i = imagecreatefromjpeg($output); 233 | self::assertSame(771, imagesx($i)); 234 | self::assertSame(961, imagesy($i)); 235 | } 236 | 237 | /** 238 | * Opening an image. 239 | */ 240 | protected function open(string $file): Image 241 | { 242 | $image = Image::open(__DIR__.'/files/'.$file); 243 | $image->setCacheDir(__DIR__.'/output/cache/'); 244 | $image->setActualCacheDir(__DIR__.'/output/cache/'); 245 | 246 | return $image; 247 | } 248 | 249 | /** 250 | * Testing transparent image. 251 | */ 252 | public function testTransparent(): void 253 | { 254 | $gif = $this->output('transparent.gif'); 255 | $i = Image::create(200, 100) 256 | ->fill('transparent') 257 | ->save($gif, 'gif'); 258 | 259 | self::assertFileExists($gif); 260 | $img = imagecreatefromgif($gif); 261 | self::assertSame(200, imagesx($img)); 262 | self::assertSame(100, imagesy($img)); 263 | $index = imagecolorat($img, 2, 2); 264 | $color = imagecolorsforindex($img, $index); 265 | self::assertSame(127, $color['alpha']); 266 | } 267 | 268 | public function testNonExistingFile(): void 269 | { 270 | $jpg = $this->output('a.jpg'); 271 | $img = $this->open('non_existing_file.jpg') 272 | ->negate(); 273 | $error = $img->save($jpg); 274 | 275 | self::assertFileExists($error); 276 | } 277 | 278 | public function testNonExistingFileNoFallback(): void 279 | { 280 | $this->expectException(\UnexpectedValueException::class); 281 | 282 | Image::open('non_existing_file.jpg') 283 | ->useFallback(false) 284 | ->save($this->output('a.jpg')); 285 | } 286 | 287 | /** 288 | * Test that image::save returns the file name. 289 | */ 290 | public function testSaveReturn(): void 291 | { 292 | $red = $this->output('red.jpg'); 293 | $result = Image::create(10, 10) 294 | ->fill('red') 295 | ->save($red); 296 | 297 | self::assertSame($red, $result); 298 | } 299 | 300 | /** 301 | * Testing merge. 302 | */ 303 | public function testMerge(): void 304 | { 305 | $out = $this->output('merge.jpg'); 306 | Image::create(100, 100) 307 | ->fill('red') 308 | ->merge(Image::create(50, 50) 309 | ->fill('black') 310 | ) 311 | ->save($out); 312 | 313 | // Merge file exists 314 | self::assertFileExists($out); 315 | 316 | // Test that the upper left zone contain a black pixel, and the lower 317 | // down contains a red one 318 | $img = imagecreatefromjpeg($out); 319 | $this->assertColorEquals('black', imagecolorat($img, 5, 12)); 320 | $this->assertColorEquals('red', imagecolorat($img, 55, 62)); 321 | } 322 | 323 | /** 324 | * Test that dependencies are well generated. 325 | */ 326 | public function testDependencies(): void 327 | { 328 | self::assertSame(array(), Image::create(100, 100) 329 | ->getDependencies()); 330 | self::assertSame(array(), Image::create(100, 100) 331 | ->merge(Image::create(100, 50)) 332 | ->getDependencies()); 333 | 334 | self::assertSame(array('toto.jpg'), Image::open('toto.jpg') 335 | ->merge(Image::create(100, 50)) 336 | ->getDependencies()); 337 | 338 | self::assertSame(array('toto.jpg', 'titi.jpg'), Image::open('toto.jpg') 339 | ->merge(Image::open('titi.jpg')) 340 | ->getDependencies()); 341 | 342 | self::assertSame(array('toto.jpg', 'titi.jpg', 'tata.jpg'), Image::open('toto.jpg') 343 | ->merge(Image::open('titi.jpg') 344 | ->merge(Image::open('tata.jpg'))) 345 | ->getDependencies()); 346 | } 347 | 348 | /** 349 | * Test that pretty name (for referencing) is well respected. 350 | */ 351 | public function testPrettyName(): void 352 | { 353 | $output = $this->open('monalisa.jpg') 354 | ->resize(100, 50)->negate() 355 | ->setPrettyName('davinci', false) 356 | ->guess(); 357 | 358 | self::assertStringContainsString('davinci', $output); 359 | 360 | $output2 = $this->open('monalisa.jpg') 361 | ->resize(100, 55)->negate() 362 | ->setPrettyName('davinci', false) 363 | ->guess(); 364 | 365 | self::assertSame($output, $output2); 366 | 367 | $prefix1 = $this->open('monalisa.jpg') 368 | ->resize(100, 50)->negate() 369 | ->setPrettyName('davinci') 370 | ->guess(); 371 | $prefix2 = $this->open('monalisa.jpg') 372 | ->resize(100, 55)->negate() 373 | ->setPrettyName('davinci') 374 | ->guess(); 375 | 376 | self::assertStringContainsString('davinci', $prefix1); 377 | self::assertStringContainsString('davinci', $prefix2); 378 | self::assertNotSame($prefix1, $prefix2); 379 | 380 | $transliterator = '\Behat\Transliterator\Transliterator'; 381 | 382 | if (class_exists($transliterator)) { 383 | $nonLatinName1 = 'ダヴィンチ'; 384 | $nonLatinOutput1 = $this->open('monalisa.jpg') 385 | ->resize(100, 50)->negate() 386 | ->setPrettyName($nonLatinName1, false) 387 | ->guess(); 388 | 389 | self::assertContains( 390 | $transliterator::urlize($transliterator::transliterate($nonLatinName1)), 391 | $nonLatinOutput1 392 | ); 393 | 394 | $nonLatinName2 = 'давинчи'; 395 | $nonLatinOutput2 = $this->open('monalisa.jpg') 396 | ->resize(100, 55)->negate() 397 | ->setPrettyName($nonLatinName2) 398 | ->guess(); 399 | 400 | self::assertContains( 401 | $transliterator::urlize($transliterator::transliterate($nonLatinName2)), 402 | $nonLatinOutput2 403 | ); 404 | } 405 | } 406 | 407 | /** 408 | * Testing inlining. 409 | */ 410 | public function testInline(): void 411 | { 412 | $output = $this->open('monalisa.jpg') 413 | ->resize(20, 20) 414 | ->inline(); 415 | 416 | self::assertSame(0, strpos($output, 'data:image/jpeg;base64,')); 417 | 418 | $data = base64_decode(substr($output, 23)); 419 | $image = imagecreatefromstring($data); 420 | 421 | self::assertSame(20, imagesx($image)); 422 | self::assertSame(20, imagesy($image)); 423 | } 424 | 425 | /** 426 | * Testing that width() can be called after cache 427 | */ 428 | public function testWidthPostCache(): void 429 | { 430 | $this->open('monalisa.jpg') 431 | ->resize(100, 50)->negate() 432 | ->png(); 433 | 434 | $dummy2 = $this->open('monalisa.jpg') 435 | ->resize(100, 50)->negate(); 436 | $png = $dummy2->png(); 437 | 438 | $i = imagecreatefrompng($png); 439 | self::assertEquals(imagesx($i), $dummy2->width()); 440 | } 441 | 442 | /** 443 | * Asserting that two colors are equals 444 | * (JPG compression is not preserving colors for instance, so we 445 | * need a non-strict way to compare it). 446 | */ 447 | protected function assertColorEquals($c1, $c2, $delta = 8): void 448 | { 449 | $c1 = ImageColor::parse($c1); 450 | $c2 = ImageColor::parse($c2); 451 | list($r1, $g1, $b1) = $this->toRGB($c1); 452 | list($r2, $g2, $b2) = $this->toRGB($c2); 453 | 454 | self::assertLessThan($delta, abs($r1 - $r2)); 455 | self::assertLessThan($delta, abs($g1 - $g2)); 456 | self::assertLessThan($delta, abs($b1 - $b2)); 457 | } 458 | 459 | protected function toRGB($color): array 460 | { 461 | $b = $color & 0xff; 462 | $g = ($color >> 8) & 0xff; 463 | $r = ($color >> 16) & 0xff; 464 | 465 | return array($r, $g, $b); 466 | } 467 | 468 | /** 469 | * Outputting an image to a file. 470 | */ 471 | protected function output(string $file): string 472 | { 473 | return __DIR__.'/output/'.$file; 474 | } 475 | 476 | /** 477 | * Reinitialize the output dir. 478 | */ 479 | public function setUp(): void 480 | { 481 | $dir = $this->output(''); 482 | `rm -rf $dir`; 483 | if( !mkdir($dir) && !is_dir($dir) ){ 484 | throw new \RuntimeException(sprintf('Directory "%s" was not created', $dir)); 485 | } 486 | if( !mkdir($concurrentDirectory = $this->output('cache')) && !is_dir($concurrentDirectory) ){ 487 | throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory)); 488 | } 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |