├── .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 | [](https://github.com/Gregwar/Image/actions/workflows/test.yml)
4 | [](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 | 
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 | 
47 |
48 | * `forceResize($width, $height, $background)`: resizes the image forcing it to
49 | be exactly `$width` by `$height`
50 |
51 | 
52 |
53 | * `cropResize($width, $height, $background)`: resizes the image preserving scale (just like `resize()`)
54 | and croping the whitespaces:
55 |
56 | 
57 |
58 | * `zoomCrop($width, $height, $background, $xPos, $yPos)`: resize and crop the image to fit to given dimensions:
59 |
60 | 
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 | 
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 |