├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .php-cs-fixer.php ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENCE.txt ├── README.md ├── composer.json ├── rector.php └── src ├── Core ├── Exception │ ├── ImageWorkshopLayerException.php │ └── ImageWorkshopLibException.php ├── ImageWorkshopLayer.php └── ImageWorkshopLib.php ├── Exception ├── ImageWorkshopBaseException.php └── ImageWorkshopException.php ├── Exif └── ExifOrientations.php └── ImageWorkshop.php /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | | Q | A 2 | | ---------------- | ----- 3 | | Bug report? | yes/no 4 | | Feature request? | yes/no 5 | | Usage question? | yes/no 6 | | PHP version used | x.y 7 | 8 | 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | | Q | A 2 | | ------------- | --- 3 | | Bug fix? | yes/no 4 | | New feature? | yes/no 5 | | Fixed tickets | #... 6 | | License | MIT 7 | 8 | 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | phpunit: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | php-version: ['8.0', '8.1'] 12 | name: 'PHPUnit - PHP/${{ matrix.php-version }} - OS/${{ matrix.os }}' 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{ matrix.php-version }} 20 | coverage: xdebug 21 | - name: Get Composer Cache Directory 22 | id: composer-cache 23 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 24 | - name: Cache dependencies 25 | uses: actions/cache@v2 26 | with: 27 | path: ${{ steps.composer-cache.outputs.dir }} 28 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 29 | restore-keys: ${{ runner.os }}-composer- 30 | - name: Install Dependencies 31 | run: composer install --no-progress 32 | - name: Starting Web server 33 | run: php -S localhost:8000 -t fixtures/ &> /dev/null & 34 | - name: PHPUnit 35 | run: vendor/bin/phpunit 36 | 37 | cs: 38 | runs-on: ${{ matrix.os }} 39 | strategy: 40 | matrix: 41 | os: [ ubuntu-latest ] 42 | php-version: [ '8.1' ] 43 | name: 'Coding style' 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v2 47 | - name: Setup PHP 48 | uses: shivammathur/setup-php@v2 49 | with: 50 | php-version: ${{ matrix.php-version }} 51 | coverage: xdebug 52 | - name: Get Composer Cache Directory 53 | id: composer-cache 54 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 55 | - name: Cache dependencies 56 | uses: actions/cache@v2 57 | with: 58 | path: ${{ steps.composer-cache.outputs.dir }} 59 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 60 | restore-keys: ${{ runner.os }}-composer- 61 | - name: Install Dependencies 62 | run: composer install --no-progress 63 | - name: PHP CS Fixer 64 | run: vendor/bin/php-cs-fixer fix -v --dry-run . 65 | 66 | phpstan: 67 | runs-on: ${{ matrix.os }} 68 | strategy: 69 | matrix: 70 | os: [ ubuntu-latest ] 71 | php-version: [ '8.0', '8.1' ] 72 | name: 'PHPStan - PHP/${{ matrix.php-version }} - OS/${{ matrix.os }}' 73 | steps: 74 | - name: Checkout 75 | uses: actions/checkout@v2 76 | - name: Setup PHP 77 | uses: shivammathur/setup-php@v2 78 | with: 79 | php-version: ${{ matrix.php-version }} 80 | coverage: xdebug 81 | - name: Get Composer Cache Directory 82 | id: composer-cache 83 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 84 | - name: Cache dependencies 85 | uses: actions/cache@v2 86 | with: 87 | path: ${{ steps.composer-cache.outputs.dir }} 88 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 89 | restore-keys: ${{ runner.os }}-composer- 90 | - name: Install Dependencies 91 | run: composer install --no-progress 92 | - name: PHPStan 93 | run: vendor/bin/phpstan analyse src -c phpstan.neon 94 | 95 | roave_bc_check: 96 | name: Roave BC Check 97 | runs-on: ubuntu-latest 98 | steps: 99 | - uses: actions/checkout@master 100 | - name: fetch tags 101 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 102 | - name: Roave BC Check 103 | uses: docker://nyholm/roave-bc-check-ga 104 | continue-on-error: true 105 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | ignoreDotFiles(false) 14 | ->ignoreVCSIgnored(true) 15 | ->in(__DIR__) 16 | ; 17 | 18 | return (new PhpCsFixer\Config()) 19 | ->setRules([ 20 | '@PSR2' => true, 21 | 'function_declaration' => ['closure_function_spacing' => 'none'], 22 | 'no_whitespace_in_blank_line' => true, 23 | ]) 24 | ; 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | This changelog references the relevant changes (bug and security fixes). 5 | 6 | **Version 2.2.0 - 2021-01-11** 7 | 8 | * PHP 8 compatibility 9 | 10 | **Version 2.1.1 - 2019-10-20** 11 | 12 | * Allow to open WebP image with `ImageWorkshop::initFromPath` 13 | 14 | **Version 2.1.0 - 2018-04-17** 15 | 16 | * Add WebP support 17 | * Set Exiff PHP extension as optional 18 | * Fix PNG quality ratio 19 | * Fix image size detectio in `pasteImage` method 20 | 21 | **Version 2.0.9 - 2015-07-05** 22 | 23 | - Fix ImageWorkshop::initFromPath with remote URL 24 | 25 | **Version 2.0.8 - 2015-06-01** 26 | 27 | - Fix exception code when file not found 28 | 29 | **Version 2.0.7 - 2015-03-22** 30 | 31 | - Allow `ImageWorkshop::initFromPath` factory working with remote URL 32 | - Improve PHP >= 5.5 compatibility 33 | - Add `fixOrientation` method to layer to change image orientation based on EXIF orientation data 34 | - Fix background color when value is setting to "000000" 35 | 36 | **Version 2.0.6 - 2014-08-01** 37 | 38 | @jasny (https://github.com/jasny) contribution, new methods : 39 | 40 | * `ImageWorkshopLayer::resizeToFit()` resizes an image to fit a bounding box. 41 | * `ImageWorkshopLayer::cropToAspectRatio()` crops either to width or height of the document to match the aspect ratio. 42 | 43 | Documentation here : https://github.com/Sybio/ImageWorkshop/pull/37#issue-28704248 44 | 45 | **Version 2.0.5 - 2013-11-12** 46 | 47 | - Implementing interlace mode (http://php.net/manual/en/function.imageinterlace.php) on save() method to display progessive JPEG image 48 | 49 | ```php 50 | $interlace = true; // set true to enable interlace, false by default 51 | $layer->save($dirPath, $filename, $createFolders, $backgroundColor, $imageQuality, $interlace); 52 | ``` 53 | 54 | Thanks @dripolles (https://github.com/dripolles) & @johnhunt (https://github.com/johnhunt) 55 | 56 | **Version 2.0.4 - 2013-09-11** 57 | 58 | - Fix a major bug when resizing both sides AND conserving proportion : layer stack problem (current layer has a new 59 | nested level in its stack, not expected), and translations with positionX and positionY are wrong. 60 | Fixed. 61 | (Initial problem : https://github.com/Sybio/ImageWorkshop/pull/14) 62 | - Add a parameter to clearStack() method 63 | 64 | **Version 2.0.2 - 2013-06-14** 65 | 66 | - Fix a new bug : when resizing or cropping, small images can have 0 pixel of width or height (because of round), which 67 | is impossible and script crashes. Now width and height are 1 pixel minimum. 68 | 69 | Note: 70 | 71 | ```php 72 | $layer->resizeInPixel(null, 0 /* or negative number */, null); 73 | ``` 74 | 75 | It will generate a 1 pixel height image, not 0. 76 | 77 | **Version 2.0.1 - 2013-06-03** 78 | 79 | - Fix an opacity bug : pure black color (#000000) always displayed fully transparent (from 0 to 99% opacity). Bug fixed ! (no known bug anymore) 80 | - Add some Exceptions to help debugging 81 | 82 | **Version 2.0.0 - 2012-11-21** 83 | 84 | New version of ImageWorkshop ! The library is now divided in 3 main classes for cleaned code: 85 | - ImageWorkshopLayer: the class which represents a layer, that you manipulate 86 | - ImageWorkshop: a factory that is used to generate layers 87 | - ImageWorkshopLib: a class containing some tools (for calculations, etc...), used by both classes 88 | 89 | Technically, only the initialization change compared with the 1.3.x versions, check the documentation: 90 | http://phpimageworkshop.com/documentation.html#chapter-initialization-of-a-layer 91 | 92 | Here an example, before and now: 93 | ```php 94 | // before 95 | $layer = new ImageWorkshop(array( 96 | 'imageFromPath' => '/path/to/images/picture.jpg', 97 | )); 98 | ``` 99 | 100 | ```php 101 | // now 102 | $layer = ImageWorkshop::initFromPath('/path/to/images/picture.jpg'); 103 | ``` 104 | 105 | And also the installation of the class: http://phpimageworkshop.com/installation.html 106 | 107 | The documentation has been updated, you can now check the documentation of each version since 1.3.3: 108 | (Ex: http://phpimageworkshop.com/doc/9/initialize-from-an-image-file.html?version=2.0.0, http://phpimageworkshop.com/doc/9/initialize-from-an-image-file.html?version=1.3.3) 109 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests from everyone.If you'd like to contribute, please 4 | read the following: 5 | 6 | Fork, then clone the repo: 7 | 8 | git clone git@github.com:your-username/ImageWorkshop.git 9 | 10 | Set up your machine: 11 | 12 | composer install 13 | 14 | Make sure the tests pass: 15 | 16 | vendor/bin/phpunit 17 | 18 | Make your change. Add tests for your change. Make the tests pass: 19 | 20 | vendor/bin/phpunit 21 | 22 | Fix the code style using PHP-CS-Fixer: 23 | 24 | vendor/bin/php-cs-fixer fix --config=.php_cs . 25 | 26 | 27 | Push to your fork and submit a pull request. 28 | 29 | At this point you're waiting on us. We may suggest some changes or improvements 30 | or alternatives. 31 | 32 | Some things that will increase the chance that your pull request is accepted: 33 | 34 | * Write tests. 35 | * Follow our style guide. 36 | * Write a good commit message. 37 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Clément Guillemain (@Sybio01) 2 | 3 | http://en.wikipedia.org/wiki/MIT_License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ================================ 2 | # ImageWorkshop class 3 | # ================================ 4 | 5 | [![Test status](https://secure.travis-ci.org/Sybio/ImageWorkshop.png?branch=master)](https://travis-ci.org/Sybio/ImageWorkshop) 6 | [![Latest Stable Version](https://poser.pugx.org/sybio/image-workshop/v/stable)](https://packagist.org/packages/sybio/image-workshop) 7 | [![Total Downloads](https://poser.pugx.org/sybio/image-workshop/downloads)](https://packagist.org/packages/sybio/image-workshop) 8 | [![Monthly Downloads](https://poser.pugx.org/sybio/image-workshop/d/monthly)](https://packagist.org/packages/sybio/image-workshop) 9 | [![License](https://poser.pugx.org/sybio/image-workshop/license)](https://packagist.org/packages/sybio/image-workshop) 10 | 11 | ### Summary and features 12 | Really flexible and easy-to-use PHP class to work with images using the GD Library 13 | 14 | http://phpimageworkshop.com/ 15 | 16 | Current `master` branch correspond to the next major release (v3) which only support PHP 8.0+. 17 | 18 | ### Installation 19 | 20 | The class is designed for PHP 8.0+... Check how to install the class here: http://phpimageworkshop.com/installation.html 21 | 22 | For older PHP versions support, install the [2.x](https://github.com/Sybio/ImageWorkshop/tree/2.x) version branch. 23 | 24 | ### Usage 25 | 26 | - Learn how to use the class in 5 minutes: http://phpimageworkshop.com/quickstart.html 27 | - The complete documentation: http://phpimageworkshop.com/documentation.html 28 | - Usefull tutorials: http://phpimageworkshop.com/tutorials.html 29 | - Changelog: [CHANGELOG.md](CHANGELOG.md) 30 | 31 | **What's new in the doc' ?** 32 | 33 | - Installation guide: http://phpimageworkshop.com/installation.html 34 | - Adding the flip documentation: http://phpimageworkshop.com/doc/25/flip-vertical-horizontal-mirror.html 35 | - Adding the opacity documentation which was omitted: http://phpimageworkshop.com/doc/24/opacity-transparency.html 36 | - Tutorial "Manage animated GIF with ImageWorkshop (and GiFFrameExtractor & GifCreator)": http://phpimageworkshop.com/tutorial/5/manage-animated-gif-with-imageworkshop.html 37 | 38 | ### @todo 39 | - Adding a method to add easily borders to a layer (external, inside and middle border) 40 | - Check given hexa' color and remove # if exists. 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sybio/image-workshop", 3 | "type": "library", 4 | "description": "Powerful PHP class using GD library to work easily with images including layer notion (like Photoshop or GIMP)", 5 | "keywords": ["image", "thumbnail", "watermark", "resize", "crop", "rotate", "library", "GD", "class"], 6 | "homepage": "http://phpimageworkshop.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Clément Guillemain", 11 | "homepage": "http://clementguillemain.fr", 12 | "role": "Developer / Freelancer" 13 | }, 14 | { 15 | "name": "ImageWorkshop Community", 16 | "homepage": "https://github.com/Sybio/ImageWorkshop/graphs/contributors" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.0", 21 | "ext-fileinfo": "*", 22 | "ext-gd": "*" 23 | }, 24 | "require-dev": { 25 | "friendsofphp/php-cs-fixer": "^3.4", 26 | "phpunit/phpunit": "^8.5.13", 27 | "phpstan/phpstan": "^1.3", 28 | "rector/rector": "^0.12.23" 29 | }, 30 | "suggest": { 31 | "ext-exif": "Allows to read and keep images EXIF data" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "PHPImageWorkshop\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "PHPImageWorkshop\\Tests\\": "tests/" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 17 | __DIR__ . '/src' 18 | ]); 19 | 20 | $config->import(SetList::DEAD_CODE); 21 | $config->import(SetList::TYPE_DECLARATION_STRICT); 22 | $config->import(SetList::TYPE_DECLARATION); 23 | $config->import(SetList::PHP_80); 24 | $config->import(SetList::PHP_74); 25 | $config->import(SetList::PHP_73); 26 | $config->import(SetList::EARLY_RETURN); 27 | $config->import(SetList::CODE_QUALITY); 28 | 29 | // register a single rule 30 | $config->rule(InlineConstructorDefaultToPropertyRector::class); 31 | $config->rule(RemoveExtraParametersRector::class); 32 | 33 | // define sets of rules 34 | $config->sets([ 35 | LevelSetList::UP_TO_PHP_80 36 | ]); 37 | 38 | $config->phpstanConfig(__DIR__ . '/phpstan.neon'); 39 | }; 40 | -------------------------------------------------------------------------------- /src/Core/Exception/ImageWorkshopLayerException.php: -------------------------------------------------------------------------------- 1 | 55 | * 56 | * Positions (x and y) of the sublayers in the stack 57 | */ 58 | protected array $layerPositions; 59 | 60 | /** 61 | * Id of the last indexed sublayer in the stack 62 | */ 63 | protected int $lastLayerId; 64 | 65 | /** 66 | * The highest sublayer level 67 | */ 68 | protected int $highestLayerLevel; 69 | 70 | /** 71 | * Background Image 72 | */ 73 | protected GdImage $image; 74 | 75 | /** 76 | * @var array 77 | * 78 | * Exif data 79 | */ 80 | protected array $exif; 81 | 82 | /** 83 | * @var string 84 | */ 85 | public const UNIT_PIXEL = 'pixel'; 86 | 87 | /** 88 | * @var string 89 | */ 90 | public const UNIT_PERCENT = 'percent'; 91 | 92 | /** 93 | * @var int 94 | */ 95 | public const ERROR_GD_NOT_INSTALLED = 1; 96 | 97 | /** 98 | * @var int 99 | */ 100 | public const ERROR_PHP_IMAGE_VAR_NOT_USED = 2; 101 | 102 | /** 103 | * @var int 104 | */ 105 | public const ERROR_FONT_NOT_FOUND = 3; 106 | 107 | /** 108 | * @var int 109 | */ 110 | public const METHOD_DEPRECATED = 4; 111 | 112 | /** 113 | * @var int 114 | */ 115 | public const ERROR_NEGATIVE_NUMBER_USED = 5; 116 | 117 | /** 118 | * @var int 119 | */ 120 | public const ERROR_NOT_WRITABLE_FOLDER = 6; 121 | 122 | /** 123 | * @var int 124 | */ 125 | public const ERROR_NOT_SUPPORTED_FORMAT = 7; 126 | 127 | /** 128 | * @var int 129 | */ 130 | public const ERROR_UNKNOW = 8; 131 | 132 | // =================================================================================== 133 | // Methods 134 | // =================================================================================== 135 | 136 | // Magicals 137 | // ========================================================= 138 | 139 | public function __construct(GdImage $image, array $exif = array()) 140 | { 141 | if (!extension_loaded('gd')) { 142 | throw new ImageWorkshopLayerException('PHPImageWorkshop requires the GD extension to be loaded.', static::ERROR_GD_NOT_INSTALLED); 143 | } 144 | 145 | $this->width = imagesx($image); 146 | $this->height = imagesy($image); 147 | $this->image = $image; 148 | $this->exif = $exif; 149 | $this->layers = $this->layerLevels = $this->layerPositions = array(); 150 | $this->clearStack(); 151 | } 152 | 153 | /** 154 | * Clone method: use it if you want to reuse an existing ImageWorkshop object in another variable 155 | * This is important because img resource var references all the same image in PHP. 156 | * Example: $b = clone $a; (never do $b = $a;) 157 | */ 158 | public function __clone() 159 | { 160 | $this->createNewVarFromBackgroundImage(); 161 | } 162 | 163 | // Superimpose a sublayer 164 | // ========================================================= 165 | 166 | /** 167 | * Add an existing ImageWorkshop sublayer and set it in the stack at a given level 168 | * Return an array containing the generated sublayer id in the stack and its corrected level: 169 | * array("layerLevel" => integer, "id" => integer) 170 | * 171 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 172 | * 173 | * @return array{layerLevel: int, id: int} 174 | */ 175 | public function addLayer(int $layerLevel, ImageWorkshopLayer $layer, int $positionX = 0, int $positionY = 0, string $position = 'LT'): array 176 | { 177 | return $this->indexLayer($layerLevel, $layer, $positionX, $positionY, $position); 178 | } 179 | 180 | /** 181 | * Add an existing ImageWorkshop sublayer and set it in the stack at the highest level 182 | * Return an array containing the generated sublayer id in the stack and the highest level: 183 | * array("layerLevel" => integer, "id" => integer) 184 | * 185 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 186 | * 187 | * @return array{layerLevel: int, id: int} 188 | */ 189 | public function addLayerOnTop(ImageWorkshopLayer $layer, int $positionX = 0, int $positionY = 0, string $position = 'LT') 190 | { 191 | return $this->indexLayer($this->highestLayerLevel + 1, $layer, $positionX, $positionY, $position); 192 | } 193 | 194 | /** 195 | * Add an existing ImageWorkshop sublayer and set it in the stack at level 1 196 | * Return an array containing the generated sublayer id in the stack and level 1: 197 | * array("layerLevel" => integer, "id" => integer) 198 | * 199 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 200 | * 201 | * @return array{layerLevel: int, id: int} 202 | */ 203 | public function addLayerBelow(ImageWorkshopLayer $layer, int $positionX = 0, int $positionY = 0, string $position = 'LT') 204 | { 205 | return $this->indexLayer(1, $layer, $positionX, $positionY, $position); 206 | } 207 | 208 | // Move a sublayer inside the stack 209 | // ========================================================= 210 | 211 | /** 212 | * Move a sublayer on the top of a group stack 213 | * Return new sublayer level if success or false otherwise 214 | * 215 | * @return int|false 216 | */ 217 | public function moveTop(int $layerId): int|bool 218 | { 219 | return $this->moveTo($layerId, $this->highestLayerLevel, false); 220 | } 221 | 222 | /** 223 | * Move a sublayer to the level 1 of a group stack 224 | * Return new sublayer level if success or false otherwise 225 | * 226 | * @return int|false 227 | */ 228 | public function moveBottom(int $layerId): int|bool 229 | { 230 | return $this->moveTo($layerId, 1, true); 231 | } 232 | 233 | /** 234 | * Move a sublayer to the level $level of a group stack 235 | * Return new sublayer level if success or false if layer isn't found 236 | * 237 | * Set $insertUnderTargetedLayer true if you want to move the sublayer under the other sublayer at the targeted level, 238 | * or false to insert it on the top of the other sublayer at the targeted level 239 | * 240 | * @return int|false 241 | */ 242 | public function moveTo(int $layerId, int $level, bool $insertUnderTargetedLayer = true) 243 | { 244 | // if the sublayer exists in stack 245 | if ($this->isLayerInIndex($layerId)) { 246 | $layerOldLevel = $this->getLayerLevel($layerId); 247 | 248 | if ($level < 1) { 249 | $level = 1; 250 | $insertUnderTargetedLayer = true; 251 | } 252 | 253 | if ($level > $this->highestLayerLevel) { 254 | $level = $this->highestLayerLevel; 255 | $insertUnderTargetedLayer = false; 256 | } 257 | 258 | // Not the same level than the current level 259 | if ($layerOldLevel !== $level) { 260 | $isUnderAndNewLevelHigher = $isUnderAndNewLevelLower = $isOnTopAndNewLevelHigher = $isOnTopAndNewLevelLower = false; 261 | 262 | if ($insertUnderTargetedLayer) { // Under level 263 | 264 | if ($level > $layerOldLevel) { // new level higher 265 | 266 | $incrementorStartingValue = $layerOldLevel; 267 | $stopLoopWhenSmallerThan = $level; 268 | $isUnderAndNewLevelHigher = true; 269 | } else { // new level lower 270 | 271 | $incrementorStartingValue = $level; 272 | $stopLoopWhenSmallerThan = $layerOldLevel; 273 | $isUnderAndNewLevelLower = true; 274 | } 275 | } else { // on the top 276 | 277 | if ($level > $layerOldLevel) { // new level higher 278 | 279 | $incrementorStartingValue = $layerOldLevel; 280 | $stopLoopWhenSmallerThan = $level; 281 | $isOnTopAndNewLevelHigher = true; 282 | } else { // new level lower 283 | 284 | $incrementorStartingValue = $level; 285 | $stopLoopWhenSmallerThan = $layerOldLevel; 286 | $isOnTopAndNewLevelLower = true; 287 | } 288 | } 289 | 290 | ksort($this->layerLevels); 291 | $layerLevelsTmp = $this->layerLevels; 292 | 293 | if ($isOnTopAndNewLevelLower) { 294 | $level++; 295 | } 296 | 297 | for ($i = $incrementorStartingValue; $i < $stopLoopWhenSmallerThan; $i++) { 298 | if ($isUnderAndNewLevelHigher || $isOnTopAndNewLevelHigher) { 299 | $this->layerLevels[$i] = $layerLevelsTmp[$i + 1]; 300 | } else { 301 | $this->layerLevels[$i + 1] = $layerLevelsTmp[$i]; 302 | } 303 | } 304 | 305 | unset($layerLevelsTmp); 306 | 307 | if ($isUnderAndNewLevelHigher) { 308 | $level--; 309 | } 310 | 311 | $this->layerLevels[$level] = $layerId; 312 | 313 | return $level; 314 | } else { 315 | return $level; 316 | } 317 | } 318 | 319 | return false; 320 | } 321 | 322 | /** 323 | * Move up a sublayer in the stack (level +1) 324 | * Return new sublayer level if success, false otherwise 325 | * 326 | * @return int|false 327 | */ 328 | public function moveUp(int $layerId): int|bool 329 | { 330 | if ($this->isLayerInIndex($layerId)) { // if the sublayer exists in the stack 331 | $layerOldLevel = $this->getLayerLevel($layerId); 332 | return $this->moveTo($layerId, $layerOldLevel + 1, false); 333 | } 334 | 335 | return false; 336 | } 337 | 338 | /** 339 | * Move down a sublayer in the stack (level -1) 340 | * Return new sublayer level if success, false otherwise 341 | * 342 | * @return int|false 343 | */ 344 | public function moveDown(int $layerId): int|bool 345 | { 346 | if ($this->isLayerInIndex($layerId)) { // if the sublayer exists in the stack 347 | $layerOldLevel = $this->getLayerLevel($layerId); 348 | return $this->moveTo($layerId, $layerOldLevel - 1, true); 349 | } 350 | 351 | return false; 352 | } 353 | 354 | // Merge layers 355 | // ========================================================= 356 | 357 | /** 358 | * Merge a sublayer with another sublayer below it in the stack 359 | * Note: the result layer will conserve the given id 360 | * Return true if success or false if layer isn't found or doesn't have a layer under it in the stack 361 | */ 362 | public function mergeDown(int $layerId): bool 363 | { 364 | // if the layer exists in document 365 | if ($this->isLayerInIndex($layerId)) { 366 | $layerLevel = $this->getLayerLevel($layerId); 367 | $layer = $this->getLayer($layerId); 368 | $layerWidth = $layer->getWidth(); 369 | $layerHeight = $layer->getHeight(); 370 | $layerPositionX = $this->layerPositions[$layerId]['x']; 371 | $layerPositionY = $this->layerPositions[$layerId]['y']; 372 | 373 | if ($layerLevel > 1) { 374 | $underLayerId = $this->layerLevels[$layerLevel - 1]; 375 | $underLayer = $this->getLayer($underLayerId); 376 | $underLayerWidth = $underLayer->getWidth(); 377 | $underLayerHeight = $underLayer->getHeight(); 378 | $underLayerPositionX = $this->layerPositions[$underLayerId]['x']; 379 | $underLayerPositionY = $this->layerPositions[$underLayerId]['y']; 380 | 381 | $totalWidthLayer = $layerWidth + $layerPositionX; 382 | $totalHeightLayer = $layerHeight + $layerPositionY; 383 | 384 | $totalWidthUnderLayer = $underLayerWidth + $underLayerPositionX; 385 | $totalHeightUnderLayer = $underLayerHeight + $underLayerPositionY; 386 | 387 | $minLayerPositionX = $layerPositionX; 388 | 389 | if ($layerPositionX > $underLayerPositionX) { 390 | $minLayerPositionX = $underLayerPositionX; 391 | } 392 | 393 | $minLayerPositionY = $layerPositionY; 394 | 395 | if ($layerPositionY > $underLayerPositionY) { 396 | $minLayerPositionY = $underLayerPositionY; 397 | } 398 | 399 | if ($totalWidthLayer > $totalWidthUnderLayer) { 400 | $layerTmpWidth = $totalWidthLayer - $minLayerPositionX; 401 | } else { 402 | $layerTmpWidth = $totalWidthUnderLayer - $minLayerPositionX; 403 | } 404 | 405 | if ($totalHeightLayer > $totalHeightUnderLayer) { 406 | $layerTmpHeight = $totalHeightLayer - $minLayerPositionY; 407 | } else { 408 | $layerTmpHeight = $totalHeightUnderLayer - $minLayerPositionY; 409 | } 410 | 411 | $layerTmp = ImageWorkshop::initVirginLayer($layerTmpWidth, $layerTmpHeight); 412 | 413 | $layerTmp->addLayer(1, $underLayer, $underLayerPositionX - $minLayerPositionX, $underLayerPositionY - $minLayerPositionY); 414 | $layerTmp->addLayer(2, $layer, $layerPositionX - $minLayerPositionX, $layerPositionY - $minLayerPositionY); 415 | 416 | // Update layers 417 | $layerTmp->mergeAll(); 418 | $this->layers[$underLayerId] = clone $layerTmp; 419 | $this->changePosition($underLayerId, $minLayerPositionX, $minLayerPositionX); 420 | } else { 421 | $layerTmp = ImageWorkshop::initFromResourceVar($this->image); 422 | $layerTmp->addLayer(1, $layer, $layerPositionX, $layerPositionY); 423 | 424 | $this->image = $layerTmp->getResult(); // Update background image 425 | } 426 | 427 | unset($layerTmp); 428 | $this->remove($layerId); // Remove the merged layer from the stack 429 | 430 | return true; 431 | } 432 | 433 | return false; 434 | } 435 | 436 | /** 437 | * Merge sublayers in the stack on the layer background 438 | */ 439 | public function mergeAll(): void 440 | { 441 | $this->image = $this->getResult(); 442 | $this->clearStack(); 443 | } 444 | 445 | /** 446 | * Paste an image on the layer 447 | * You can specify the position left (in pixels) and the position top (in pixels) of the added image relatives to the layer 448 | * Otherwise, it will be set at 0 and 0 449 | * 450 | * @param string $unit Use one of `UNIT_*` constants, "UNIT_PIXEL" by default 451 | */ 452 | public function pasteImage(string $unit, GdImage $image, int $positionX = 0, int $positionY = 0): void 453 | { 454 | if (!in_array($unit, [self::UNIT_PIXEL, self::UNIT_PERCENT])) { 455 | throw ImageWorkshopException::invalidUnitArgument(); 456 | } 457 | 458 | if ($unit === self::UNIT_PERCENT) { 459 | $positionX = (int) round(($positionX / 100) * $this->width); 460 | $positionY = (int) round(($positionY / 100) * $this->height); 461 | } 462 | 463 | imagecopy($this->image, $image, $positionX, $positionY, 0, 0, imagesx($image), imagesy($image)); 464 | } 465 | 466 | // Change sublayer positions 467 | // ========================================================= 468 | 469 | /** 470 | * Change the position of a sublayer for new positions 471 | */ 472 | public function changePosition(int $layerId, int $newPosX = null, int $newPosY = null): bool 473 | { 474 | // if the sublayer exists in the stack 475 | if ($this->isLayerInIndex($layerId)) { 476 | if ($newPosX !== null) { 477 | $this->layerPositions[$layerId]['x'] = $newPosX; 478 | } 479 | 480 | if ($newPosY !== null) { 481 | $this->layerPositions[$layerId]['y'] = $newPosY; 482 | } 483 | 484 | return true; 485 | } 486 | 487 | return false; 488 | } 489 | 490 | /** 491 | * Apply a translation on a sublayer that change its positions 492 | * 493 | * @return array{x: int, y: int}|false 494 | */ 495 | public function applyTranslation(int $layerId, int $addedPosX = null, int $addedPosY = null): array|bool 496 | { 497 | // if the sublayer exists in the stack 498 | if ($this->isLayerInIndex($layerId)) { 499 | if ($addedPosX !== null) { 500 | $this->layerPositions[$layerId]['x'] += $addedPosX; 501 | } 502 | 503 | if ($addedPosY !== null) { 504 | $this->layerPositions[$layerId]['y'] += $addedPosY; 505 | } 506 | 507 | return $this->layerPositions[$layerId]; 508 | } 509 | 510 | return false; 511 | } 512 | 513 | // Removing sublayers 514 | // ========================================================= 515 | 516 | /** 517 | * Delete a layer (return true if success, false if no sublayer is found) 518 | */ 519 | public function remove(int $layerId): bool 520 | { 521 | // if the layer exists in document 522 | if ($this->isLayerInIndex($layerId)) { 523 | $layerToDeleteLevel = $this->getLayerLevel($layerId); 524 | 525 | // delete 526 | $this->layers[$layerId]->delete(); 527 | unset($this->layers[$layerId]); 528 | unset($this->layerLevels[$layerToDeleteLevel]); 529 | unset($this->layerPositions[$layerId]); 530 | 531 | // One or plural layers are sub of the deleted layer 532 | if (array_key_exists(($layerToDeleteLevel + 1), $this->layerLevels)) { 533 | ksort($this->layerLevels); 534 | 535 | $layerLevelsTmp = $this->layerLevels; 536 | 537 | $maxOldestLevel = 1; 538 | foreach ($layerLevelsTmp as $levelTmp => $layerIdTmp) { 539 | if ($levelTmp > $layerToDeleteLevel) { 540 | $this->layerLevels[($levelTmp - 1)] = $layerIdTmp; 541 | } 542 | 543 | $maxOldestLevel++; 544 | } 545 | unset($layerLevelsTmp); 546 | unset($this->layerLevels[$maxOldestLevel]); 547 | } 548 | 549 | $this->highestLayerLevel--; 550 | 551 | return true; 552 | } 553 | 554 | return false; 555 | } 556 | 557 | /** 558 | * Reset the layer stack 559 | * 560 | * @param bool $deleteSubImgVar Delete sublayers image resource var 561 | */ 562 | public function clearStack(bool $deleteSubImgVar = true): void 563 | { 564 | if ($deleteSubImgVar) { 565 | foreach ($this->layers as $layer) { 566 | $layer->delete(); 567 | } 568 | } 569 | 570 | unset($this->layers); 571 | unset($this->layerLevels); 572 | unset($this->layerPositions); 573 | 574 | $this->lastLayerId = 0; 575 | $this->layers = array(); 576 | $this->layerLevels = array(); 577 | $this->layerPositions = array(); 578 | $this->highestLayerLevel = 0; 579 | } 580 | 581 | // Perform an action 582 | // ========================================================= 583 | 584 | /** 585 | * Resize the layer by specifying pixel 586 | * 587 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 588 | * 589 | * $positionX, $positionY, $position can be ignored unless you choose a new width AND a new height AND to conserve proportion. 590 | */ 591 | public function resizeInPixel(int $newWidth = null, int $newHeight = null, bool $converseProportion = false, int $positionX = 0, int $positionY = 0, string $position = 'MM'): void 592 | { 593 | $this->resize(self::UNIT_PIXEL, $newWidth, $newHeight, $converseProportion, $positionX, $positionY, $position); 594 | } 595 | 596 | /** 597 | * Resize the layer by specifying a percent 598 | * 599 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 600 | * 601 | * $positionX, $positionY, $position can be ignored unless you choose a new width AND a new height AND to conserve proportion. 602 | */ 603 | public function resizeInPercent(float $percentWidth = null, float $percentHeight = null, bool $converseProportion = false, int $positionX = 0, int $positionY = 0, string $position = 'MM'): void 604 | { 605 | $this->resize(self::UNIT_PERCENT, $percentWidth, $percentHeight, $converseProportion, $positionX, $positionY, $position); 606 | } 607 | 608 | /** 609 | * Resize the layer to fit a bounding box by specifying pixel 610 | */ 611 | public function resizeToFit(int $width, int $height, bool $converseProportion = false): void 612 | { 613 | if ($this->getWidth() <= $width && $this->getHeight() <= $height) { 614 | return; 615 | } 616 | 617 | if (!$converseProportion) { 618 | $width = min($width, $this->getWidth()); 619 | $height = min($height, $this->getHeight()); 620 | } 621 | 622 | $this->resize(self::UNIT_PIXEL, $width, $height, $converseProportion, createNewLayer: false); // Fix bug here 623 | } 624 | 625 | /** 626 | * Resize the layer 627 | * 628 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 629 | * 630 | * $positionX, $positionY, $position can be ignored unless you choose a new width AND a new height AND to conserve proportion. 631 | */ 632 | public function resize(string $unit = self::UNIT_PIXEL, int|float $newWidth = null, int|float $newHeight = null, bool $converseProportion = false, int $positionX = 0, int $positionY = 0, string $position = 'MM', bool $createNewLayer = true): void 633 | { 634 | if (is_numeric($newWidth) || is_numeric($newHeight)) { 635 | $widthResizePercent = 100; 636 | $heightResizePercent = 100; 637 | 638 | if ($unit === self::UNIT_PERCENT) { 639 | if ($newWidth) { 640 | $newWidth = (int) round(($newWidth / 100) * $this->width); 641 | } 642 | 643 | if ($newHeight) { 644 | $newHeight = (int) round(($newHeight / 100) * $this->height); 645 | } 646 | } 647 | 648 | if (is_numeric($newWidth) && $newWidth <= 0) { 649 | $newWidth = 1; 650 | } 651 | 652 | if (is_numeric($newHeight) && $newHeight <= 0) { 653 | $newHeight = 1; 654 | } 655 | 656 | if ($converseProportion) { // Proportion are conserved 657 | 658 | if ($newWidth && $newHeight) { // Proportions + $newWidth + $newHeight 659 | 660 | if ($this->getWidth() > $this->getHeight()) { 661 | $this->resizeInPixel($newWidth, null, true); 662 | 663 | if ($this->getHeight() > $newHeight) { 664 | $this->resizeInPixel(null, $newHeight, true); 665 | } 666 | } else { 667 | $this->resizeInPixel(null, $newHeight, true); 668 | 669 | if ($this->getWidth() > $newWidth) { 670 | $this->resizeInPixel($newWidth, null, true); 671 | } 672 | } 673 | 674 | if ($converseProportion && $createNewLayer && ($this->getWidth() !== $newWidth || $this->getHeight() !== $newHeight)) { 675 | $layerTmp = ImageWorkshop::initVirginLayer($newWidth, $newHeight); 676 | 677 | $layerTmp->addLayer(1, $this, $positionX, $positionY, $position); 678 | 679 | // Reset part of stack 680 | 681 | unset($this->image); 682 | unset($this->layerLevels); 683 | unset($this->layerPositions); 684 | unset($this->layers); 685 | 686 | // Update current object 687 | 688 | $this->width = $layerTmp->getWidth(); 689 | $this->height = $layerTmp->getHeight(); 690 | $this->layerLevels = $layerTmp->layers[1]->getLayerLevels(); 691 | $this->layerPositions = $layerTmp->layers[1]->getLayerPositions(); 692 | $this->layers = $layerTmp->layers[1]->getLayers(); 693 | $this->lastLayerId = $layerTmp->layers[1]->getLastLayerId(); 694 | $this->highestLayerLevel = $layerTmp->layers[1]->getHighestLayerLevel(); 695 | 696 | $translations = $layerTmp->getLayerPosition(1); 697 | 698 | foreach ($this->layers as $id => $layer) { 699 | $this->applyTranslation($id, $translations['x'], $translations['y']); 700 | } 701 | 702 | $layerTmp->layers[1]->clearStack(false); 703 | $this->image = $layerTmp->getResult(); 704 | unset($layerTmp); 705 | } 706 | 707 | return; 708 | } elseif ($newWidth) { 709 | $widthResizePercent = $newWidth / ($this->width / 100); 710 | $newHeight = (int) round(($widthResizePercent / 100) * $this->height); 711 | $heightResizePercent = $widthResizePercent; 712 | } elseif ($newHeight) { 713 | $heightResizePercent = $newHeight / ($this->height / 100); 714 | $newWidth = (int) round(($heightResizePercent / 100) * $this->width); 715 | $widthResizePercent = $heightResizePercent; 716 | } 717 | } elseif (($newWidth && !$newHeight) || (!$newWidth && $newHeight)) { // New width OR new height is given 718 | 719 | if ($newWidth) { 720 | $widthResizePercent = $newWidth / ($this->width / 100); 721 | $heightResizePercent = 100; 722 | $newHeight = $this->height; 723 | } else { 724 | $heightResizePercent = $newHeight / ($this->height / 100); 725 | $widthResizePercent = 100; 726 | $newWidth = $this->width; 727 | } 728 | } else { // New width AND new height are given 729 | 730 | $widthResizePercent = $newWidth / ($this->width / 100); 731 | $heightResizePercent = $newHeight / ($this->height / 100); 732 | } 733 | 734 | // Update the layer positions in the stack 735 | 736 | foreach ($this->layerPositions as $layerId => $layerPosition) { 737 | $newPosX = (int) round(($widthResizePercent / 100) * $layerPosition['x']); 738 | $newPosY = (int) round(($heightResizePercent / 100) * $layerPosition['y']); 739 | 740 | $this->changePosition($layerId, $newPosX, $newPosY); 741 | } 742 | 743 | // Resize layers in the stack 744 | 745 | $layers = $this->layers; 746 | 747 | foreach ($layers as $key => $layer) { 748 | $layer->resizeInPercent($widthResizePercent, $heightResizePercent); 749 | $this->layers[$key] = $layer; 750 | } 751 | 752 | $this->resizeBackground($newWidth, $newHeight); // Resize the layer 753 | } 754 | } 755 | 756 | /** 757 | * Resize the layer by its largest side by specifying pixel 758 | */ 759 | public function resizeByLargestSideInPixel(int $newLargestSideWidth, bool $converseProportion = false): void 760 | { 761 | $this->resizeByLargestSide(self::UNIT_PIXEL, $newLargestSideWidth, $converseProportion); 762 | } 763 | 764 | /** 765 | * Resize the layer by its largest side by specifying percent 766 | */ 767 | public function resizeByLargestSideInPercent(int $newLargestSideWidth, bool $converseProportion = false): void 768 | { 769 | $this->resizeByLargestSide(self::UNIT_PERCENT, $newLargestSideWidth, $converseProportion); 770 | } 771 | 772 | /** 773 | * Resize the layer by its largest side 774 | * 775 | * @param int $newLargestSideWidth percent 776 | */ 777 | public function resizeByLargestSide(string $unit, int $newLargestSideWidth, bool $converseProportion = false): void 778 | { 779 | if (!in_array($unit, [self::UNIT_PIXEL, self::UNIT_PERCENT])) { 780 | throw ImageWorkshopException::invalidUnitArgument(); 781 | } 782 | 783 | if ($unit === self::UNIT_PERCENT) { 784 | $newLargestSideWidth = (int) round(($newLargestSideWidth / 100) * $this->getLargestSideWidth()); 785 | } 786 | 787 | if ($this->getWidth() > $this->getHeight()) { 788 | $this->resizeInPixel($newLargestSideWidth, null, $converseProportion); 789 | } else { 790 | $this->resizeInPixel(null, $newLargestSideWidth, $converseProportion); 791 | } 792 | } 793 | 794 | /** 795 | * Resize the layer by its narrow side by specifying pixel 796 | */ 797 | public function resizeByNarrowSideInPixel(int $newNarrowSideWidth, bool $converseProportion = false): void 798 | { 799 | $this->resizeByNarrowSide(self::UNIT_PIXEL, $newNarrowSideWidth, $converseProportion); 800 | } 801 | 802 | /** 803 | * Resize the layer by its narrow side by specifying percent 804 | * 805 | * @param int $newNarrowSideWidth percent 806 | */ 807 | public function resizeByNarrowSideInPercent(int $newNarrowSideWidth, bool $converseProportion = false): void 808 | { 809 | $this->resizeByNarrowSide(self::UNIT_PERCENT, $newNarrowSideWidth, $converseProportion); 810 | } 811 | 812 | /** 813 | * Resize the layer by its narrow side 814 | */ 815 | public function resizeByNarrowSide(string $unit, int $newNarrowSideWidth, bool $converseProportion = false): void 816 | { 817 | if (!in_array($unit, [self::UNIT_PIXEL, self::UNIT_PERCENT])) { 818 | throw ImageWorkshopException::invalidUnitArgument(); 819 | } 820 | 821 | if ($unit === self::UNIT_PERCENT) { 822 | $newNarrowSideWidth = (int) round(($newNarrowSideWidth / 100) * $this->getNarrowSideWidth()); 823 | } 824 | 825 | if ($this->getWidth() < $this->getHeight()) { 826 | $this->resizeInPixel($newNarrowSideWidth, null, $converseProportion); 827 | } else { 828 | $this->resizeInPixel(null, $newNarrowSideWidth, $converseProportion); 829 | } 830 | } 831 | 832 | /** 833 | * Crop the document by specifying pixels 834 | * 835 | * $backgroundColor: can be set transparent (The script will be longer to execute) 836 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 837 | */ 838 | public function cropInPixel(int $width = 0, int $height = 0, int $positionX = 0, int $positionY = 0, string $position = 'LT'): void 839 | { 840 | $this->crop(self::UNIT_PIXEL, $width, $height, $positionX, $positionY, $position); 841 | } 842 | 843 | /** 844 | * Crop the document by specifying percent 845 | * 846 | * $backgroundColor can be set transparent (but script could be long to execute) 847 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 848 | */ 849 | public function cropInPercent(float $percentWidth = 0.0, float $percentHeight = 0.0, float $positionXPercent = 0.0, float $positionYPercent = 0.0, string $position = 'LT'): void 850 | { 851 | $this->crop(self::UNIT_PERCENT, $percentWidth, $percentHeight, $positionXPercent, $positionYPercent, $position); 852 | } 853 | 854 | /** 855 | * Crop the document 856 | * 857 | * $backgroundColor can be set transparent (but script could be long to execute) 858 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 859 | */ 860 | public function crop(string $unit = self::UNIT_PIXEL, int|float $width = 0, int|float $height = 0, int|float $positionX = 0, int|float $positionY = 0, string $position = 'LT'): void 861 | { 862 | if ($width < 0 || $height < 0) { 863 | throw new ImageWorkshopLayerException('You can\'t use negative $width or $height for "'.__METHOD__.'" method.', static::ERROR_NEGATIVE_NUMBER_USED); 864 | } 865 | 866 | if ($unit === self::UNIT_PERCENT) { 867 | $width = (int) round(($width / 100) * $this->width); 868 | $height = (int) round(($height / 100) * $this->height); 869 | 870 | $positionX = (int) round(($positionX / 100) * $this->width); 871 | $positionY = (int) round(($positionY / 100) * $this->height); 872 | } 873 | 874 | if (($width !== $this->width || $positionX === 0) || ($height !== $this->height || $positionY === 0)) { 875 | if ($width === 0) { 876 | $width = 1; 877 | } 878 | 879 | if ($height === 0) { 880 | $height = 1; 881 | } 882 | 883 | $layerTmp = ImageWorkshop::initVirginLayer($width, $height); 884 | $layerClone = ImageWorkshop::initVirginLayer($this->width, $this->height); 885 | 886 | imagedestroy($layerClone->image); 887 | $layerClone->image = $this->image; 888 | 889 | $layerTmp->addLayer(1, $layerClone, -$positionX, -$positionY, $position); 890 | 891 | $newPos = $layerTmp->getLayerPositions(); 892 | $layerNewPosX = $newPos[1]['x']; 893 | $layerNewPosY = $newPos[1]['y']; 894 | 895 | // update the layer 896 | $this->width = $layerTmp->getWidth(); 897 | $this->height = $layerTmp->getHeight(); 898 | $this->image = $layerTmp->getResult(); 899 | unset($layerTmp); 900 | unset($layerClone); 901 | 902 | $this->updateLayerPositionsAfterCropping($layerNewPosX, $layerNewPosY); 903 | } 904 | } 905 | 906 | /** 907 | * Crop the document to a specific aspect ratio by specifying a shift in pixel 908 | * 909 | * $backgroundColor: can be set transparent (The script will be longer to execute) 910 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 911 | */ 912 | public function cropToAspectRatioInPixel(int $width = 0, int $height = 0, int $positionX = 0, int $positionY = 0, string $position = 'LT'): void 913 | { 914 | $this->cropToAspectRatio(self::UNIT_PIXEL, $width, $height, $positionX, $positionY, $position); 915 | } 916 | 917 | /** 918 | * Crop the document to a specific aspect ratio by specifying a shift in percent 919 | * 920 | * $backgroundColor can be set transparent (but script could be long to execute) 921 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 922 | */ 923 | public function cropToAspectRatioInPercent(int $width = 0, int $height = 0, float $positionXPercent = 0.0, float $positionYPercent = 0.0, string $position = 'LT'): void 924 | { 925 | $this->cropToAspectRatio(self::UNIT_PERCENT, $width, $height, $positionXPercent, $positionYPercent, $position); 926 | } 927 | 928 | /** 929 | * Crop the document to a specific aspect ratio 930 | * 931 | * $backgroundColor can be set transparent (but script could be long to execute) 932 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 933 | */ 934 | public function cropToAspectRatio(string $unit = self::UNIT_PIXEL, int $width = 0, int $height = 0, int|float $positionX = 0, int|float $positionY = 0, string $position = 'LT'): void 935 | { 936 | if ($width < 0 || $height < 0) { 937 | throw new ImageWorkshopLayerException('You can\'t use negative $width or $height for "'.__METHOD__.'" method.', static::ERROR_NEGATIVE_NUMBER_USED); 938 | } 939 | 940 | if ($width === 0) { 941 | $width = 1; 942 | } 943 | 944 | if ($height === 0) { 945 | $height = 1; 946 | } 947 | 948 | if ($this->width / $this->height <= $width / $height) { 949 | $newWidth = $this->width; 950 | $newHeight = round($height * ($this->width / $width)); 951 | } else { 952 | $newWidth = round($width * ($this->height / $height)); 953 | $newHeight = $this->height; 954 | } 955 | 956 | if ($unit === self::UNIT_PERCENT) { 957 | $positionX = (int) round(($positionX / 100) * ($this->width - $newWidth)); 958 | $positionY = (int) round(($positionY / 100) * ($this->height - $newHeight)); 959 | } 960 | 961 | $this->cropInPixel($newWidth, $newHeight, $positionX, $positionY, $position); 962 | } 963 | 964 | /** 965 | * Crop the maximum possible from left top ("LT"), "RT"... by specifying a shift in pixel 966 | * 967 | * $backgroundColor can be set transparent (but script could be long to execute) 968 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 969 | */ 970 | public function cropMaximumInPixel(int $positionX = 0, int $positionY = 0, string $position = 'LT'): void 971 | { 972 | $this->cropMaximum(self::UNIT_PIXEL, $positionX, $positionY, $position); 973 | } 974 | 975 | /** 976 | * Crop the maximum possible from left top ("LT"), "RT"... by specifying a shift in percent 977 | * 978 | * $backgroundColor can be set transparent (but script could be long to execute) 979 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 980 | */ 981 | public function cropMaximumInPercent(int $positionXPercent = 0, int $positionYPercent = 0, $position = 'LT'): void 982 | { 983 | $this->cropMaximum(self::UNIT_PERCENT, $positionXPercent, $positionYPercent, $position); 984 | } 985 | 986 | /** 987 | * Crop the maximum possible from left top 988 | * 989 | * $backgroundColor can be set transparent (but script could be long to execute) 990 | * $position: http://phpimageworkshop.com/doc/22/corners-positions-schema-of-an-image.html 991 | */ 992 | public function cropMaximum(string $unit = self::UNIT_PIXEL, int $positionX = 0, int $positionY = 0, string $position = 'LT'): void 993 | { 994 | $narrowSide = $this->getNarrowSideWidth(); 995 | 996 | if ($unit === self::UNIT_PERCENT) { 997 | $positionX = (int) round(($positionX / 100) * $this->width); 998 | $positionY = (int) round(($positionY / 100) * $this->height); 999 | } 1000 | 1001 | $this->cropInPixel($narrowSide, $narrowSide, $positionX, $positionY, $position); 1002 | } 1003 | 1004 | /** 1005 | * Rotate the layer (in degree) 1006 | */ 1007 | public function rotate(float $degrees): void 1008 | { 1009 | if ($degrees !== 0) { 1010 | if ($degrees < -360 || $degrees > 360) { 1011 | $degrees = (float) ($degrees % 360); 1012 | } 1013 | 1014 | if ($degrees < 0 && $degrees >= -360) { 1015 | $degrees = 360 + $degrees; 1016 | } 1017 | 1018 | $transparentColor = imageColorAllocateAlpha($this->image, 0, 0, 0, 127); 1019 | $rotationDegrees = ($degrees > 0) ? intval(360 * (1 - $degrees / 360)) : $degrees; // Used to fixed PHP >= 5.5 rotation with base angle 90°, 180° 1020 | 1021 | // Rotate the layer background image 1022 | $imageRotated = imagerotate($this->image, $rotationDegrees, $transparentColor); 1023 | imagealphablending($imageRotated, true); 1024 | imagesavealpha($imageRotated, true); 1025 | 1026 | unset($this->image); 1027 | 1028 | $this->image = $imageRotated; 1029 | 1030 | $oldWidth = $this->width; 1031 | $oldHeight = $this->height; 1032 | 1033 | $this->width = imagesx($this->image); 1034 | $this->height = imagesy($this->image); 1035 | 1036 | foreach ($this->layers as $layerId => $layer) { 1037 | $layerSelfOldCenterPosition = array( 1038 | 'x' => $layer->width / 2, 1039 | 'y' => $layer->height / 2, 1040 | ); 1041 | 1042 | $smallImageCenter = array( 1043 | 'x' => $layerSelfOldCenterPosition['x'] + $this->layerPositions[$layerId]['x'], 1044 | 'y' => $layerSelfOldCenterPosition['y'] + $this->layerPositions[$layerId]['y'], 1045 | ); 1046 | 1047 | $this->layers[$layerId]->rotate($degrees); 1048 | 1049 | $ro = sqrt($smallImageCenter['x'] ** 2 + $smallImageCenter['y'] ** 2); 1050 | 1051 | $teta = (acos($smallImageCenter['x'] / $ro)) * 180 / pi(); 1052 | 1053 | $a = $ro * cos(($teta + $degrees) * pi() / 180); 1054 | $b = $ro * sin(($teta + $degrees) * pi() / 180); 1055 | 1056 | if ($degrees > 0 && $degrees <= 90) { 1057 | $newPositionX = $a - ($this->layers[$layerId]->width / 2) + $oldHeight * sin(($degrees * pi()) / 180); 1058 | $newPositionY = $b - ($this->layers[$layerId]->height / 2); 1059 | } elseif ($degrees > 90 && $degrees <= 180) { 1060 | $newPositionX = $a - ($this->layers[$layerId]->width / 2) + $this->width; 1061 | $newPositionY = $b - ($this->layers[$layerId]->height / 2) + $oldHeight * (-cos(($degrees) * pi() / 180)); 1062 | } elseif ($degrees > 180 && $degrees <= 270) { 1063 | $newPositionX = $a - ($this->layers[$layerId]->width / 2) + $oldWidth * (-cos(($degrees) * pi() / 180)); 1064 | $newPositionY = $b - ($this->layers[$layerId]->height / 2) + $this->height; 1065 | } else { 1066 | $newPositionX = $a - ($this->layers[$layerId]->width / 2); 1067 | $newPositionY = $b - ($this->layers[$layerId]->height / 2) + $oldWidth * (-sin(($degrees) * pi() / 180)); 1068 | } 1069 | 1070 | $this->layerPositions[$layerId] = array( 1071 | 'x' => $newPositionX, 1072 | 'y' => $newPositionY, 1073 | ); 1074 | } 1075 | } 1076 | } 1077 | 1078 | /** 1079 | * Change the opacity of the layer 1080 | * $recursive: apply it on sublayers 1081 | */ 1082 | public function opacity(int $opacity, bool $recursive = true): void 1083 | { 1084 | if ($recursive) { 1085 | $layers = $this->layers; 1086 | 1087 | foreach ($layers as $key => $layer) { 1088 | $layer->opacity($opacity, true); 1089 | $this->layers[$key] = $layer; 1090 | } 1091 | } 1092 | 1093 | $transparentImage = ImageWorkshopLib::generateImage($this->getWidth(), $this->getHeight()); 1094 | 1095 | ImageWorkshopLib::imageCopyMergeAlpha($transparentImage, $this->image, 0, 0, 0, 0, $this->getWidth(), $this->getHeight(), $opacity); 1096 | 1097 | unset($this->image); 1098 | $this->image = $transparentImage; 1099 | unset($transparentImage); 1100 | } 1101 | 1102 | /** 1103 | * Apply a filter on the layer 1104 | * Be careful: some filters can damage transparent images, use it sparingly ! (A good pratice is to use mergeAll on your layer before applying a filter) 1105 | * 1106 | * @param int $filterType (http://www.php.net/manual/en/function.imagefilter.php) 1107 | */ 1108 | public function applyFilter(int $filterType, int $arg1 = null, int $arg2 = null, int $arg3 = null, int $arg4 = null, bool $recursive = false): void 1109 | { 1110 | if ($filterType === IMG_FILTER_COLORIZE) { 1111 | imagefilter($this->image, $filterType, $arg1, $arg2, $arg3, $arg4); 1112 | } elseif ($filterType === IMG_FILTER_BRIGHTNESS || $filterType === IMG_FILTER_CONTRAST || $filterType === IMG_FILTER_SMOOTH) { 1113 | imagefilter($this->image, $filterType, $arg1); 1114 | } elseif ($filterType === IMG_FILTER_PIXELATE) { 1115 | imagefilter($this->image, $filterType, $arg1, $arg2); 1116 | } else { 1117 | imagefilter($this->image, $filterType); 1118 | } 1119 | 1120 | if ($recursive) { 1121 | $layers = $this->layers; 1122 | 1123 | foreach ($layers as $layerId => $layer) { 1124 | $this->layers[$layerId]->applyFilter($filterType, $arg1, $arg2, $arg3, $arg4, true); 1125 | } 1126 | } 1127 | } 1128 | 1129 | /** 1130 | * Apply horizontal or vertical flip (Transformation) 1131 | */ 1132 | public function flip(string $type = 'horizontal'): void 1133 | { 1134 | $layers = $this->layers; 1135 | 1136 | foreach ($layers as $key => $layer) { 1137 | $layer->flip($type); 1138 | $this->layers[$key] = $layer; 1139 | } 1140 | 1141 | $temp = ImageWorkshopLib::generateImage($this->width, $this->height); 1142 | 1143 | if ($type === 'horizontal') { 1144 | imagecopyresampled($temp, $this->image, 0, 0, $this->width - 1, 0, $this->width, $this->height, -$this->width, $this->height); 1145 | $this->image = $temp; 1146 | 1147 | foreach ($this->layerPositions as $layerId => $layerPositions) { 1148 | $this->changePosition($layerId, $this->width - $this->layers[$layerId]->getWidth() - $layerPositions['x'], $layerPositions['y']); 1149 | } 1150 | } elseif ($type === 'vertical') { 1151 | imagecopyresampled($temp, $this->image, 0, 0, 0, $this->height - 1, $this->width, $this->height, $this->width, -1 * $this->height); 1152 | $this->image = $temp; 1153 | 1154 | foreach ($this->layerPositions as $layerId => $layerPositions) { 1155 | $this->changePosition($layerId, $layerPositions['x'], $this->height - $this->layers[$layerId]->getHeight() - $layerPositions['y']); 1156 | } 1157 | } 1158 | 1159 | unset($temp); 1160 | } 1161 | 1162 | /** 1163 | * Add a text on the background image of the layer using a default font registered in GD 1164 | */ 1165 | public function writeText(string $text, int $font = 1, string $color = 'ffffff', int $positionX = 0, int $positionY = 0, string $align = 'horizontal'): void 1166 | { 1167 | $RGBTextColor = ImageWorkshopLib::convertHexToRGB($color); 1168 | $textColor = imagecolorallocate($this->image, $RGBTextColor['R'], $RGBTextColor['G'], $RGBTextColor['B']); 1169 | 1170 | if ($align === 'horizontal') { 1171 | imagestring($this->image, $font, $positionX, $positionY, $text, $textColor); 1172 | } else { 1173 | imagestringup($this->image, $font, $positionX, $positionY, $text, $textColor); 1174 | } 1175 | } 1176 | 1177 | /** 1178 | * Add a text on the background image of the layer using a font localized at $fontPath 1179 | * Return the text coordonates 1180 | * 1181 | * @return array|false 1182 | */ 1183 | public function write(string $text, string $fontPath, int $fontSize = 13, string $color = 'ffffff', int $positionX = 0, int $positionY = 0, int $fontRotation = 0): array|bool 1184 | { 1185 | if (!file_exists($fontPath)) { 1186 | throw new ImageWorkshopLayerException('Can\'t find a font file at this path : "'.$fontPath.'".', static::ERROR_FONT_NOT_FOUND); 1187 | } 1188 | 1189 | $RGBTextColor = ImageWorkshopLib::convertHexToRGB($color); 1190 | $textColor = imagecolorallocate($this->image, $RGBTextColor['R'], $RGBTextColor['G'], $RGBTextColor['B']); 1191 | 1192 | return imagettftext($this->image, $fontSize, $fontRotation, $positionX, $positionY, $textColor, $fontPath, $text); 1193 | } 1194 | 1195 | // Manage the result 1196 | // ========================================================= 1197 | 1198 | /** 1199 | * Return a merged resource image 1200 | * 1201 | * $backgroundColor is really usefull if you want to save a JPG or GIF, because the transparency of the background 1202 | * would be remove for a colored background, so you should choose a color like "ffffff" (white) 1203 | */ 1204 | public function getResult(string $backgroundColor = null): GdImage 1205 | { 1206 | $imagesToMerge = array(); 1207 | ksort($this->layerLevels); 1208 | 1209 | foreach ($this->layerLevels as $layerLevel => $layerId) { 1210 | $imagesToMerge[$layerLevel] = $this->layers[$layerId]->getResult(); 1211 | 1212 | // Layer positions 1213 | if ($this->layerPositions[$layerId]['x'] !== 0 || $this->layerPositions[$layerId]['y'] !== 0) { 1214 | $virginLayoutImageTmp = ImageWorkshopLib::generateImage($this->width, $this->height); 1215 | ImageWorkshopLib::mergeTwoImages($virginLayoutImageTmp, $imagesToMerge[$layerLevel], $this->layerPositions[$layerId]['x'], $this->layerPositions[$layerId]['y'], 0, 0); 1216 | $imagesToMerge[$layerLevel] = $virginLayoutImageTmp; 1217 | unset($virginLayoutImageTmp); 1218 | } 1219 | } 1220 | 1221 | $mergedImage = $this->image; 1222 | ksort($imagesToMerge); 1223 | 1224 | foreach ($imagesToMerge as $image) { 1225 | ImageWorkshopLib::mergeTwoImages($mergedImage, $image); 1226 | } 1227 | 1228 | $opacity = 127; 1229 | 1230 | if ($backgroundColor !== 'transparent') { 1231 | $opacity = 0; 1232 | } 1233 | 1234 | $backgroundImage = ImageWorkshopLib::generateImage($this->width, $this->height, (string) $backgroundColor, $opacity); 1235 | ImageWorkshopLib::mergeTwoImages($backgroundImage, $mergedImage); 1236 | $mergedImage = $backgroundImage; 1237 | unset($backgroundImage); 1238 | 1239 | return $mergedImage; 1240 | } 1241 | 1242 | /** 1243 | * Save the resulting image at the specified path 1244 | * 1245 | * $backgroundColor is really usefull if you want to save a JPG or GIF, because the transparency of the background 1246 | * would be remove for a colored background, so you should choose a color like "ffffff" (white) 1247 | * 1248 | * If the file already exists, it will be override ! 1249 | * 1250 | * $imageQuality is useless for GIF 1251 | * 1252 | * Ex: $folder = __DIR__."/../web/images/2012" 1253 | * $imageName = "butterfly.jpg" 1254 | * $createFolders = true 1255 | * $imageQuality = 95 1256 | * $backgroundColor = "ffffff" 1257 | */ 1258 | public function save(string $folder, string $imageName, bool $createFolders = true, string $backgroundColor = null, int $imageQuality = 75, bool $interlace = false): void 1259 | { 1260 | if (is_file($folder)) { 1261 | throw new ImageWorkshopLayerException(sprintf('Destination folder "%s" is a file.', $folder), self::ERROR_NOT_WRITABLE_FOLDER); 1262 | } 1263 | 1264 | if ((!is_dir($folder) && !$createFolders)) { 1265 | throw new ImageWorkshopLayerException(sprintf('Destination folder "%s" not exists.', $folder), self::ERROR_NOT_WRITABLE_FOLDER); 1266 | } 1267 | 1268 | if (is_dir($folder) && !is_writable($folder)) { 1269 | throw new ImageWorkshopLayerException(sprintf('Destination folder "%s" not writable.', $folder), self::ERROR_NOT_WRITABLE_FOLDER); 1270 | } 1271 | 1272 | $extension = explode('.', $imageName); 1273 | $extension = strtolower($extension[count($extension) - 1]); 1274 | 1275 | $filename = sprintf('%s/%s', rtrim($folder, '/'), ltrim($imageName, '/')); 1276 | 1277 | // Creating the folders if they don't exist 1278 | $dirname = dirname($filename); 1279 | if (!is_dir($dirname) && $createFolders) { 1280 | if (!mkdir($dirname, 0777, true)) { 1281 | throw new ImageWorkshopLayerException(sprintf('Unable to create destination folder "%s".', $dirname), self::ERROR_NOT_WRITABLE_FOLDER); 1282 | } 1283 | 1284 | $oldUmask = umask(0); 1285 | umask($oldUmask); 1286 | chmod($dirname, 0777); 1287 | } 1288 | 1289 | if (($extension === 'jpg' || $extension === 'jpeg' || $extension === 'gif') && (!$backgroundColor || $backgroundColor === 'transparent')) { 1290 | $backgroundColor = 'ffffff'; 1291 | } 1292 | 1293 | $image = $this->getResult($backgroundColor); 1294 | 1295 | imageinterlace($image, $interlace); 1296 | 1297 | if ($extension === 'jpg' || $extension === 'jpeg') { 1298 | $isSaved = imagejpeg($image, $filename, $imageQuality); 1299 | } elseif ($extension === 'gif') { 1300 | $isSaved = imagegif($image, $filename); 1301 | } elseif ($extension === 'png') { 1302 | if ($imageQuality >= 100) { 1303 | $imageQuality = 0; 1304 | } elseif ($imageQuality <= 0) { 1305 | $imageQuality = 10; 1306 | } else { 1307 | $imageQuality = (int) round((100 - $imageQuality) / 10); 1308 | } 1309 | 1310 | $isSaved = imagepng($image, $filename, intval($imageQuality)); 1311 | } elseif ($extension === 'webp') { 1312 | if (!function_exists('imagewebp')) { 1313 | throw new ImageWorkshopLayerException(sprintf('Image format "%s" not supported by PHP version', $extension), self::ERROR_NOT_SUPPORTED_FORMAT); 1314 | } 1315 | 1316 | $isSaved = imagewebp($image, $filename, $imageQuality); 1317 | } else { 1318 | throw new ImageWorkshopLayerException(sprintf('Image format "%s" not supported.', $extension), self::ERROR_NOT_SUPPORTED_FORMAT); 1319 | } 1320 | 1321 | if (!$isSaved) { 1322 | throw new ImageWorkshopLayerException(sprintf('Error occurs when save image "%s".', $folder), self::ERROR_UNKNOW); 1323 | } 1324 | 1325 | unset($image); 1326 | } 1327 | 1328 | // Checkers 1329 | // ========================================================= 1330 | 1331 | /** 1332 | * Check if a sublayer exists in the stack for a given id 1333 | */ 1334 | public function isLayerInIndex(int $layerId): bool 1335 | { 1336 | return array_key_exists($layerId, $this->layers); 1337 | } 1338 | 1339 | // Getter / Setter 1340 | // ========================================================= 1341 | 1342 | /** 1343 | * Return the narrow side width of the layer 1344 | */ 1345 | public function getNarrowSideWidth(): int 1346 | { 1347 | $narrowSideWidth = $this->getWidth(); 1348 | 1349 | if ($this->getHeight() < $narrowSideWidth) { 1350 | $narrowSideWidth = $this->getHeight(); 1351 | } 1352 | 1353 | return $narrowSideWidth; 1354 | } 1355 | 1356 | /** 1357 | * Return the largest side width of the layer 1358 | */ 1359 | public function getLargestSideWidth(): int 1360 | { 1361 | $largestSideWidth = $this->getWidth(); 1362 | 1363 | if ($this->getHeight() > $largestSideWidth) { 1364 | $largestSideWidth = $this->getHeight(); 1365 | } 1366 | 1367 | return $largestSideWidth; 1368 | } 1369 | 1370 | /** 1371 | * Get the level of a sublayer 1372 | * Return sublayer level if success or false if layer isn't found 1373 | * 1374 | * @return int|false 1375 | */ 1376 | public function getLayerLevel(int $layerId): int|bool 1377 | { 1378 | if ($this->isLayerInIndex($layerId)) { // if the layer exists in document 1379 | return array_search($layerId, $this->layerLevels); 1380 | } 1381 | 1382 | return false; 1383 | } 1384 | 1385 | /** 1386 | * Get a sublayer in the stack 1387 | * Don't forget to use clone method: $b = clone $a->getLayer(3); 1388 | */ 1389 | public function getLayer(int $layerId): self 1390 | { 1391 | return $this->layers[$layerId]; 1392 | } 1393 | 1394 | /** 1395 | * Getter width 1396 | */ 1397 | public function getWidth(): int 1398 | { 1399 | return $this->width; 1400 | } 1401 | 1402 | /** 1403 | * Getter height 1404 | */ 1405 | public function getHeight(): int 1406 | { 1407 | return $this->height; 1408 | } 1409 | 1410 | /** 1411 | * Getter image 1412 | */ 1413 | public function getImage(): GdImage 1414 | { 1415 | return $this->image; 1416 | } 1417 | 1418 | /** 1419 | * Getter layers 1420 | * 1421 | * @return self[] 1422 | */ 1423 | public function getLayers(): array 1424 | { 1425 | return $this->layers; 1426 | } 1427 | 1428 | /** 1429 | * Getter layerLevels 1430 | * 1431 | * @return int[] 1432 | */ 1433 | public function getLayerLevels(): array 1434 | { 1435 | return $this->layerLevels; 1436 | } 1437 | 1438 | /** 1439 | * Get all the positions of the sublayers 1440 | * 1441 | * @return array 1442 | */ 1443 | public function getLayerPositions(): array 1444 | { 1445 | return $this->layerPositions; 1446 | } 1447 | 1448 | /** 1449 | * Get the position of this sublayer 1450 | * 1451 | * @return array{x: int, y: int}|false 1452 | */ 1453 | public function getLayerPosition(int $layerId): array|bool 1454 | { 1455 | if ($this->isLayerInIndex($layerId)) { // if the sublayer exists in the stack 1456 | return $this->layerPositions[$layerId]; 1457 | } 1458 | 1459 | return false; 1460 | } 1461 | 1462 | /** 1463 | * Getter highestLayerLevel 1464 | */ 1465 | public function getHighestLayerLevel(): int 1466 | { 1467 | return $this->highestLayerLevel; 1468 | } 1469 | 1470 | /** 1471 | * Getter lastLayerId 1472 | */ 1473 | public function getLastLayerId(): int 1474 | { 1475 | return $this->lastLayerId; 1476 | } 1477 | 1478 | // Internals 1479 | // ========================================================= 1480 | 1481 | /** 1482 | * Delete the current object 1483 | */ 1484 | public function delete(): void 1485 | { 1486 | imagedestroy($this->image); 1487 | $this->clearStack(); 1488 | } 1489 | 1490 | /** 1491 | * Create a new background image var from the old background image var 1492 | */ 1493 | public function createNewVarFromBackgroundImage(): void 1494 | { 1495 | $virginImage = ImageWorkshopLib::generateImage($this->getWidth(), $this->getHeight()); // New background image 1496 | 1497 | ImageWorkshopLib::mergeTwoImages($virginImage, $this->image, 0, 0, 0, 0); 1498 | unset($this->image); 1499 | 1500 | $this->image = $virginImage; 1501 | unset($virginImage); 1502 | 1503 | $layers = $this->layers; 1504 | 1505 | foreach ($layers as $layerId => $layer) { 1506 | $this->layers[$layerId] = clone $this->layers[$layerId]; 1507 | } 1508 | } 1509 | 1510 | /** 1511 | * Index a sublayer in the layer stack 1512 | * Return an array containing the generated sublayer id and its final level: 1513 | * array("layerLevel" => integer, "id" => integer) 1514 | * 1515 | * @return array{layerLevel: int, id: int} 1516 | */ 1517 | protected function indexLayer(int $layerLevel, ImageWorkshopLayer $layer, int $positionX, int $positionY, string $position): array 1518 | { 1519 | // Choose an id for the added layer 1520 | $layerId = $this->lastLayerId + 1; 1521 | 1522 | // Clone $layer to duplicate image resource var 1523 | $layer = clone $layer; 1524 | 1525 | // Add the layer in the stack 1526 | $this->layers[$layerId] = $layer; 1527 | 1528 | // Add the layer positions in the main layer 1529 | $this->layerPositions[$layerId] = ImageWorkshopLib::calculatePositions($this->getWidth(), $this->getHeight(), $layer->getWidth(), $layer->getHeight(), $positionX, $positionY, $position); 1530 | 1531 | // Update the lastLayerId of the workshop 1532 | $this->lastLayerId = $layerId; 1533 | 1534 | // Add the layer level in the stack 1535 | $layerLevel = $this->indexLevelInDocument($layerLevel, $layerId); 1536 | 1537 | return array( 1538 | 'layerLevel' => $layerLevel, 1539 | 'id' => $layerId, 1540 | ); 1541 | } 1542 | 1543 | /** 1544 | * Index a layer level and update the layers levels in the document 1545 | * Return the corrected level of the layer 1546 | */ 1547 | protected function indexLevelInDocument(int $layerLevel, int $layerId): int 1548 | { 1549 | if (array_key_exists($layerLevel, $this->layerLevels)) { // Level already exists 1550 | 1551 | ksort($this->layerLevels); // All layers after this level and the layer which have this level are updated 1552 | $layerLevelsTmp = $this->layerLevels; 1553 | 1554 | foreach ($layerLevelsTmp as $levelTmp => $layerIdTmp) { 1555 | if ($levelTmp >= $layerLevel) { 1556 | $this->layerLevels[$levelTmp + 1] = $layerIdTmp; 1557 | } 1558 | } 1559 | 1560 | unset($layerLevelsTmp); 1561 | } else { // Level isn't taken 1562 | if ($this->highestLayerLevel < $layerLevel) { // If given level is too high, proceed adjustement 1563 | $layerLevel = $this->highestLayerLevel + 1; 1564 | } 1565 | } 1566 | 1567 | $this->layerLevels[$layerLevel] = $layerId; 1568 | $this->highestLayerLevel = max(array_flip($this->layerLevels)); // Update $highestLayerLevel 1569 | 1570 | return $layerLevel; 1571 | } 1572 | 1573 | /** 1574 | * Update the positions of layers in the stack after cropping 1575 | */ 1576 | public function updateLayerPositionsAfterCropping(int $positionX, int $positionY): void 1577 | { 1578 | foreach ($this->layers as $layerId => $layer) { 1579 | $oldLayerPosX = $this->layerPositions[$layerId]['x']; 1580 | $oldLayerPosY = $this->layerPositions[$layerId]['y']; 1581 | 1582 | $newLayerPosX = $oldLayerPosX + $positionX; 1583 | $newLayerPosY = $oldLayerPosY + $positionY; 1584 | 1585 | $this->changePosition($layerId, $newLayerPosX, $newLayerPosY); 1586 | } 1587 | } 1588 | 1589 | /** 1590 | * Resize the background of a layer 1591 | */ 1592 | public function resizeBackground(int $newWidth, int $newHeight): void 1593 | { 1594 | $oldWidth = $this->width; 1595 | $oldHeight = $this->height; 1596 | 1597 | $this->width = $newWidth; 1598 | $this->height = $newHeight; 1599 | 1600 | $virginLayoutImage = ImageWorkshopLib::generateImage($this->width, $this->height); 1601 | 1602 | imagecopyresampled($virginLayoutImage, $this->image, 0, 0, 0, 0, $this->width, $this->height, $oldWidth, $oldHeight); 1603 | 1604 | unset($this->image); 1605 | $this->image = $virginLayoutImage; 1606 | } 1607 | 1608 | /** 1609 | * Fix image orientation based on exif data 1610 | */ 1611 | public function fixOrientation(): void 1612 | { 1613 | if (!isset($this->exif['Orientation']) || 0 === $this->exif['Orientation']) { 1614 | return; 1615 | } 1616 | 1617 | switch ($this->exif['Orientation']) { 1618 | case ExifOrientations::TOP_RIGHT: 1619 | $this->flip('horizontal'); 1620 | break; 1621 | 1622 | case ExifOrientations::BOTTOM_RIGHT: 1623 | $this->rotate(180); 1624 | break; 1625 | 1626 | case ExifOrientations::BOTTOM_LEFT: 1627 | $this->flip('vertical'); 1628 | break; 1629 | 1630 | case ExifOrientations::LEFT_TOP: 1631 | $this->rotate(-90); 1632 | $this->flip('vertical'); 1633 | break; 1634 | 1635 | case ExifOrientations::RIGHT_TOP: 1636 | $this->rotate(90); 1637 | break; 1638 | 1639 | case ExifOrientations::RIGHT_BOTTOM: 1640 | $this->rotate(90); 1641 | $this->flip('horizontal'); 1642 | break; 1643 | 1644 | case ExifOrientations::LEFT_BOTTOM: 1645 | $this->rotate(-90); 1646 | break; 1647 | } 1648 | 1649 | $this->exif['Orientation'] = ExifOrientations::TOP_LEFT; 1650 | } 1651 | } 1652 | -------------------------------------------------------------------------------- /src/Core/ImageWorkshopLib.php: -------------------------------------------------------------------------------- 1 | $layerPositionX, 59 | 'y' => $layerPositionY, 60 | ); 61 | } 62 | 63 | /** 64 | * Convert Hex color to RGB color format 65 | * 66 | * @return array{R: int, G: int, B: int} 67 | */ 68 | public static function convertHexToRGB(?string $hex): array 69 | { 70 | return array( 71 | 'R' => (int) base_convert(substr($hex ?? '', 0, 2), 16, 10), 72 | 'G' => (int) base_convert(substr($hex ?? '', 2, 2), 16, 10), 73 | 'B' => (int) base_convert(substr($hex ?? '', 4, 2), 16, 10), 74 | ); 75 | } 76 | 77 | /** 78 | * Generate a new image 79 | */ 80 | public static function generateImage(int $width = 100, int $height = 100, string $color = 'ffffff', int $opacity = 127): GdImage 81 | { 82 | $image = imagecreatetruecolor($width, $height); 83 | imagesavealpha($image, true); 84 | 85 | if ($color === 'transparent') { 86 | $color = 'ffffff'; 87 | $opacity = 127; 88 | } 89 | 90 | $RGBColors = ImageWorkshopLib::convertHexToRGB($color); 91 | $color = imagecolorallocatealpha($image, $RGBColors['R'], $RGBColors['G'], $RGBColors['B'], $opacity); 92 | 93 | imagefill($image, 0, 0, $color); 94 | 95 | return $image; 96 | } 97 | 98 | /** 99 | * Return dimension of a text 100 | * 101 | * @return array{left: int, top: int, width: int, height: int}|false 102 | */ 103 | public static function getTextBoxDimension(float $fontSize, float $fontAngle, string $fontFile, string $text): array|bool 104 | { 105 | if (!file_exists($fontFile)) { 106 | throw new ImageWorkshopLibException('Can\'t find a font file at this path : "'.$fontFile.'".', static::ERROR_FONT_NOT_FOUND); 107 | } 108 | 109 | $box = imagettfbbox($fontSize, $fontAngle, $fontFile, $text); 110 | 111 | if (!$box) { 112 | return false; 113 | } 114 | 115 | $minX = min(array($box[0], $box[2], $box[4], $box[6])); 116 | $maxX = max(array($box[0], $box[2], $box[4], $box[6])); 117 | $minY = min(array($box[1], $box[3], $box[5], $box[7])); 118 | $maxY = max(array($box[1], $box[3], $box[5], $box[7])); 119 | $width = ($maxX - $minX); 120 | $height = ($maxY - $minY); 121 | $left = abs($minX) + $width; 122 | $top = abs($minY) + $height; 123 | 124 | // to calculate the exact bounding box, we write the text in a large image 125 | $img = @imagecreatetruecolor($width << 2, $height << 2); 126 | $white = imagecolorallocate($img, 255, 255, 255); 127 | $black = imagecolorallocate($img, 0, 0, 0); 128 | imagefilledrectangle($img, 0, 0, imagesx($img), imagesy($img), $black); 129 | 130 | // for ensure that the text is completely in the image 131 | imagettftext($img, $fontSize, $fontAngle, $left, $top, $white, $fontFile, $text); 132 | 133 | // start scanning (0=> black => empty) 134 | $rleft = $w4 = $width<<2; 135 | $rright = 0; 136 | $rbottom = 0; 137 | $rtop = $h4 = $height<<2; 138 | 139 | for ($x = 0; $x < $w4; $x++) { 140 | for ($y = 0; $y < $h4; $y++) { 141 | if (imagecolorat($img, $x, $y)) { 142 | $rleft = min($rleft, $x); 143 | $rright = max($rright, $x); 144 | $rtop = min($rtop, $y); 145 | $rbottom = max($rbottom, $y); 146 | } 147 | } 148 | } 149 | 150 | imagedestroy($img); 151 | 152 | return array( 153 | 'left' => $left - $rleft, 154 | 'top' => $top - $rtop, 155 | 'width' => $rright - $rleft + 1, 156 | 'height' => $rbottom - $rtop + 1, 157 | ); 158 | } 159 | 160 | /** 161 | * Copy an image on another one and converse transparency 162 | */ 163 | public static function imageCopyMergeAlpha(GdImage $destImg, GdImage $srcImg, int $destX, int $destY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct = 0): void 164 | { 165 | $destW = imageSX($destImg); 166 | $destH = imageSY($destImg); 167 | $alpha = 0; 168 | 169 | for ($y = 0; $y < $srcH + $srcY; $y++) { 170 | for ($x = 0; $x < $srcW + $srcX; $x++) { 171 | if ($x + $destX >= 0 && $x + $destX < $destW && $x + $srcX >= 0 && $x + $srcX < $srcW && $y + $destY >= 0 && $y + $destY < $destH && $y + $srcY >= 0 && $y + $srcY < $srcH) { 172 | $destPixel = imageColorsForIndex($destImg, imageColorat($destImg, $x + $destX, $y + $destY)); 173 | $srcImgColorat = imageColorat($srcImg, $x + $srcX, $y + $srcY); 174 | 175 | if ($srcImgColorat >= 0) { 176 | $srcPixel = imageColorsForIndex($srcImg, $srcImgColorat); 177 | 178 | $srcAlpha = 1 - ($srcPixel['alpha'] / 127); 179 | $destAlpha = 1 - ($destPixel['alpha'] / 127); 180 | $opacity = $srcAlpha * $pct / 100; 181 | 182 | if ($destAlpha >= $opacity) { 183 | $alpha = $destAlpha; 184 | } 185 | 186 | if ($destAlpha < $opacity) { 187 | $alpha = $opacity; 188 | } 189 | 190 | if ($alpha > 1) { 191 | $alpha = 1; 192 | } 193 | 194 | if ($opacity > 0) { 195 | $destRed = round((($destPixel['red'] * $destAlpha * (1 - $opacity)))); 196 | $destGreen = round((($destPixel['green'] * $destAlpha * (1 - $opacity)))); 197 | $destBlue = round((($destPixel['blue'] * $destAlpha * (1 - $opacity)))); 198 | $srcRed = round((($srcPixel['red'] * $opacity))); 199 | $srcGreen = round((($srcPixel['green'] * $opacity))); 200 | $srcBlue = round((($srcPixel['blue'] * $opacity))); 201 | $red = round(($destRed + $srcRed) / ($destAlpha * (1 - $opacity) + $opacity)); 202 | $green = round(($destGreen + $srcGreen) / ($destAlpha * (1 - $opacity) + $opacity)); 203 | $blue = round(($destBlue + $srcBlue) / ($destAlpha * (1 - $opacity) + $opacity)); 204 | 205 | if ($red > 255) { 206 | $red = 255; 207 | } 208 | 209 | if ($green > 255) { 210 | $green = 255; 211 | } 212 | 213 | if ($blue > 255) { 214 | $blue = 255; 215 | } 216 | 217 | $alpha = round((1 - $alpha) * 127); 218 | $color = imageColorAllocateAlpha($destImg, $red, $green, $blue, (int) $alpha); 219 | imageSetPixel($destImg, $x + $destX, $y + $destY, $color); 220 | } 221 | } 222 | } 223 | } 224 | } 225 | } 226 | 227 | /** 228 | * Merge two image var 229 | */ 230 | public static function mergeTwoImages(GdImage $destinationImage, GdImage $sourceImage, int $destinationPosX = 0, int $destinationPosY = 0, int $sourcePosX = 0, int $sourcePosY = 0): void 231 | { 232 | imageCopy($destinationImage, $sourceImage, $destinationPosX, $destinationPosY, $sourcePosX, $sourcePosY, imageSX($sourceImage), imageSY($sourceImage)); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/Exception/ImageWorkshopBaseException.php: -------------------------------------------------------------------------------- 1 | code}]: {$this->message}\n"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Exception/ImageWorkshopException.php: -------------------------------------------------------------------------------- 1 | fixOrientation(); 101 | } 102 | 103 | return $layer; 104 | } 105 | 106 | /** 107 | * Initialize a text layer 108 | */ 109 | public static function initTextLayer(string $text, string $fontPath, int $fontSize = 13, string $fontColor = 'ffffff', int $textRotation = 0, string $backgroundColor = null): ImageWorkshopLayer 110 | { 111 | $textDimensions = ImageWorkshopLib::getTextBoxDimension($fontSize, $textRotation, $fontPath, $text); 112 | 113 | $layer = static::initVirginLayer($textDimensions['width'], $textDimensions['height'], $backgroundColor); 114 | $layer->write($text, $fontPath, $fontSize, $fontColor, $textDimensions['left'], $textDimensions['top'], $textRotation); 115 | 116 | return $layer; 117 | } 118 | 119 | /** 120 | * Initialize a new virgin layer 121 | */ 122 | public static function initVirginLayer(int $width = 100, int $height = 100, string $backgroundColor = null): ImageWorkshopLayer 123 | { 124 | $opacity = 0; 125 | 126 | if (null === $backgroundColor || $backgroundColor === 'transparent') { 127 | $opacity = 127; 128 | $backgroundColor = 'ffffff'; 129 | } 130 | 131 | return new ImageWorkshopLayer(ImageWorkshopLib::generateImage($width, $height, $backgroundColor, $opacity)); 132 | } 133 | 134 | /** 135 | * Initialize a layer from a resource image var 136 | */ 137 | public static function initFromResourceVar(GdImage $image): ImageWorkshopLayer 138 | { 139 | return new ImageWorkshopLayer($image); 140 | } 141 | 142 | /** 143 | * Initialize a layer from a string (obtains with file_get_contents, cURL...) 144 | * 145 | * This not recommended to initialize JPEG string with this method, GD displays bugs ! 146 | */ 147 | public static function initFromString(string $imageString): ImageWorkshopLayer 148 | { 149 | if (!$image = @imageCreateFromString($imageString)) { 150 | throw new ImageWorkshopException('Can\'t generate an image from the given string.', static::ERROR_CREATE_IMAGE_FROM_STRING); 151 | } 152 | 153 | return new ImageWorkshopLayer($image); 154 | } 155 | } 156 | --------------------------------------------------------------------------------