├── .github └── workflows │ ├── lint.yml │ ├── settings │ ├── both.json │ ├── ffi-only.json │ └── vips-only.json │ └── tests.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── lib ├── Imagine │ ├── Image │ │ └── VipsProfile.php │ └── Vips │ │ ├── Drawer.php │ │ ├── Effects.php │ │ ├── Font.php │ │ ├── Image.php │ │ ├── Imagine.php │ │ └── Layers.php └── resources │ └── colorprofiles │ ├── AdobeRGB1998.icc │ ├── cmyk.icm │ ├── gray.icc │ └── sRGB.icm └── tests ├── BasicImageTest.php └── EnabledTest.php /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | tests: 9 | name: Lint check 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Setup PHP 13 | uses: shivammathur/setup-php@v2 14 | with: 15 | php-version: "8.1" 16 | tools: composer:v2 17 | coverage: none 18 | 19 | - name: Checkout code 20 | uses: actions/checkout@v3 21 | 22 | - name: Install composer dependencies 23 | run: | 24 | composer update --prefer-dist --no-interaction --no-progress --no-ansi ${COMPOSER_FLAGS} 25 | 26 | - name: PHP CS Fixer 27 | run: composer lint 28 | -------------------------------------------------------------------------------- /.github/workflows/settings/both.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": "", 3 | "ini_values": "ffi.enable=true, zend.max_allowed_stack_size=-1", 4 | "aptinstall": "libvips-dev" 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/settings/ffi-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": "", 3 | "ini_values": "ffi.enable=true, zend.max_allowed_stack_size=-1", 4 | "aptinstall": "libvips" 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/settings/vips-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": ":ffi", 3 | "ini_values": "", 4 | "aptinstall": "libvips-dev" 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan and PHPUnit tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | tests: 9 | name: PHP ${{ matrix.php }} ${{ matrix.settings }} ${{ matrix.COMPOSER_FLAGS }} 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | 14 | matrix: 15 | php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] 16 | settings: ['both', 'vips-only', 'ffi-only'] 17 | COMPOSER_FLAGS: [''] 18 | 19 | include: 20 | - php: '7.2' 21 | settings: 'vips-only' 22 | - php: '7.3' 23 | settings: 'vips-only' 24 | - php: '8.0' 25 | COMPOSER_FLAGS: "--prefer-lowest" 26 | settings: 'vips-only' 27 | - php: '8.0' 28 | COMPOSER_FLAGS: "--prefer-lowest" 29 | settings: 'both' 30 | - php: '8.0' 31 | COMPOSER_FLAGS: "--prefer-lowest" 32 | settings: 'ffi-only' 33 | # currently fails on PHP 8.2 with vips-only, due to https://github.com/libvips/php-vips/pull/174 34 | # once php-vips 1.0.10 is out, we can enable tests for PHP 8.2 for all "settings" and put this above in matrix.php 35 | - php: '8.2' 36 | settings: 'ffi-only' 37 | - php: '8.2' 38 | settings: 'both' 39 | steps: 40 | - name: Checkout code 41 | uses: actions/checkout@v3 42 | 43 | - name: Get Settings 44 | id: set_var 45 | run: | 46 | echo "SETTINGS_JSON=$(jq -c . < .github/workflows/settings/${{ matrix.settings }}.json)" >> $GITHUB_ENV 47 | 48 | - name: Setup PHP 49 | uses: shivammathur/setup-php@v2 50 | with: 51 | php-version: ${{ matrix.php }} 52 | tools: composer:v2 53 | ini-values: ${{fromJson(env.SETTINGS_JSON).ini_values}} 54 | coverage: none 55 | extensions: ${{fromJson(env.SETTINGS_JSON).extensions}} 56 | 57 | - name: Install vips 58 | run: | 59 | sudo apt update 60 | sudo apt install -y ${{fromJson(env.SETTINGS_JSON).aptinstall}} --no-install-recommends 61 | 62 | - name: Install vips extension 63 | run: sudo pecl install vips 64 | if: fromJson(env.SETTINGS_JSON).aptinstall == 'libvips-dev' 65 | 66 | - name: Install vips ext config 67 | run: echo "extension=vips.so" >> $(php -i | grep /.+/php.ini -oE) 68 | if: fromJson(env.SETTINGS_JSON).aptinstall == 'libvips-dev' 69 | 70 | - name: Install composer dependencies 71 | run: | 72 | composer update --prefer-dist --no-interaction --no-progress --no-ansi ${{ matrix.COMPOSER_FLAGS || '' }} 73 | 74 | - name: Debug PHP settings 75 | run: | 76 | php -r 'echo "Has VIPS Extension: " . (extension_loaded("vips") ? "true" : "false") . PHP_EOL; echo "Has FFI Extension: " . (extension_loaded("ffi") ? "true" : "false") . PHP_EOL; echo "Has FFI Class: " . (class_exists(FFI::class) ? "true" : "false") . PHP_EOL; echo "Has FFI Enabled: " . (ini_get("ffi.enable") === "1" ? "true" : "false") . PHP_EOL; echo "Has zend.max_allowed_stack_size correct: " . (ini_get("zend.max_allowed_stack_size") === "-1" ? "true" : "false") . PHP_EOL;' 77 | 78 | - name: PHPStan 79 | run: composer phpstan 80 | 81 | - name: PHPUnit 82 | run: composer phpunit 83 | 84 | - name: Composer audit 85 | run: | 86 | composer audit --no-dev 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /bin 3 | /composer.lock 4 | /.php_cs.cache 5 | /.php-cs-fixer.cache 6 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | notPath('src/AppBundle/Command/TestCommand.php') 5 | ->exclude('tests/*/Fixtures') 6 | ->exclude('var') 7 | ->in(__DIR__); 8 | 9 | $config = new PhpCsFixer\Config(); 10 | 11 | return $config 12 | ->setRiskyAllowed(true) 13 | ->setRules([ 14 | '@Symfony' => true, 15 | '@Symfony:risky' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'combine_consecutive_unsets' => true, 18 | 'heredoc_to_nowdoc' => true, 19 | 'no_extra_blank_lines' => ['tokens' => ['break', 'continue', 'extra', 'return', 'throw', 'use', 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block']], 20 | 'no_unreachable_default_argument_value' => true, 21 | 'no_useless_else' => true, 22 | 'no_useless_return' => true, 23 | 'non_printable_character' => true, 24 | 'ordered_class_elements' => true, 25 | 'ordered_imports' => true, 26 | 'phpdoc_add_missing_param_annotation' => true, 27 | 'phpdoc_order' => true, 28 | 'random_api_migration' => true, 29 | 'psr_autoloading' => true, 30 | 'strict_param' => true, 31 | 'native_function_invocation' => ['include' => ['@compiler_optimized']], 32 | 'phpdoc_no_empty_return' => false, 33 | 'no_superfluous_phpdoc_tags' => false, 34 | 'fully_qualified_strict_types' => false, 35 | 'native_function_invocation' => false, 36 | 'no_null_property_initialization' => false, 37 | 'phpdoc_trim' => false, 38 | 'blank_line_before_statement' => false, 39 | 'phpdoc_add_missing_param_annotation' => false, 40 | 'modernize_strpos' => false, 41 | 'trailing_comma_in_multiline' => false, 42 | 'no_whitespace_in_blank_line' => false, 43 | ]) 44 | ->setFinder( 45 | $finder 46 | ); 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | ### 0.41.0 (2025-06-04) 3 | 4 | * BC Break! If you tried to overwrite `autorotate` or `n` defaults via the load/open methods, they didn't apply. 5 | If you do that, they are actually applied now, which might change the behaviour of your code. 6 | https://github.com/rokka-io/imagine-vips/pull/43. 7 | 8 | ### 0.40.0 (2025-01-16) 9 | 10 | * BC Break! The `\Imagine\Vips\Drawer::text()` method will now behave like in the other adapters. You can 11 | still use `\Imagine\Vips\Drawer::textWithHeight()` for the old behaviour. 12 | `\Imagine\Vips\Font::box()` was also updated. 13 | By [@phareous](https://github.com/phareous) in https://github.com/rokka-io/imagine-vips/pull/38 14 | * Enable loading of animated WebP images. By [@andy-wr](https://github.com/andi-wr) in https://github.com/rokka-io/imagine-vips/issues/35 15 | * Improved lipvips detection. By [@alexander-schranz](https://github.com/alexander-schranz) 16 | 17 | ### 0.31.0 (2022-10-12) 18 | * Make it compatible with jcupitt/vips 2.1 (and disable support for 2.0, 1.0.x is still supported). See https://github.com/libvips/php-vips/pull/147 for details. 19 | * Add more phpstan tests for all the combinations 20 | * Replace vips_version with Jcupitt\Vips\Config::version() for better support, when vips-ext is not available 21 | * Add some simple PHPUnit tests 22 | 23 | ### 0.30.1 (2022-10-04) 24 | * Fix issue with PHP < 8.0 25 | 26 | ### 0.30.0 (2022-10-04) 27 | * Add `force_magick` for using magicksave insteaf of gifsave 28 | * Make it run on PHP 8.1 without warnings 29 | * Fix some other type issues 30 | * BC break, if you extended \Imagine\Vips\Layers. You need to add some return types now 31 | 32 | ### 0.20.0 (2022-04-29) 33 | * Uses the new FFI based libvips/php-vips (2.0) library, if FFI is installed. 34 | If FFI is not installed, still uses the old library, which needs the 35 | libvips/php-vips-ext extension. 36 | 37 | ### 0.14.0 (2021-12-09) 38 | * Add Drawer::text() support 39 | * Add JPEG-XL (jxl) support (needs libvips 8.11 with builtin support) 40 | * Remove support for PHP 7.0, minimum is now PHP 7.1 41 | * Use gifsave, when vips 8.12 is installed (needs the cgif library) 42 | * Fixed two bugs when converting pixel to color (thanks to @chmgr #21) 43 | 44 | ### 0.13.0 (2021-02-22) 45 | * Add PHP 8 compatibility 46 | * Add Avif support 47 | * Strip metadata in Heif 48 | 49 | ### 0.12.0 (2020-07-14) 50 | * Improve color profile handling. Always transform them. 51 | 52 | ### 0.11.0 (2020-02-17) 53 | * Fix gif delay for vips versions < 8.9 54 | * Add webp_reduction_effort save option. Default is 4, max is 6. 55 | 56 | ### 0.10.1 (2020-01-14) 57 | 58 | * Fix some issues when adding new frames to layers. 59 | * Throw an NotSupportedException when trying to unset a layer as not yet supported. 60 | 61 | ### 0.10.0 (2020-01-14) 62 | 63 | * Improved handling of animated gifs and webp 64 | * Possibility to define delay per frame with vips 8.9 (`Layers::setDelay($index, $delay)` et al.) 65 | 66 | ### 0.9.2 (2020-01-09) 67 | 68 | * Improved coalesce for animated gifs 69 | * Added Image::isOpaque($vips) 70 | 71 | ### 0.9.1 (2020-01-08) 72 | 73 | * Throw a proper NotSupportedException (thanks to @alexander-schranz) 74 | * Get rid of some warnings when using vips 8.9. 75 | * Autorotate HEIF images on load. 76 | * Support animated gif save to file. 77 | * Throw an early error, if magicksave can't save an image. 78 | * Remove 'shrink' options, when not supported by a vips loader. 79 | 80 | ### 0.9.0 (2019-03-07) 81 | 82 | * BREAKING CHANGE: Based on imagine 1.1.0 83 | * Added support for layers. Animated GIFs should now work without imagick, but needs vips 8.7. 84 | * Add support for 'heif_quality' (only useful if your imagemagick or vips 8.8 supports heif). 85 | * Add support for 'jp2_quality' (only useful if your imagemagick supports jpeg2000). 86 | * Add support for 'png_quality' to define quality of pngquant (only useful if vips is compiled with libimagequant). 87 | If set to 100, no lossy conversion is applied (default). 88 | * Add support for magicksave. If you have vips >= 8.7 and imagemagick is included, we now 89 | directly use magicksave to save non-supported-by-vips file formats. No need to convert it to an imagick 90 | object first, resulting in much better performance. 91 | * Add possibility for individual vips save options 92 | * Replace colorprofile with free ones. 93 | * Support for animated webp, needs vips 8.8. 94 | 95 | ### 0.1.0 (2017-12-06) 96 | 97 | * Add 2nd optional parameter to `Image::convertToAlternative` to provide your own options for loading the image as tiff. 98 | 99 | ### 0.0.5 (2017-12-03) 100 | 101 | * ext/vips 1.0.8 is required. Throw exceptions in methods, which needs vips 8.6. 102 | * Add constructor config array to be able to set `vips_cache_set_max_mem` et al. 103 | * Fix paste method to work with future php-vips versions 104 | * Added php-cs-fixer 105 | 106 | ### 0.0.4 (2017-11-27) 107 | 108 | * Fix some operations on grayscale images 109 | * Convert CMYK to sRGB early on 110 | * Convert CMYK to sRGB before save, in case we still have a CMYK picture 111 | * Fix colorprofile for GREY16 pictures 112 | * Fix `Image::paste` to make it faster, when you have many pastes. 113 | 114 | ### 0.0.3 (2017-11-21) 115 | * Fix conversion from cmyk to rgb, when no profile is supplied 116 | * Add `convertToAlternative(ImagineInterface $imagine = null)` to convert the image to 117 | another imagine adapter. Uses Imagick or GD, if none set. 118 | * Fix resize for some format changes 119 | * Fix grayscale for cmyk 120 | * Fix negative effect for images with transparency 121 | 122 | ### 0.0.2 (2017-11-20) 123 | * Improve Profile/Palette/ICC support 124 | * Improve `generateImage` to make it faster, thanks to jcupitt 125 | 126 | ### 0.0.1 (2017-11-19) 127 | * Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 rokka.io 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 10 | furnished 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 THE 21 | SOFTWARE. 22 | 23 | This software uses code from the php imagine library. See 24 | https://github.com/avalanche123/Imagine for details. 25 | Copyright (c) 2004-2012 Bulat Shakirzyanov 26 | 27 | This software embeds Adobe ICC Profiles, see license at 28 | http://www.adobe.com/support/downloads/iccprofiles/icc_eula_mac_dist.html . 29 | 30 | This software also embeds ICC Profile from colormanagement.org. Please 31 | find information about their license at http://colormanagement.org/ . 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libvips adapter for Imagine 2 | 3 | [![Static analysis](https://github.com/rokka-io/imagine-vips/actions/workflows/tests.yml/badge.svg)](https://github.com/rokka-io/imagine-vips/actions/workflows/tests.yml) 4 | [![Latest Stable Version](https://poser.pugx.org/rokka/imagine-vips/version.png)](https://packagist.org/packages/rokka/imagine-vips) 5 | 6 | 7 | This package provides a [libvips](https://github.com/libvips/libvips) integration for [Imagine](https://imagine.readthedocs.io/en/latest/). The [VIPS image processing system](https://libvips.github.io/libvips/) is a very fast, multi-threaded image processing library with low memory needs. 8 | 9 | Version 8.7 or higher of libvips is highly recommended. `paste` and `rotate` by angles other than multipliers of 90 are not supported with older versions of libvips. 10 | 11 | You either need the [PHP FFI](https://www.php.net/manual/en/book.ffi.php) extension (recommended, since that's the currently supported way by the libvips maintainer) or the 12 | [php-vips-ext](https://github.com/libvips/php-vips-ext) extension version 1.0.8 or higher (you need to install that manually). 13 | And the [php-vips](https://github.com/libvips/php-vips) classes (automatically installed by composer) 14 | 15 | 16 | The most (to us at least) important stuff is implemented. There may be edge cases, which are not covered yet, but those will be hopefully fixed soon. Report them, if you encounter one. 17 | 18 | Even it this is not a 1.0.0 release yet, the library is somehow battle tested as we use it on [rokka.io](https://rokka.io). 19 | 20 | ## Installation 21 | 22 | Just run the following 23 | 24 | ``` 25 | composer require rokka/imagine-vips 26 | ``` 27 | 28 | and then you can use it like any other Imagine implementation with eg. 29 | 30 | ``` 31 | $imagine = new \Imagine\Vips\Imagine(); 32 | 33 | $size = new Imagine\Image\Box(40, 40); 34 | $mode = Imagine\Image\ImageInterface::THUMBNAIL_INSET; 35 | 36 | $imagine->open('/path/to/large_image.jpg') 37 | ->thumbnail($size, $mode) 38 | ->save('/path/to/thumbnail.png') 39 | ``` 40 | 41 | ## Missing stuff 42 | 43 | Needs vips 8.6 or higher: 44 | 45 | * paste 46 | * rotate by angles other than multipliers of 90 47 | 48 | Not implemented yet 49 | 50 | * Complete Drawer support, only text is. 51 | * Methods: 52 | * fill 53 | * histogram 54 | * Filters: 55 | * colorize 56 | 57 | Most of them are not that important to us, so any contributions are welcome. Drawer for example may be a low hanging fruit, if you want to get into it. 58 | 59 | ### Layers and Animated gifs 60 | 61 | If you have vips 8.7.0, layers and animated gifs should work like with imagick. 62 | 63 | ## Saving files 64 | 65 | Natively supported by libvips for saving are jpg, png, webp and tiff. If you have vips 8.7.0 with imagemagick support, it will use vips "[magicksave](https://libvips.github.io/libvips/API/current/VipsForeignSave.html#vips-magicksave)" for all other formats. It not, this adapter falls back to the Imagick or GD implementation. 66 | 67 | ## Contribution 68 | 69 | Any contribution is very appreciated, just file an issue or send a Pull Request. 70 | 71 | 72 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rokka/imagine-vips", 3 | "description": "libvips adapter for imagine", 4 | "keywords": [ 5 | "image manipulation", 6 | "image processing", 7 | "drawing", 8 | "graphics", 9 | "vips", 10 | "libvips", 11 | "php-vips" 12 | ], 13 | "homepage": "https://github.com/rokka-io/imagine-vips", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "rokka", 18 | "email": "rokka@rokka.io", 19 | "homepage": "https://rokka.io" 20 | } 21 | ], 22 | "config": { 23 | "bin-dir": "bin" 24 | }, 25 | "prefer-stable": true, 26 | "require": { 27 | "php": "^7.2 || ^8.0", 28 | "imagine/imagine": "^1.0", 29 | "jcupitt/vips" : "^2.1.1 || ^1.0.3", 30 | "phenx/php-font-lib": "^0.5.2 || ^1.0" 31 | 32 | }, 33 | "require-dev": { 34 | "friendsofphp/php-cs-fixer": "^3.4", 35 | "phpstan/phpstan": "^1.8", 36 | "phpunit/phpunit": "^8 || ^9" 37 | 38 | }, 39 | "suggest": { 40 | "ext-gd": "to use the GD implementation fallback for saving unsupported file formats", 41 | "ext-imagick": "to use the Imagick implementation fallback for saving unsupported file formats" 42 | }, 43 | "autoload": { 44 | "psr-0": { 45 | "Imagine": "lib/" 46 | } 47 | }, 48 | "scripts": { 49 | "phpstan": "phpstan analyze -l 5 lib/", 50 | "phpunit": "phpunit tests", 51 | "lint": "php-cs-fixer fix --dry-run -v --diff", 52 | "lint:fix": "php-cs-fixer fix -v --diff" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/Imagine/Image/VipsProfile.php: -------------------------------------------------------------------------------- 1 | name = $name; 23 | $this->data = $data; 24 | $this->path = $path; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function name() 31 | { 32 | return $this->name; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function data() 39 | { 40 | return $this->data; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function path() 47 | { 48 | if (null == $this->path) { 49 | self::getTmpFileFromRawData($this->data); 50 | } 51 | 52 | return $this->path; 53 | } 54 | 55 | public static function fromRawData(string $profile) 56 | { 57 | $profileFile = self::getTmpFileFromRawData($profile); 58 | 59 | return new self(basename($profileFile), $profile, $profileFile); 60 | } 61 | 62 | /** 63 | * Creates a profile from a path to a file. 64 | * 65 | * @param string $path 66 | * 67 | * @throws InvalidArgumentException In case the provided path is not valid 68 | * 69 | * @return self 70 | */ 71 | public static function fromPath($path) 72 | { 73 | if (!file_exists($path) || !is_file($path) || !is_readable($path)) { 74 | throw new InvalidArgumentException(sprintf('Path %s is an invalid profile file or is not readable', $path)); 75 | } 76 | 77 | return new self(basename($path), file_get_contents($path), $path); 78 | } 79 | 80 | protected static function getTmpFileFromRawData(string $profile): string 81 | { 82 | $profileMd5 = md5($profile); 83 | $profileFile = sys_get_temp_dir().'/imagine-vips-profile-'.$profileMd5.'.icc'; 84 | if (!file_exists($profileFile)) { 85 | file_put_contents($profileFile, $profile); 86 | } 87 | 88 | return $profileFile; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/Imagine/Vips/Drawer.php: -------------------------------------------------------------------------------- 1 | image = $image; 26 | } 27 | 28 | /** 29 | * Standard text render function for Imagine. 30 | * 31 | * @param string $string 32 | * @param int $angle 33 | * @param int|null $width 34 | * 35 | * @throws \FontLib\Exception\FontNotFoundException 36 | * @throws \Jcupitt\Vips\Exception 37 | * 38 | * @return $this|Drawer 39 | */ 40 | public function text( 41 | $string, 42 | AbstractFont $font, 43 | PointInterface $position, 44 | $angle = 0, 45 | $width = null 46 | ) { 47 | [$red, $green, $blue, $alpha] = Image::getColorArrayAlpha($font->getColor()); 48 | $fontSize = (int) ($font->getSize() * (96 / 72)); 49 | 50 | $text = $this->image->getVips()->text($string, [ 51 | 'font' => \FontLib\Font::load($font->getFile())->getFontFullName().' '.$fontSize, 52 | 'fontfile' => $font->getFile(), 53 | 'dpi' => 72, 54 | ]); 55 | 56 | if (0 !== $angle) { 57 | $text = $text->similarity(['angle' => $angle]); 58 | } 59 | 60 | $vips = $this->image->getVips(); 61 | 62 | // write text, the second array is the text background box 63 | $overlay = $text->ifthenelse([$red, $green, $blue, $alpha], [0, 0, 0, 0], ['blend' => true]); 64 | 65 | $overlay = $overlay->copy(['interpretation' => 'srgb']); 66 | 67 | // expand to size of the frame, place in proper position 68 | $overlay = $overlay->embed($position->getX(), $position->getY(), $vips->width, $vips->height); 69 | 70 | // @TODO handle animated gif, possible with something like this to expand to full size of gif roll 71 | // $overlay = $overlay->replicate(1, $vips->height) 72 | 73 | // composite text image on top of main image 74 | $vips = $vips->composite2($overlay, 'over'); 75 | 76 | $this->image->setVips($vips); 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Draw text onto an image. 83 | * 84 | * This code is not totally tested, but works basically. 85 | * 86 | * @param int $angle 87 | * @param int|null $width 88 | * @param int|null $height 89 | * @param string $align 90 | * 91 | * @throws \FontLib\Exception\FontNotFoundException 92 | * @throws \Imagine\Exception\NotSupportedException 93 | * @throws \Imagine\Exception\RuntimeException 94 | * @throws \Jcupitt\Vips\Exception 95 | */ 96 | public function textWithHeight( 97 | $string, 98 | AbstractFont $font, 99 | PointInterface $position, 100 | $angle = 0, 101 | $width = null, 102 | $height = null, 103 | $align = 'centre' 104 | ) { 105 | $size = $font->getSize(); 106 | $resize = 4; 107 | $colors = Image::getColorArrayAlpha($font->getColor()); 108 | $alpha = array_pop($colors); 109 | $FL = \FontLib\Font::load($font->getFile()); 110 | 111 | switch ($align) { 112 | case 'left': 113 | $vipsAlign = Align::LOW; 114 | break; 115 | case 'right': 116 | $vipsAlign = Align::HIGH; 117 | break; 118 | default: 119 | $vipsAlign = Align::CENTRE; 120 | } 121 | $text = $this->image->getVips()->text($string, [ 122 | 'font' => $FL->getFontFullName().' '.$size * $resize, 123 | 'fontfile' => $font->getFile(), 124 | 'width' => $width * $resize, 125 | 'align' => $vipsAlign, 126 | 'spacing' => 0, 127 | ]); 128 | 129 | if (0 !== $angle) { 130 | $text = $text->similarity(['angle' => $angle]); 131 | } 132 | 133 | $red = $text->newFromImage($colors)->copy(['interpretation' => 'srgb']); 134 | $overlay = $red->bandjoin($text); 135 | 136 | $overlay = $overlay->multiply([1, 1, 1, (255 - $alpha) / 255]); 137 | 138 | $overlay = $overlay->resize(1 / $resize); 139 | 140 | $newWidth = $overlay->width; 141 | $newHeight = $overlay->height; 142 | if (null !== $width && $overlay->width < $width) { 143 | $newWidth = $width; 144 | } 145 | if (null !== $height && $overlay->height < $height) { 146 | $newHeight = $height; 147 | } 148 | if ($newHeight !== $overlay->height || $newWidth !== $overlay->width) { 149 | $pixel = VipsImage::black(1, 1)->cast(BandFormat::UCHAR); 150 | $pixel = $pixel->embed(0, 0, $newWidth, $newHeight, ['extend' => Extend::COPY]); 151 | 152 | if ('centre' === $align) { 153 | $overlay = $pixel->insert($overlay, (int) ($newWidth - $overlay->width) / 2, ($newHeight - $overlay->height) / 2); 154 | } elseif ('left' === $align) { 155 | $overlay = $pixel->insert($overlay, (int) 0, ($newHeight - $overlay->height) / 2); 156 | } elseif ('right' === $align) { 157 | $overlay = $pixel->insert($overlay, (int) $newWidth - $overlay->width, ($newHeight - $overlay->height) / 2); 158 | } 159 | } 160 | 161 | $vips = $this->image->getVips(); 162 | if (!$vips->hasAlpha()) { 163 | $vips = $vips->bandjoin([0]); 164 | } 165 | // $vips = $vips->premultiply(); 166 | $vips = $this->image->pasteVipsImage($overlay, $position); 167 | $this->image->setVips($vips); 168 | } 169 | 170 | public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1) 171 | { 172 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 173 | } 174 | 175 | public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) 176 | { 177 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 178 | } 179 | 180 | public function dot(PointInterface $position, ColorInterface $color) 181 | { 182 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 183 | } 184 | 185 | public function circle(PointInterface $center, $radius, ColorInterface $color, $fill = false, $thickness = 1) 186 | { 187 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 188 | } 189 | 190 | public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1) 191 | { 192 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 193 | } 194 | 195 | public function line(PointInterface $start, PointInterface $end, ColorInterface $outline, $thickness = 1) 196 | { 197 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 198 | } 199 | 200 | public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) 201 | { 202 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 203 | } 204 | 205 | public function polygon(array $coordinates, ColorInterface $color, $fill = false, $thickness = 1) 206 | { 207 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 208 | } 209 | 210 | public function rectangle(PointInterface $leftTop, PointInterface $rightBottom, ColorInterface $color, $fill = false, $thickness = 1) 211 | { 212 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /lib/Imagine/Vips/Effects.php: -------------------------------------------------------------------------------- 1 | image = $image; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function gamma($correction) 40 | { 41 | try { 42 | $this->image->applyToLayers(function (VipsImage $vips) use ($correction): VipsImage { 43 | return $vips->gamma(['exponent' => $correction]); 44 | }); 45 | } catch (Exception $e) { 46 | throw new RuntimeException('Failed to apply gamma correction to the image', $e->getCode(), $e); 47 | } 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function negative() 56 | { 57 | try { 58 | $this->image->applyToLayers(function (VipsImage $vips): VipsImage { 59 | if ($vips->hasAlpha()) { 60 | $imageWithoutAlpha = $vips->extract_band(0, ['n' => $vips->bands - 1]); 61 | $alpha = $vips->extract_band($vips->bands - 1, ['n' => 1]); 62 | $newVips = $imageWithoutAlpha->invert()->bandjoin($alpha); 63 | } else { 64 | $newVips = $vips->invert(); 65 | } 66 | 67 | return $newVips; 68 | }); 69 | } catch (Exception $e) { 70 | throw new RuntimeException('Failed to negate the image', $e->getCode(), $e); 71 | } 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function grayscale() 80 | { 81 | try { 82 | $this->image->applyToLayers(function (VipsImage $vips): VipsImage { 83 | // FIXME: maybe more interpretations don't work 84 | if (Interpretation::CMYK == $vips->interpretation) { 85 | $vips = $vips->icc_import(['embedded' => true]); 86 | } 87 | $vips = $vips->colourspace(Interpretation::B_W); 88 | // remove icc_profile_data, since this can be wrong 89 | 90 | return $vips; 91 | }); 92 | try { 93 | $this->image->vipsCopy(); 94 | $this->image->getVips()->remove('icc-profile-data'); 95 | } catch (\Jcupitt\Vips\Exception $e) { 96 | // throws an exception if not existing, so just move on 97 | } 98 | } catch (Exception $e) { 99 | throw new RuntimeException('Failed to grayscale the image', $e->getCode(), $e); 100 | } 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | public function colorize(ColorInterface $color) 109 | { 110 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | public function sharpen() 117 | { 118 | try { 119 | $this->image->applyToLayers(function (VipsImage $vips): VipsImage { 120 | $oldinterpretation = $vips->interpretation; 121 | $vips = $vips->sharpen(); 122 | if ($oldinterpretation != $vips->interpretation) { 123 | $vips = $vips->colourspace($oldinterpretation); 124 | } 125 | 126 | return $vips; 127 | }); 128 | } catch (Exception $e) { 129 | throw new RuntimeException('Failed to sharpen the image', $e->getCode(), $e); 130 | } 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * {@inheritdoc} 137 | */ 138 | public function blur($sigma = 1) 139 | { 140 | try { 141 | $this->image->applyToLayers(function (VipsImage $vips) use ($sigma): VipsImage { 142 | return $vips->gaussblur($sigma); 143 | }); 144 | } catch (\Exception $e) { 145 | throw new RuntimeException('Failed to blur the image', $e->getCode(), $e); 146 | } 147 | 148 | return $this; 149 | } 150 | 151 | public function brightness($brightness) 152 | { 153 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter. You can use modulate() instead.'); 154 | } 155 | 156 | public function convolve(Matrix $matrix) 157 | { 158 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 159 | } 160 | 161 | /** 162 | * Modulates an image for brightness, saturation and hue. 163 | * 164 | * @param int $brightness Multiplier in percent 165 | * @param int $saturation Multiplier in percent 166 | * @param int $hue rotate by degrees on the color wheel, 0/360 don't change anything 167 | */ 168 | public function modulate(int $brightness = 100, int $saturation = 100, int $hue = 0): self 169 | { 170 | $vips = $this->image->getVips(); 171 | $originalColorspace = $vips->interpretation; 172 | $lch = $vips->colourspace(Interpretation::LCH); 173 | $multiply = [$brightness / 100, $saturation / 100, 1]; 174 | if ($lch->hasAlpha()) { 175 | $multiply[] = 1; 176 | } 177 | $lch = $lch->multiply($multiply); 178 | 179 | if (0 != $hue) { 180 | $add = [0, 0, $hue]; 181 | if ($lch->hasAlpha()) { 182 | $add[] = 0; 183 | } 184 | $lch = $lch->add($add); 185 | } 186 | // we can't convert from lch to rgb, needs srgb. 187 | if (Interpretation::RGB === $originalColorspace) { 188 | $originalColorspace = Interpretation::SRGB; 189 | } 190 | $image = $lch->colourspace($originalColorspace); 191 | $this->image->setVips($image); 192 | 193 | return $this; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/Imagine/Vips/Font.php: -------------------------------------------------------------------------------- 1 | file); 37 | 38 | $fontSize = (int) ($this->size * (96 / 72)); 39 | $text = VipsImage::text($string, [ 40 | 'fontfile' => $this->file, 41 | 'font' => $FL->getFontFullName().' '.$fontSize, 42 | 'dpi' => 72, 43 | 'height' => $fontSize, 44 | ]); 45 | 46 | return new Box($text->width, $text->height); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/Imagine/Vips/Image.php: -------------------------------------------------------------------------------- 1 | Interpretation::SRGB, 91 | PaletteInterface::PALETTE_GRAYSCALE => Interpretation::B_W, 92 | ]; 93 | 94 | private static $interpretationIccProfileMapping = [ 95 | Interpretation::B_W => self::ICC_DEFAULT_PROFILE_BW, 96 | Interpretation::GREY16 => self::ICC_DEFAULT_PROFILE_BW, 97 | Interpretation::CMYK => self::ICC_DEFAULT_PROFILE_CMYK, 98 | ]; 99 | 100 | /** 101 | * Constructs a new Image instance. 102 | */ 103 | public function __construct(VipsImage $vips, PaletteInterface $palette, MetadataBag $metadata) 104 | { 105 | $this->vips = $vips; 106 | 107 | $this->metadata = $metadata; 108 | $this->palette = $palette; 109 | $this->layers = new Layers($this); 110 | if ($palette instanceof CMYK) { 111 | // convert to RGB when it's CMYK to make life much easier later on. 112 | // If someone really needs CMYK support, there's lots of stuff failing, which needs to be fixed 113 | // But it could be added. 114 | $new = $this->usePalette(new RGB()); 115 | $this->vips = $new->getVips(); 116 | $this->palette = $new->palette(); 117 | $this->layers = $new->layers(); 118 | } 119 | } 120 | 121 | public function __clone() 122 | { 123 | parent::__clone(); 124 | $this->layers = new Layers($this); 125 | } 126 | 127 | /** 128 | * {@inheritdoc} 129 | */ 130 | public function __toString() 131 | { 132 | return $this->get('png'); 133 | } 134 | 135 | /** 136 | * Returns the underlying \Jcupitt\Vips\Image instance. 137 | * 138 | * @return VipsImage 139 | */ 140 | public function getVips() 141 | { 142 | return $this->vips; 143 | } 144 | 145 | /** 146 | * Copies the image, mainly needed when manipulation image metadata. 147 | * 148 | * @return VipsImage 149 | */ 150 | public function vipsCopy() 151 | { 152 | $this->vips = $this->vips->copy(); 153 | 154 | return $this->vips; 155 | } 156 | 157 | /** 158 | * @param bool $updatePalette In case the palette should changed and should be updated 159 | * 160 | * @return self 161 | */ 162 | public function setVips(VipsImage $vips, $updatePalette = false) 163 | { 164 | if ($this->vips->interpretation != $vips->interpretation) { 165 | $updatePalette = true; 166 | } 167 | 168 | $this->vips = $vips; 169 | if ($updatePalette) { 170 | $this->updatePalette(); 171 | } 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * {@inheritdoc} 178 | * 179 | * @return ImageInterface 180 | */ 181 | public function copy() 182 | { 183 | $clone = clone $this->vips->copy(); 184 | 185 | return new self($clone, $this->palette, clone $this->metadata); 186 | } 187 | 188 | /** 189 | * {@inheritdoc} 190 | * 191 | * @return ImageInterface 192 | */ 193 | public function crop(PointInterface $start, BoxInterface $size) 194 | { 195 | $thisBox = $this->getSize(); 196 | if (!$start->in($thisBox)) { 197 | throw new OutOfBoundsException('Crop coordinates must start at minimum 0, 0 position from top left corner, crop height and width must be positive integers and must not exceed the current image borders'); 198 | } 199 | // behave the same as imagick and gd, if box is too big, resize to max possible value, so that it 200 | // stops at right and bottom border 201 | if (!$thisBox->contains($size, $start)) { 202 | if ($start->getX() + $size->getWidth() > $thisBox->getWidth()) { 203 | $size = new Box($thisBox->getWidth() - $start->getX(), $size->getHeight()); 204 | } 205 | if ($start->getY() + $size->getHeight() > $thisBox->getHeight()) { 206 | $size = new Box($size->getWidth(), $thisBox->getHeight() - $start->getY()); 207 | } 208 | } 209 | try { 210 | $this->applyToLayers(function (VipsImage $vips) use ($size, $start): VipsImage { 211 | return $vips->crop($start->getX(), $start->getY(), $size->getWidth(), $size->getHeight()); 212 | }); 213 | } catch (VipsException $e) { 214 | throw new RuntimeException('Crop operation failed', $e->getCode(), $e); 215 | } 216 | 217 | return $this; 218 | } 219 | 220 | /** 221 | * {@inheritdoc} 222 | * 223 | * @return ImageInterface 224 | */ 225 | public function flipHorizontally() 226 | { 227 | try { 228 | $this->applyToLayers(function (VipsImage $vips): VipsImage { 229 | return $vips->flip(Direction::HORIZONTAL); 230 | }); 231 | } catch (VipsException $e) { 232 | throw new RuntimeException('Horizontal Flip operation failed', $e->getCode(), $e); 233 | } 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * {@inheritdoc} 240 | * 241 | * @return ImageInterface 242 | */ 243 | public function flipVertically() 244 | { 245 | try { 246 | $this->applyToLayers(function (VipsImage $vips): VipsImage { 247 | return $vips->flip(Direction::VERTICAL); 248 | }); 249 | } catch (VipsException $e) { 250 | throw new RuntimeException('Vertical Flip operation failed', $e->getCode(), $e); 251 | } 252 | 253 | return $this; 254 | } 255 | 256 | /** 257 | * {@inheritdoc} 258 | * 259 | * @return ImageInterface 260 | */ 261 | public function strip() 262 | { 263 | $this->strip = true; 264 | 265 | return $this; 266 | } 267 | 268 | /** 269 | * {@inheritdoc} 270 | * 271 | * @return ImageInterface 272 | */ 273 | public function paste(ImageInterface $image, PointInterface $start, $alpha = 100) 274 | { 275 | if (version_compare(Config::version(), '8.6', '<')) { 276 | throw new RuntimeException('The paste method needs at least vips 8.6'); 277 | } 278 | 279 | if (!$image instanceof self) { 280 | if (method_exists($image, 'convertToVips')) { 281 | $image = $image->convertToVips(); 282 | } else { 283 | throw new RuntimeException("Paste image needs to be a Imagine\Vips\Image object"); 284 | } 285 | } 286 | $this->vips = $this->pasteVipsImage($image->getVips(), $start, $alpha); 287 | 288 | return $this; 289 | } 290 | 291 | public function pasteVipsImage(VipsImage $vips, PointInterface $start, $alpha = 100) 292 | { 293 | if (!$vips->hasAlpha()) { 294 | if ($this->vips->hasAlpha()) { 295 | $vips = $vips->bandjoin([255]); 296 | } 297 | } 298 | if (!$this->vips->hasAlpha()) { 299 | if ($vips->hasAlpha()) { 300 | $this->vips = $this->vips->bandjoin([255]); 301 | } 302 | } 303 | 304 | $vips = self::extendImageWithVips($vips, $this->getSize(), $start); 305 | 306 | $this->applyToLayers(function (VipsImage $vipsLayer) use ($vips): VipsImage { 307 | return $vipsLayer->composite([$vips], [BlendMode::OVER]); 308 | }); 309 | 310 | return $this->vips->copyMemory(); 311 | } 312 | 313 | public static function generateImage(BoxInterface $size, ?ColorInterface $color = null) 314 | { 315 | $width = $size->getWidth(); 316 | $height = $size->getHeight(); 317 | $palette = null !== $color ? $color->getPalette() : new RGB(); 318 | $color = null !== $color ? $color : $palette->color('fff'); 319 | if ($palette instanceof RGB) { 320 | [$red, $green, $blue, $alpha] = self::getColorArrayAlpha($color); 321 | 322 | // Make a 1x1 pixel with all the channels and cast it to provided format. 323 | $pixel = VipsImage::black(1, 1)->add([$red, $green, $blue, $alpha])->cast(BandFormat::UCHAR); 324 | } elseif ($palette instanceof Grayscale) { 325 | [$gray, $alpha] = self::getColorArrayAlpha($color, 2); 326 | 327 | // Make a 1x1 pixel with all the channels and cast it to provided format. 328 | $pixel = VipsImage::black(1, 1)->add([$gray, $alpha])->cast(BandFormat::UCHAR); 329 | } else { 330 | throw new RuntimeException('Only RGB and Grayscale are supported for generating an image currently.'); 331 | } 332 | // Extend this 1x1 pixel to match the origin image dimensions. 333 | $vips = $pixel->embed(0, 0, $width, $height, ['extend' => Extend::COPY]); 334 | $vips = $vips->copy(['interpretation' => self::getInterpretation($color->getPalette())]); 335 | 336 | return $vips; 337 | } 338 | 339 | /** 340 | * Resizes current image and returns self. 341 | * 342 | * @param mixed $filter Not supported yet 343 | * 344 | * @return self 345 | */ 346 | public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDEFINED) 347 | { 348 | try { 349 | $this->applyToLayers(function (VipsImage $vips) use ($size): VipsImage { 350 | $original_format = $vips->format; 351 | if ($vips->hasAlpha()) { 352 | $vips = $vips->premultiply(); 353 | } 354 | $vips = $vips->resize($size->getWidth() / $vips->width, ['vscale' => $size->getHeight() / $vips->height]); 355 | if ($vips->hasAlpha()) { 356 | $vips = $vips->unpremultiply(); 357 | if ($vips->format != $original_format) { 358 | $vips = $vips->cast($original_format); 359 | } 360 | } 361 | 362 | return $vips; 363 | }); 364 | } catch (VipsException $e) { 365 | throw new RuntimeException('Resize operation failed', $e->getCode(), $e); 366 | } 367 | 368 | return $this; 369 | } 370 | 371 | public function applyToLayers(callable $callback) 372 | { 373 | $layers = $this->layers(); 374 | $n = \count($layers); 375 | for ($i = 0; $i < $n; ++$i) { 376 | $image = $layers[$i]; 377 | $vips = $image->getVips(); 378 | $vips = $callback($vips); 379 | $image->setVips($vips); 380 | } 381 | } 382 | 383 | /** 384 | * {@inheritdoc} 385 | * 386 | * @return ImageInterface 387 | */ 388 | public function rotate($angle, ?ColorInterface $background = null) 389 | { 390 | $color = $background ?: $this->palette->color('fff'); 391 | try { 392 | $this->applyToLayers(function (VipsImage $vips) use ($angle, $color): VipsImage { 393 | switch ($angle) { 394 | case 0: 395 | case 360: 396 | case -360: 397 | break; 398 | case 90: 399 | case -270: 400 | $vips = $vips->rot90(); 401 | break; 402 | case 180: 403 | case -180: 404 | $vips = $vips->rot180(); 405 | break; 406 | case 270: 407 | case -90: 408 | $vips = $vips->rot270(); 409 | break; 410 | default: 411 | if (!$vips->hasAlpha()) { 412 | // FIXME, alpha channel with Grey16 isn't doing well on rotation. there's only alpha in the end 413 | if (Interpretation::GREY16 !== $vips->interpretation) { 414 | $vips = $vips->bandjoin(255); 415 | } 416 | } 417 | if (version_compare(Config::version(), '8.6', '<')) { 418 | throw new RuntimeException('The rotate method for angles != 90, 180, 270 needs at least vips 8.6'); 419 | } 420 | $vips = $vips->similarity(['angle' => $angle, 'background' => self::getColorArrayAlpha($color, $vips->bands)]); 421 | } 422 | 423 | return $vips; 424 | }); 425 | } catch (VipsException $e) { 426 | throw new RuntimeException('Rotate operation failed. '.$e->getMessage(), $e->getCode(), $e); 427 | } 428 | 429 | return $this; 430 | } 431 | 432 | /** 433 | * {@inheritdoc} 434 | * 435 | * @return ImageInterface 436 | */ 437 | public function save($path = null, array $options = []) 438 | { 439 | /** @var Image $image */ 440 | $image = $this->prepareOutput($options); 441 | $options = $this->applyImageOptions($image->getVips(), $options, $path); 442 | $format = $options['format']; 443 | 444 | [$method, $saveOptions] = $this->getSaveMethodAndOptions($format, $options); 445 | $vips = $this->joinMultilayer($format, $image); 446 | 447 | if (null !== $method) { 448 | try { 449 | $vips->$method($path, $saveOptions); 450 | 451 | return $this; 452 | } catch (\Jcupitt\Vips\Exception $e) { 453 | // try the alternative approach if method is magicksave, if we fail here, mainly means that the magicksave stuff isn't 454 | // installed 455 | if ('magicksave' !== $method) { 456 | throw $e; 457 | } 458 | // if vips can't read it with libMagick, the alternatives can't either. throw an error 459 | if (strpos($e->getMessage(), 'libMagick error: no decode delegate for this image format') > 0) { 460 | throw new NotSupportedException('Image format is not supported.', 0, $e); 461 | } 462 | } 463 | } 464 | $alt = $this->convertToAlternativeForSave($options, $image, $format); 465 | 466 | return $alt->save($path, $options); 467 | } 468 | 469 | /** 470 | * {@inheritdoc} 471 | * 472 | * @return ImageInterface 473 | */ 474 | public function show($format, array $options = []) 475 | { 476 | header('Content-type: '.$this->getMimeType($format)); 477 | echo $this->get($format, $options); 478 | 479 | return $this; 480 | } 481 | 482 | /** 483 | * {@inheritdoc} 484 | */ 485 | public function get($format, array $options = []) 486 | { 487 | $options['format'] = $format; 488 | /** @var Image $image */ 489 | $image = $this->prepareOutput($options); 490 | $options = $this->applyImageOptions($image->getVips(), $options); 491 | [$method, $saveOptions] = $this->getSaveMethodAndOptions($format, $options); 492 | 493 | $vips = $this->joinMultilayer($format, $image); 494 | if (null !== $method) { 495 | try { 496 | $saveMethod = $method.'_buffer'; 497 | 498 | return $vips->$saveMethod($saveOptions); 499 | } catch (\Jcupitt\Vips\Exception $e) { 500 | // try the alternative approach if method is magicksave, if we fail here, mainly means that the magicksave stuff isn't 501 | // installed 502 | if ('magicksave' !== $method) { 503 | throw $e; 504 | } 505 | 506 | // if vips can't read it with libMagick, the alternatives can't either. throw an error 507 | if (strpos($e->getMessage(), 'libMagick error: no decode delegate for this image format') > 0) { 508 | throw new NotSupportedException('Image format is not supported.', 0, $e); 509 | } 510 | } 511 | } 512 | $alt = $this->convertToAlternativeForSave($options, $image, $format); 513 | 514 | return $alt->get($format, $options); 515 | } 516 | 517 | /** 518 | * {@inheritdoc} 519 | */ 520 | public function interlace($scheme) 521 | { 522 | // FIXME: implement in vips 523 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 524 | } 525 | 526 | /** 527 | * {@inheritdoc} 528 | */ 529 | public function draw() 530 | { 531 | return new Drawer($this); 532 | } 533 | 534 | /** 535 | * {@inheritdoc} 536 | */ 537 | public function effects() 538 | { 539 | return new Effects($this); 540 | } 541 | 542 | /** 543 | * {@inheritdoc} 544 | */ 545 | public function getSize() 546 | { 547 | $width = $this->vips->width; 548 | $height = $this->vips->height; 549 | 550 | return new Box($width, $height); 551 | } 552 | 553 | /** 554 | * {@inheritdoc} 555 | * 556 | * @return ImageInterface 557 | */ 558 | public function applyMask(ImageInterface $mask) 559 | { 560 | if (!$mask instanceof self) { 561 | throw new InvalidArgumentException('Can only apply instances of Imagine\Imagick\Image as masks'); 562 | } 563 | 564 | $size = $this->getSize(); 565 | $maskSize = $mask->getSize(); 566 | 567 | if ($size != $maskSize) { 568 | throw new InvalidArgumentException(sprintf('The given mask doesn\'t match current image\'s size, Current mask\'s dimensions are %s, while image\'s dimensions are %s', $maskSize, $size)); 569 | } 570 | 571 | $mask = $mask->getVips()->colourspace(Interpretation::B_W)->extract_band(0); 572 | // remove alpha 573 | if ($this->vips->hasAlpha()) { 574 | $new = $this->vips->extract_band(0, ['n' => $this->vips->bands - 1]); 575 | } else { 576 | $new = $this->vips->copy(); 577 | } 578 | $new = $new->bandjoin($mask); 579 | $newImage = clone $this; 580 | $newImage->setVips($new, true); 581 | 582 | return $newImage; 583 | } 584 | 585 | /** 586 | * {@inheritdoc} 587 | */ 588 | public function mask() 589 | { 590 | /** @var VipsImage $lch */ 591 | $lch = $this->vips->colourspace(Interpretation::LCH); 592 | $multiply = [1, 0, 1]; 593 | if ($lch->hasAlpha()) { 594 | $multiply[] = 1; 595 | } 596 | $lch = $lch->multiply($multiply); 597 | $lch = $lch->colourspace(Interpretation::B_W); 598 | // $lch = $lch->extract_band(0); 599 | $newImage = clone $this; 600 | $newImage->setVips($lch, true); 601 | 602 | return $newImage; 603 | } 604 | 605 | /** 606 | * {@inheritdoc} 607 | * 608 | * @return ImageInterface 609 | */ 610 | public function fill(FillInterface $fill) 611 | { 612 | // FIXME: implement in vips 613 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 614 | } 615 | 616 | /** 617 | * {@inheritdoc} 618 | */ 619 | public function histogram() 620 | { 621 | // FIXME: implement in vips 622 | throw new NotSupportedException(__METHOD__.' not implemented yet in the vips adapter.'); 623 | } 624 | 625 | /** 626 | * {@inheritdoc} 627 | */ 628 | public function getColorAt(PointInterface $point) 629 | { 630 | if (!$point->in($this->getSize())) { 631 | throw new RuntimeException(sprintf('Error getting color at point [%s,%s]. The point must be inside the image of size [%s,%s]', $point->getX(), $point->getY(), $this->getSize()->getWidth(), $this->getSize()->getHeight())); 632 | } 633 | 634 | try { 635 | $pixel = $this->vips->getpoint($point->getX(), $point->getY()); 636 | } catch (VipsException $e) { 637 | throw new RuntimeException('Error while getting image pixel color', $e->getCode(), $e); 638 | } 639 | if (\is_array($pixel)) { 640 | return $this->pixelToColor($pixel); 641 | } 642 | } 643 | 644 | /** 645 | * Returns a color given a pixel, depending the Palette context. 646 | * 647 | * Note : this method is public for PHP 5.3 compatibility 648 | * 649 | * @throws InvalidArgumentException In case a unknown color is requested 650 | * 651 | * @return ColorInterface 652 | */ 653 | public function pixelToColor(array $pixel) 654 | { 655 | if ($this->vips->hasAlpha()) { 656 | $alpha = (int) (array_pop($pixel) / 255 * 100); 657 | } else { 658 | $alpha = $this->palette->supportsAlpha() ? 100 : null; 659 | } 660 | if ($this->palette() instanceof RGB) { 661 | return $this->palette()->color($pixel, $alpha); 662 | } 663 | if ($this->palette() instanceof Grayscale) { 664 | $g = (int) $pixel[0]; 665 | 666 | return $this->palette()->color([$g, $g, $g], $alpha); 667 | } 668 | 669 | throw new NotSupportedException('Image has a not supported palette'); 670 | } 671 | 672 | /** 673 | * {@inheritdoc} 674 | */ 675 | public function layers() 676 | { 677 | return $this->layers; 678 | } 679 | 680 | /** 681 | * {@inheritdoc} 682 | */ 683 | public function usePalette(PaletteInterface $palette) 684 | { 685 | $new = clone $this; 686 | $vipsNew = $new->getVips(); 687 | 688 | if (!isset(self::$colorspaceMapping[$palette->name()])) { 689 | $newColorspace = Interpretation::SRGB; 690 | } else { 691 | $newColorspace = self::$colorspaceMapping[$palette->name()]; 692 | } 693 | 694 | $vipsNew = $this->applyProfile($palette->profile(), $vipsNew); 695 | $vipsNew = $vipsNew->colourspace($newColorspace); 696 | 697 | try { 698 | // try to remove icc-profile-data, not sure that's always correct, for srgb and 'bw' it seems to. 699 | $vipsNew = $vipsNew->copy(); 700 | $vipsNew->remove('icc-profile-data'); 701 | } catch (VipsException $e) { 702 | } 703 | 704 | $profile = $palette->profile(); 705 | // convert to a ICC profile, if it's not the default one 706 | $defaultProfile = $this->getDefaultProfileForInterpretation($vipsNew); 707 | if ($profile->name() != $defaultProfile) { 708 | $vipsNew = $this->applyProfile($palette->profile(), $vipsNew); 709 | } 710 | 711 | $this->setVips($vipsNew, true); 712 | 713 | return $this; 714 | } 715 | 716 | /** 717 | * {@inheritdoc} 718 | */ 719 | public function palette() 720 | { 721 | return $this->palette; 722 | } 723 | 724 | /** 725 | * {@inheritdoc} 726 | */ 727 | public function profile(ProfileInterface $profile) 728 | { 729 | $new = clone $this; 730 | $vips = $new->getVips(); 731 | $new->setVips($this->applyProfile($profile, $vips), true); 732 | 733 | return $new; 734 | } 735 | 736 | public static function getColorArrayAlpha(ColorInterface $color, $bands = 4): array 737 | { 738 | if ($color->getPalette() instanceof RGB) { 739 | return [ 740 | $color->getValue(ColorInterface::COLOR_RED), 741 | $color->getValue(ColorInterface::COLOR_GREEN), 742 | $color->getValue(ColorInterface::COLOR_BLUE), 743 | $color->getAlpha() / 100 * 255, 744 | ]; 745 | } 746 | if ($color->getPalette() instanceof Grayscale) { 747 | if ($bands <= 2) { 748 | return [ 749 | $color->getValue(ColorInterface::COLOR_GRAY), 750 | $color->getAlpha() / 100 * 255, 751 | ]; 752 | } 753 | 754 | return [ 755 | $color->getValue(ColorInterface::COLOR_GRAY), 756 | $color->getValue(ColorInterface::COLOR_GRAY), 757 | $color->getValue(ColorInterface::COLOR_GRAY), 758 | $color->getAlpha() / 100 * 255, 759 | ]; 760 | } 761 | throw new NotSupportedException('Image has a not supported palette.'); 762 | } 763 | 764 | /** 765 | * @param ImagineInterface|null $imagine the alternative imagine interface to use, autodetects, if not set 766 | * @param array $tiffOptions options to load the tiff image for conversion, eg ['strip' => true] 767 | * @param bool $asTiff 768 | * 769 | * @return ImageInterface 770 | */ 771 | public function convertToAlternative(?ImagineInterface $imagine = null, array $tiffOptions = [], $asTiff = false) 772 | { 773 | if (null === $imagine) { 774 | $oldMetaReader = null; 775 | if (class_exists('Imagick')) { 776 | $imagine = new \Imagine\Imagick\Imagine(); 777 | } else { 778 | $imagine = new \Imagine\Gd\Imagine(); 779 | } 780 | } else { 781 | $oldMetaReader = $imagine->getMetadataReader(); 782 | } 783 | 784 | // no need to reread meta data, saves lots of memory 785 | $imagine->setMetadataReader(new DefaultMetadataReader()); 786 | 787 | $image = $imagine->load($this->getImageStringForLoad($this->vips, $tiffOptions, $asTiff)); 788 | // readd metadata 789 | foreach ($this->metadata() as $key => $value) { 790 | $image->metadata()->offsetSet($key, $value); 791 | } 792 | 793 | if (null !== $oldMetaReader) { 794 | $imagine->setMetadataReader($oldMetaReader); 795 | } 796 | 797 | // if there's only one layer, we can do an early return 798 | if (1 == \count($this->layers())) { 799 | return $image; 800 | } 801 | $i = 0; 802 | if (!($this->layers() instanceof Layers)) { 803 | throw new \RuntimeException('Layers was not the correct class: '.Layers::class.', but '.\get_class($image->layers())); 804 | } 805 | foreach ($this->layers()->getResources() as $res) { 806 | if (0 == $i) { 807 | ++$i; 808 | continue; 809 | } 810 | $newLayer = $imagine->load($this->getImageStringForLoad($res)); 811 | $image->layers()->add($newLayer); 812 | ++$i; 813 | } 814 | try { 815 | // if there's a gif-delay option, set this 816 | $delay = $this->vips->get('gif-delay'); 817 | $loop = $this->vips->get('gif-loop'); 818 | $image->layers()->animate('gif', $delay * 10, $loop); 819 | } catch (Exception $e) { 820 | } 821 | 822 | return $image; 823 | } 824 | 825 | public function updatePalette() 826 | { 827 | $this->palette = Imagine::createPalette($this->vips); 828 | } 829 | 830 | public static function isOpaque(VipsImage $vips) 831 | { 832 | if (!$vips->hasAlpha()) { 833 | return true; 834 | } 835 | 836 | return 255 === (int) $vips->extract_band($vips->bands - 1)->min(); 837 | } 838 | 839 | public static function extendImageWithVips(VipsImage $vips, BoxInterface $box, PointInterface $start) 840 | { 841 | if ($vips->bands > 2) { 842 | $color = new \Imagine\Image\Palette\Color\RGB(new RGB(), [255, 255, 255], 0); 843 | } else { 844 | $color = new Gray(new Grayscale(), [255], 0); 845 | } 846 | if (!$vips->hasAlpha()) { 847 | $vips = $vips->bandjoin([255]); 848 | } 849 | $new = self::generateImage($box, $color); 850 | $vips = $new->insert($vips, $start->getX(), $start->getY()); 851 | 852 | return $vips; 853 | } 854 | 855 | protected function applyProfile(ProfileInterface $profile, VipsImage $vips) 856 | { 857 | $defaultProfile = $this->getDefaultProfileForInterpretation($vips); 858 | try { 859 | $vips = $vips->icc_transform( 860 | VipsProfile::fromRawData($profile->data())->path(), 861 | [ 862 | 'embedded' => true, 863 | 'intent' => 'perceptual', 864 | 'input_profile' => __DIR__.'/../../resources/colorprofiles/'.$defaultProfile, 865 | ] 866 | ); 867 | } catch (Exception $e) { 868 | // if there's an exception, usually something is wrong with the embedded profile 869 | // try without 870 | try { 871 | $vips = $vips->icc_transform( 872 | VipsProfile::fromRawData($profile->data())->path(), 873 | [ 874 | 'embedded' => false, 875 | 'intent' => 'perceptual', 876 | 'input_profile' => __DIR__.'/../../resources/colorprofiles/'.$defaultProfile, 877 | ] 878 | ); 879 | } catch (Exception $e) { 880 | throw new RuntimeException('icc_transform error. Message: '.$e->getMessage().'. With defaultProfile: '.$defaultProfile); 881 | } 882 | } 883 | 884 | return $vips; 885 | } 886 | 887 | protected static function getInterpretation(PaletteInterface $palette) 888 | { 889 | if ($palette instanceof RGB) { 890 | return Interpretation::SRGB; 891 | } 892 | if ($palette instanceof Grayscale) { 893 | return Interpretation::B_W; 894 | } 895 | if ($palette instanceof CMYK) { 896 | return Interpretation::CMYK; 897 | } 898 | throw new NotSupportedException('Image has a not supported palette'); 899 | } 900 | 901 | /** 902 | * @param VipsImage $vips 903 | * 904 | * @return string 905 | */ 906 | protected function getDefaultProfileForInterpretation($vips) 907 | { 908 | $defaultProfile = self::ICC_DEFAULT_PROFILE_DEFAULT; 909 | if (isset(self::$interpretationIccProfileMapping[$vips->interpretation])) { 910 | $defaultProfile = self::$interpretationIccProfileMapping[$vips->interpretation]; 911 | } 912 | 913 | return $defaultProfile; 914 | } 915 | 916 | /** 917 | * @param array|null $tiffOptions options to load the tiff image for conversion, eg ['strip' => true] 918 | * 919 | * @return string 920 | */ 921 | protected function getImageStringForLoad(VipsImage $res, $tiffOptions = [], $asTiff = false) 922 | { 923 | $options = array_merge(['compression' => ForeignTiffCompression::NONE], $tiffOptions); 924 | 925 | if ($asTiff) { 926 | return $res->tiffsave_buffer($options); 927 | } 928 | 929 | return $res->pngsave_buffer($options); 930 | } 931 | 932 | /** 933 | * @param string $path 934 | */ 935 | private function prepareOutput(array $options, $path = null): self 936 | { 937 | // convert to RGB if it's cmyk 938 | if ($this->palette() instanceof CMYK) { 939 | return $this->usePalette(new RGB()); 940 | } 941 | 942 | return $this; 943 | } 944 | 945 | /** 946 | * Internal. 947 | * 948 | * Flatten the image. 949 | */ 950 | private function flatten() 951 | { 952 | try { 953 | if ($this->vips->hasAlpha()) { 954 | return $this->vips->flatten(); 955 | } 956 | 957 | return $this->vips; 958 | } catch (VipsException $e) { 959 | throw new RuntimeException('Flatten operation failed', $e->getCode(), $e); 960 | } 961 | } 962 | 963 | /** 964 | * Internal. 965 | * 966 | * Applies options before save or output 967 | * 968 | * @param string $path 969 | * 970 | * @throws InvalidArgumentException 971 | * @throws RuntimeException 972 | */ 973 | private function applyImageOptions(VipsImage $vips, array $options, $path = null): array 974 | { 975 | if (isset($options['format'])) { 976 | $format = $options['format']; 977 | } elseif ('' !== $extension = pathinfo($path, \PATHINFO_EXTENSION)) { 978 | $format = $extension; 979 | } else { 980 | // FIXME, may not work 981 | $format = pathinfo($vips->filename, \PATHINFO_EXTENSION); 982 | } 983 | $format = strtolower($format); 984 | $options['format'] = $format; 985 | 986 | if (!isset($options[self::OPTION_JPEG_QUALITY]) && \in_array($format, ['jpeg', 'jpg', 'pjpeg'], true)) { 987 | $options[self::OPTION_JPEG_QUALITY] = 92; 988 | } 989 | 990 | if (!isset($options[self::OPTION_JXL_QUALITY]) && \in_array($format, ['jxl'], true)) { 991 | $options[self::OPTION_JXL_QUALITY] = 92; 992 | } 993 | if (!isset($options[self::OPTION_JXL_LOSSLESS]) && \in_array($format, ['jxl'], true)) { 994 | $options[self::OPTION_JXL_LOSSLESS] = false; 995 | } 996 | 997 | if (!isset($options[self::OPTION_PNG_QUALITY]) && \in_array($format, ['png'], true)) { 998 | $options[self::OPTION_PNG_QUALITY] = 100; // don't do pngquant, if set to 100 999 | } 1000 | if (!isset($options[self::OPTION_WEBP_QUALITY]) && \in_array($format, ['webp'], true)) { 1001 | $options[self::OPTION_WEBP_QUALITY] = 80; // FIXME: correct value? 1002 | } 1003 | if (!isset($options[self::OPTION_WEBP_LOSSLESS]) && \in_array($format, ['webp'], true)) { 1004 | $options[self::OPTION_WEBP_LOSSLESS] = false; 1005 | } 1006 | 1007 | if ('png' === $format) { 1008 | if (!isset($options[self::OPTION_PNG_COMPRESSION_LEVEL])) { 1009 | $options[self::OPTION_PNG_COMPRESSION_LEVEL] = 7; 1010 | } 1011 | // FIXME: implement different png_compression_filter 1012 | if (!isset($options[self::OPTION_PNG_COMPRESSION_FILTER])) { 1013 | $options[self::OPTION_PNG_COMPRESSION_FILTER] = 5; 1014 | } 1015 | } 1016 | /* FIXME: do we need this? 1017 | if (isset($options['resolution-units']) && isset($options['resolution-x']) && isset($options['resolution-y'])) { 1018 | if ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERCENTIMETER) { 1019 | $vips->setImageUnits(\Imagick::RESOLUTION_PIXELSPERCENTIMETER); 1020 | } elseif ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERINCH) { 1021 | $vips->setImageUnits(\Imagick::RESOLUTION_PIXELSPERINCH); 1022 | } else { 1023 | throw new RuntimeException('Unsupported image unit format'); 1024 | } 1025 | 1026 | $filter = ImageInterface::FILTER_UNDEFINED; 1027 | if (!empty($options['resampling-filter'])) { 1028 | $filter = $options['resampling-filter']; 1029 | } 1030 | 1031 | $image->setImageResolution($options['resolution-x'], $options['resolution-y']); 1032 | $image->resampleImage($options['resolution-x'], $options['resolution-y'], $this->getFilter($filter), 0); 1033 | } 1034 | */ 1035 | return $options; 1036 | } 1037 | 1038 | /** 1039 | * Internal. 1040 | * 1041 | * Get the mime type based on format. 1042 | * 1043 | * @param string $format 1044 | * 1045 | * @throws RuntimeException 1046 | * 1047 | * @return string mime-type 1048 | */ 1049 | private function getMimeType($format) 1050 | { 1051 | static $mimeTypes = [ 1052 | 'jpeg' => 'image/jpeg', 1053 | 'jpg' => 'image/jpeg', 1054 | 'gif' => 'image/gif', 1055 | 'png' => 'image/png', 1056 | 'wbmp' => 'image/vnd.wap.wbmp', 1057 | 'xbm' => 'image/xbm', 1058 | 'webp' => 'image/webp', 1059 | ]; 1060 | 1061 | if (!isset($mimeTypes[$format])) { 1062 | throw new RuntimeException(sprintf('Unsupported format given. Only %s are supported, %s given', implode(', ', array_keys($mimeTypes)), $format)); 1063 | } 1064 | 1065 | return $mimeTypes[$format]; 1066 | } 1067 | 1068 | private function getColorArray(ColorInterface $color): array 1069 | { 1070 | return [$color->getValue(ColorInterface::COLOR_RED), 1071 | $color->getValue(ColorInterface::COLOR_GREEN), 1072 | $color->getValue(ColorInterface::COLOR_BLUE), 1073 | ]; 1074 | } 1075 | 1076 | private function applySaveOptions(array $saveOptions, array $options): array 1077 | { 1078 | if (isset($options['vips'])) { 1079 | $saveOptions = array_merge($saveOptions, $options['vips']); 1080 | } 1081 | 1082 | return $saveOptions; 1083 | } 1084 | 1085 | /** 1086 | * @param string $format 1087 | */ 1088 | private function getSaveMethodAndOptions($format, array $options): array 1089 | { 1090 | $method = null; 1091 | $saveOptions = []; 1092 | if ('jpg' == $format || 'jpeg' == $format) { 1093 | $saveOptions = $this->applySaveOptions(['strip' => $this->strip, 'Q' => $options[self::OPTION_JPEG_QUALITY], 'interlace' => true], $options); 1094 | $method = 'jpegsave'; 1095 | } elseif ('jxl' == $format) { 1096 | $jxlOptions = [ 1097 | 'strip' => $this->strip, 1098 | 'lossless' => $options[self::OPTION_JXL_LOSSLESS], 1099 | ]; 1100 | if (isset($options[self::OPTION_JXL_DISTANCE])) { 1101 | $jxlOptions['distance'] = $options[self::OPTION_JXL_DISTANCE]; 1102 | } else { 1103 | $jxlOptions['Q'] = $options[self::OPTION_JXL_QUALITY]; 1104 | } 1105 | 1106 | if (isset($options[self::OPTION_JXL_EFFORT])) { 1107 | $jxlOptions['effort'] = $options[self::OPTION_JXL_EFFORT]; 1108 | } 1109 | $saveOptions = $this->applySaveOptions($jxlOptions, $options); 1110 | $method = 'jxlsave'; 1111 | } elseif ('png' == $format) { 1112 | $pngOptions = ['strip' => $this->strip, 'compression' => $options[self::OPTION_PNG_COMPRESSION_LEVEL]]; 1113 | if ($options[self::OPTION_PNG_QUALITY] < 100) { 1114 | $this->convertTo8BitMax(); 1115 | $pngOptions['Q'] = $options[self::OPTION_PNG_QUALITY]; 1116 | $pngOptions['palette'] = true; 1117 | } 1118 | $saveOptions = $this->applySaveOptions($pngOptions, $options); 1119 | $method = 'pngsave'; 1120 | } elseif ('webp' == $format) { 1121 | $saveOptions = $this->applySaveOptions([ 1122 | 'strip' => $this->strip, 1123 | 'Q' => $options[self::OPTION_WEBP_QUALITY], 1124 | 'lossless' => $options[self::OPTION_WEBP_LOSSLESS], 1125 | ], $options); 1126 | if (isset($options[self::OPTION_WEBP_REDUCTION_EFFORT]) && version_compare(Config::version(), '8.8', '>=')) { 1127 | $saveOptions['reduction_effort'] = $options[self::OPTION_WEBP_REDUCTION_EFFORT]; 1128 | } 1129 | 1130 | $method = 'webpsave'; 1131 | } elseif ('tiff' == $format) { 1132 | $saveOptions = $this->applySaveOptions([], $options); 1133 | $method = 'tiffsave'; 1134 | } elseif (('heif' == $format || 'heic' == $format) && version_compare(Config::version(), '8.8.0', '>=')) { 1135 | $saveOptions = $this->applySaveOptions(['Q' => $options[self::OPTION_HEIF_QUALITY], 'strip' => $this->strip], $options); 1136 | $method = 'heifsave'; 1137 | } elseif (('avif' == $format) && version_compare(Config::version(), '8.9.0', '>=')) { 1138 | $saveOptions = $this->applySaveOptions(['Q' => $options[self::OPTION_AVIF_QUALITY], 'compression' => 'av1', 'strip' => $this->strip], $options); 1139 | $method = 'heifsave'; 1140 | } elseif ('gif' == $format) { 1141 | if (version_compare(Config::version(), '8.12.0', '>=') && !(isset($options['force_magick']) && true === $options['force_magick'])) { 1142 | $saveOptions = $this->applySaveOptions([], $options); 1143 | $method = 'gifsave'; 1144 | } else { 1145 | $saveOptions = $this->applySaveOptions(['format' => 'gif'], $options); 1146 | $method = 'magicksave'; 1147 | } 1148 | $delayProperty = 'delay'; 1149 | if (version_compare(Config::version(), '8.9', '<')) { 1150 | $delayProperty = 'gif-delay'; 1151 | } 1152 | if (0 === $this->vips->typeof($delayProperty)) { 1153 | $this->layers()->animate('gif', Layers::DEFAULT_GIF_DELAY, 0); 1154 | } 1155 | } elseif ('jp2' == $format) { 1156 | $saveOptions = $this->applySaveOptions(['format' => 'jp2', 'quality' => $options['jp2_quality']], $options); 1157 | $method = 'magicksave'; 1158 | } else { 1159 | // use magicksave, if available and possible 1160 | // ppm in vips has some strange issues, save in fallback... 1161 | if ('ppm' !== $format && version_compare(Config::version(), '8.7.0', '>=')) { 1162 | if ('heic' == $format || 'heif' === $format) { 1163 | $saveOptions = ['quality' => $options[self::OPTION_HEIF_QUALITY], 'format' => $format]; 1164 | $method = 'magicksave'; 1165 | } 1166 | // if only the format option is set, we can use that, otherwise we fall back to the alternative 1167 | // since they may be options, magicksave doesn't support yet 1168 | elseif (isset($options['format']) && 1 === \count($options)) { 1169 | $saveOptions = ['format' => $format]; 1170 | $method = 'magicksave'; 1171 | } 1172 | } 1173 | } 1174 | 1175 | return [$method, $saveOptions]; 1176 | } 1177 | 1178 | private function convertToAlternativeForSave(array $options, self $image, string $format): ImageInterface 1179 | { 1180 | // fallback to imagemagick or gd 1181 | $alt = $image->convertToAlternative(); 1182 | // set heif quality, if heif is asked for 1183 | if ('heic' === $format || 'heif' === $format) { 1184 | if ($alt instanceof \Imagine\Imagick\Image && isset($options[self::OPTION_HEIF_QUALITY])) { 1185 | $alt->getImagick()->setCompressionQuality($options[self::OPTION_HEIF_QUALITY]); 1186 | } 1187 | } 1188 | 1189 | return $alt; 1190 | } 1191 | 1192 | /** 1193 | * @param \Imagine\Vips\Image $image 1194 | * 1195 | * @throws \Imagine\Exception\OutOfBoundsException 1196 | * @throws \Imagine\Exception\RuntimeException 1197 | * @throws \Jcupitt\Vips\Exception 1198 | */ 1199 | private function joinMultilayer($format, self $image): VipsImage 1200 | { 1201 | $vips = $this->getVips(); 1202 | if ((('webp' === $format && version_compare(Config::version(), '8.8.0', '>=')) 1203 | || 'gif' === $format) 1204 | && \count($image->layers()) > 1) { 1205 | $vips = $vips->copy(); 1206 | $height = $vips->height; 1207 | $width = $vips->width; 1208 | $vips->set('page-height', $height); 1209 | 1210 | if (!($image->layers() instanceof Layers)) { 1211 | throw new \RuntimeException('Layers was not the correct class: '.Layers::class.', but '.\get_class($image->layers())); 1212 | } 1213 | foreach ($image->layers()->getResources() as $_k => $_v) { 1214 | if (0 === $_k) { 1215 | continue; 1216 | } 1217 | // make frame the same size as the original, if height is not the same (if width is not the same, join will take care of it 1218 | if ($_v->height !== $height) { 1219 | $_v = $_v->embed(0, 0, $width, $height, ['extend' => Extend::BACKGROUND]); 1220 | } 1221 | $vips = $vips->join($_v, 'vertical', ['expand' => true]); 1222 | } 1223 | } 1224 | 1225 | return $vips; 1226 | } 1227 | 1228 | private function convertTo8BitMax(): self 1229 | { 1230 | switch ($this->vips->interpretation) { 1231 | case Interpretation::GREY16: 1232 | $this->vips = $this->vips->colourspace(Interpretation::B_W); 1233 | break; 1234 | case Interpretation::RGB16: 1235 | $this->vips = $this->vips->colourspace(Interpretation::SRGB); 1236 | break; 1237 | } 1238 | 1239 | return $this; 1240 | } 1241 | } 1242 | -------------------------------------------------------------------------------- /lib/Imagine/Vips/Imagine.php: -------------------------------------------------------------------------------- 1 | calls the following php function 41 | * 42 | * max_mem -> vips_cache_set_max_mem 43 | * max_ops -> vips_cache_set_max 44 | * max_files -> vips_cache_set_max_files 45 | * concurrency -> vips_concurrency_set 46 | * 47 | * @throws RuntimeException 48 | */ 49 | public function __construct(array $config = []) 50 | { 51 | if (class_exists(FFI::class)) { 52 | if (!\extension_loaded('ffi')) { 53 | throw new RuntimeException('ffi extension not installed'); 54 | } 55 | } else { 56 | if (!\extension_loaded('vips')) { 57 | throw new RuntimeException('vips extension not installed'); 58 | } 59 | } 60 | foreach ($config as $key => $value) { 61 | switch ($key) { 62 | case 'max_mem': 63 | Config::cacheSetMaxMem($value); 64 | break; 65 | case 'max_ops': 66 | Config::cacheSetMax($value); 67 | break; 68 | case 'max_files': 69 | Config::cacheSetMaxFiles($value); 70 | break; 71 | case 'concurrency': 72 | Config::concurrencySet($value); 73 | break; 74 | } 75 | } 76 | } 77 | 78 | public function open($path, $loadOptions = []) 79 | { 80 | $path = $this->checkPath($path); 81 | 82 | try { 83 | $loader = VipsImage::findLoad($path); 84 | $loadOptions = $this->getLoadOptions($loader, $loadOptions); 85 | $vips = VipsImage::newFromFile($path, $loadOptions); 86 | if ('VipsForeignLoadTiffFile' === $loader) { 87 | $vips = $this->removeUnnecessaryAlphaChannels($vips); 88 | } 89 | 90 | return new Image($vips, self::createPalette($vips), $this->getMetadataReader()->readFile($path)); 91 | } catch (\Exception $e) { 92 | throw new RuntimeException(sprintf('Unable to open image %s', $path), $e->getCode(), $e); 93 | } 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function create(BoxInterface $size, ?ColorInterface $color = null) 100 | { 101 | $vips = Image::generateImage($size, $color); 102 | 103 | return new Image($vips, self::createPalette($vips), new MetadataBag()); 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function load($string, $loadOptions = []) 110 | { 111 | try { 112 | $loader = VipsImage::findLoadBuffer($string); 113 | $loadOptions = $this->getLoadOptions($loader, $loadOptions); 114 | $vips = VipsImage::newFromBuffer($string, '', $loadOptions); 115 | if ('VipsForeignLoadTiffBuffer' === $loader) { 116 | $vips = $this->removeUnnecessaryAlphaChannels($vips); 117 | } 118 | 119 | return new Image($vips, self::createPalette($vips), $this->getMetadataReader()->readData($string)); 120 | } catch (\Exception $e) { 121 | // sometimes we have files with colorspaces vips does not support (heic files for eaxample), 122 | // let's try loading them with imagick, 123 | // and convert them to png and then load again with vips. 124 | // not the fastest thing, of course, but fine for our usecase 125 | if (false !== strpos($e->getMessage(), 'magickload_buffer: unsupported colorspace') && class_exists('Imagick')) { 126 | $im = new \Imagick(); 127 | $im->readImageBlob($string); 128 | $im->setFormat('png'); 129 | 130 | return $this->load($im->getImageBlob(), $loadOptions); 131 | } 132 | 133 | if ('gifload_buffer: Image is defective, decoding aborted' === $e->getMessage()) { 134 | throw new RuntimeException('Image is defective, decoding aborted', $e->getCode(), $e); 135 | } 136 | 137 | throw new RuntimeException('Could not load image from string. Message: '.$e->getMessage(), $e->getCode(), $e); 138 | } 139 | } 140 | 141 | /** 142 | * {@inheritdoc} 143 | */ 144 | public function read($resource) 145 | { 146 | if (!\is_resource($resource)) { 147 | throw new InvalidArgumentException('Variable does not contain a stream resource'); 148 | } 149 | 150 | $content = stream_get_contents($resource); 151 | 152 | return $this->load($content); 153 | } 154 | 155 | /** 156 | * {@inheritdoc} 157 | */ 158 | public function font($file, $size, ColorInterface $color) 159 | { 160 | return new Font($file, $size, $color); 161 | } 162 | 163 | /** 164 | * Returns the palette corresponding to an VIPS resource colorspace. 165 | * 166 | * @throws NotSupportedException 167 | * 168 | * @return PaletteInterface 169 | */ 170 | public static function createPalette(VipsImage $vips) 171 | { 172 | switch ($vips->interpretation) { 173 | case Interpretation::RGB: 174 | case Interpretation::RGB16: 175 | case Interpretation::SRGB: 176 | $palette = new RGB(); 177 | break; 178 | case Interpretation::CMYK: 179 | $palette = new CMYK(); 180 | break; 181 | case Interpretation::GREY16: 182 | case Interpretation::B_W: 183 | $palette = new Grayscale(); 184 | break; 185 | default: 186 | throw new NotSupportedException('Only RGB, CMYK and Grayscale colorspace are currently supported'); 187 | } 188 | try { 189 | $profile = $vips->get('icc-profile-data'); 190 | $palette->useProfile(VipsProfile::fromRawData($profile)); 191 | } catch (Exception $e) { 192 | } 193 | 194 | return $palette; 195 | } 196 | 197 | /** 198 | * Checks, if the necessary Libraries are installed. 199 | * 200 | * @return bool 201 | */ 202 | public static function hasVipsInstalled() 203 | { 204 | try { 205 | // if we're still on php-vips 1.0, check if 'vips' extension is installed 206 | if (\extension_loaded('vips')) { 207 | return true; 208 | } 209 | 210 | if (class_exists(FFI::class) 211 | // if ffi extension is not installed, we can't use php-vips 2 212 | && extension_loaded('ffi') 213 | // preload is not yet supported by vips see https://github.com/libvips/php-vips 214 | && '1' === \ini_get('ffi.enable') 215 | ) { 216 | // this will throw an exception, if libvips is not installed 217 | // will return false in the catch block 218 | Config::version(); 219 | 220 | return true; 221 | } 222 | 223 | return false; 224 | } catch (\Exception $e) { 225 | return false; 226 | } 227 | } 228 | 229 | protected function getLoadOptions($loader, $loadOptions = []) 230 | { 231 | $options = []; 232 | switch ($loader) { 233 | case 'VipsForeignLoadJpegFile': 234 | case 'VipsForeignLoadJpegBuffer': 235 | $options['autorotate'] = true; 236 | break; 237 | case 'VipsForeignLoadHeifFile': 238 | case 'VipsForeignLoadHeifBuffer': 239 | $options['autorotate'] = true; 240 | $options['n'] = -1; 241 | break; 242 | case 'VipsForeignLoadGifFile': 243 | case 'VipsForeignLoadGifBuffer': 244 | $options['n'] = -1; 245 | break; 246 | case 'VipsForeignLoadWebpFile': 247 | case 'VipsForeignLoadWebpBuffer': 248 | $options['n'] = -1; 249 | break; 250 | } 251 | $options = array_merge($options, $loadOptions); 252 | // FIXME: remove not allowed options 253 | 254 | if (isset($options['shrink'])) { 255 | switch ($loader) { 256 | case 'VipsForeignLoadJpegFile': 257 | case 'VipsForeignLoadJpegBuffer': 258 | case 'VipsForeignLoadWebpFile': 259 | case 'VipsForeignLoadWebpBuffer': 260 | break; 261 | default: 262 | unset($options['shrink']); 263 | } 264 | } 265 | 266 | return $options; 267 | } 268 | 269 | /** 270 | * Some files (esp. tiff) can have more than one alpha layer.. We just remove all except one. 271 | * Not sure, this is the right approach, but good enough for now. 272 | * 273 | * @param \Jcupitt\Vips\Image $vips 274 | * 275 | * @return \Jcupitt\Vips\Image 276 | */ 277 | protected function removeUnnecessaryAlphaChannels($vips) 278 | { 279 | $lastVipsWithAlpha = $vips; 280 | 281 | while ($vips->hasAlpha()) { 282 | $lastVipsWithAlpha = $vips; 283 | $vips = $vips->extract_band(0, ['n' => $vips->bands - 1]); 284 | } 285 | 286 | return $lastVipsWithAlpha; 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /lib/Imagine/Vips/Layers.php: -------------------------------------------------------------------------------- 1 | image = $image; 51 | 52 | $vips = $image->getVips(); 53 | // try extracting layers 54 | if (null !== $layers) { 55 | $this->layers = $layers->layers; 56 | $this->resources = $layers->resources; 57 | $this->count = \count($layers->resources) + \count($this->layers); 58 | } else { 59 | try { 60 | if ($vips->get('page-height')) { 61 | $page_height = $vips->get('page-height'); 62 | $total_height = $vips->height; 63 | $total_width = $vips->width; 64 | for ($i = 0; $i < ($total_height / $page_height); ++$i) { 65 | $this->resources[$i] = $vips->crop(0, $page_height * $i, $total_width, $page_height); 66 | } 67 | $image->setVips($this->resources[0]); 68 | } 69 | } catch (Exception $e) { 70 | $this->resources[0] = $vips; 71 | } 72 | $this->count = \count($this->resources); 73 | // always set the first layer 74 | $this->layers[0] = $this->image; 75 | // we don't need it, it's in $this->image 76 | unset($this->resources[0]); 77 | } 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function merge() 84 | { 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function animate($format, $delay, $loops) 91 | { 92 | $vips = $this->image->vipsCopy(); 93 | if (version_compare(Config::version(), '8.9', '<')) { 94 | $vips->set('gif-delay', $delay / 10); 95 | } else { 96 | $vips->set('delay', array_fill(0, \count($this), $delay)); 97 | } 98 | $vips->set('gif-loop', $loops); 99 | if (0 === $vips->typeof('page-height')) { 100 | $vips->set('page-height', (int) ($vips->height / \count($this))); 101 | } 102 | $this->image->setVips($vips); 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | public function coalesce() 111 | { 112 | $merged = $this->extractAt(0)->getVips(); 113 | $width = $merged->width; 114 | $height = $merged->height; 115 | $i = 0; 116 | foreach ($this->getResources() as $res) { 117 | if (0 == $i) { 118 | ++$i; 119 | continue; 120 | } 121 | 122 | // if width and height are the same and it is opaque, we don't have to composite 123 | if (($res->width === $width && $res->height === $height) && Image::isOpaque($res)) { 124 | ++$i; 125 | continue; 126 | } 127 | 128 | $merged = $merged->composite([$res], [BlendMode::OVER])->copyMemory(); 129 | 130 | $frame = clone $merged; 131 | unset($this->layers[$i]); 132 | $this->resources[$i] = $frame; 133 | ++$i; 134 | } 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * {@inheritdoc} 141 | */ 142 | public function current(): Image 143 | { 144 | return $this->extractAt($this->offset); 145 | } 146 | 147 | /** 148 | * {@inheritdoc} 149 | */ 150 | public function key(): int 151 | { 152 | return $this->offset; 153 | } 154 | 155 | /** 156 | * {@inheritdoc} 157 | */ 158 | public function next(): void 159 | { 160 | ++$this->offset; 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | */ 166 | public function rewind(): void 167 | { 168 | $this->offset = 0; 169 | } 170 | 171 | /** 172 | * {@inheritdoc} 173 | */ 174 | public function valid(): bool 175 | { 176 | return $this->offset < \count($this); 177 | } 178 | 179 | /** 180 | * {@inheritdoc} 181 | */ 182 | public function count(): int 183 | { 184 | return $this->count; 185 | } 186 | 187 | /** 188 | * {@inheritdoc} 189 | */ 190 | public function offsetExists($offset): bool 191 | { 192 | return \is_int($offset) && $offset >= 0 && $offset < \count($this); 193 | } 194 | 195 | /** 196 | * {@inheritdoc} 197 | */ 198 | public function offsetGet($offset): Image 199 | { 200 | return $this->extractAt($offset); 201 | } 202 | 203 | /** 204 | * {@inheritdoc} 205 | */ 206 | public function offsetSet($offset, $image): void 207 | { 208 | if (null === $offset) { 209 | $offset = $this->count; 210 | } 211 | 212 | if (!(isset($this->layers[$offset]) || isset($this->resources[$offset]))) { 213 | ++$this->count; 214 | } 215 | 216 | $this->layers[$offset] = $image; 217 | 218 | if (isset($this->resources[$offset])) { 219 | unset($this->resources[$offset]); 220 | } 221 | 222 | if (2 === $this->count) { 223 | $this->image->vipsCopy(); 224 | $this->image->getVips()->set('page-height', $this->image->getVips()->height); 225 | } 226 | } 227 | 228 | /** 229 | * {@inheritdoc} 230 | */ 231 | public function offsetUnset($offset): void 232 | { 233 | throw new NotSupportedException('Removing frames is not supported yet.'); 234 | } 235 | 236 | public function getResource($offset) 237 | { 238 | if (0 === $offset) { 239 | return $this->image->getVips(); 240 | } 241 | // if we already have an image object for this $offset, use this 242 | if (isset($this->layers[$offset])) { 243 | return $this->layers[$offset]->getVips(); 244 | } 245 | 246 | return $this->resources[$offset]; 247 | } 248 | 249 | /** 250 | * @return \Jcupitt\Vips\Image[] 251 | */ 252 | public function getResources() 253 | { 254 | $resources = []; 255 | $count = \count($this); 256 | for ($i = 0; $i < $count; ++$i) { 257 | $resources[$i] = $this->getResource($i); 258 | } 259 | 260 | return $resources; 261 | } 262 | 263 | /** 264 | * Returns the delays in milliseconds per frame as array (or null, if not set yet). 265 | * 266 | * @throws \Imagine\Exception\RuntimeException 267 | * 268 | * @return array|null 269 | */ 270 | public function getDelays() 271 | { 272 | if (version_compare(Config::version(), '8.9', '<')) { 273 | throw new RuntimeException('This feature needs at least vips 8.9'); 274 | } 275 | $vips = $this->image->getVips(); 276 | try { 277 | return $vips->get('delay'); 278 | } catch (\Exception $e) { 279 | return null; 280 | } 281 | } 282 | 283 | /** 284 | * Sets the delays for all the frames in an animated image. 285 | * 286 | * @param int[] $delays 287 | * 288 | * @throws \Jcupitt\Vips\Exception 289 | */ 290 | public function setDelays($delays) 291 | { 292 | if (version_compare(Config::version(), '8.9', '<')) { 293 | throw new RuntimeException('This feature needs at least vips 8.9'); 294 | } 295 | $vips = $this->image->vipsCopy(); 296 | $vips->set('delay', $delays); 297 | } 298 | 299 | /** 300 | * Gets delay in milliseconds for a single frame. 301 | * 302 | * @throws \Imagine\Exception\RuntimeException 303 | * 304 | * @return int|null Delay in miliseconds 305 | */ 306 | public function getDelay($index) 307 | { 308 | if (version_compare(Config::version(), '8.9', '<')) { 309 | throw new RuntimeException('This feature needs at least vips 8.9'); 310 | } 311 | $vips = $this->image->getVips(); 312 | try { 313 | $delays = $this->getDelays(); 314 | if (null === $delays) { 315 | return null; 316 | } 317 | if (isset($delays[$index])) { 318 | return $delays[$index]; 319 | } 320 | 321 | return null; 322 | } catch (\Exception $e) { 323 | return null; 324 | } 325 | } 326 | 327 | /** 328 | * Sets delay for a single frame. 329 | * 330 | * @param $index int Frame number 331 | * @param $delay int Delay in miliseconds 332 | * 333 | * @throws \Imagine\Exception\NotSupportedException 334 | * @throws \Imagine\Exception\RuntimeException 335 | * @throws \Jcupitt\Vips\Exception 336 | */ 337 | public function setDelay($index, $delay) 338 | { 339 | if (version_compare(Config::version(), '8.9', '<')) { 340 | throw new RuntimeException('This feature needs at least vips 8.9'); 341 | } 342 | $vips = $this->image->getVips(); 343 | $delays = $this->getDelays(); 344 | if (null === $delays) { 345 | $delays = array_fill(0, \count($this), self::DEFAULT_GIF_DELAY); 346 | $this->setDelays($delays); 347 | } 348 | $oldValue = null; 349 | if (isset($delays[$index])) { 350 | $oldValue = $delays[$index]; 351 | } 352 | if ($oldValue != $delay) { 353 | $delays[$index] = $delay; 354 | $this->setDelays($delays); 355 | } 356 | } 357 | 358 | /** 359 | * Tries to extract layer at given offset. 360 | * 361 | * @param int $offset 362 | * 363 | * @throws RuntimeException 364 | * 365 | * @return Image 366 | */ 367 | private function extractAt($offset) 368 | { 369 | if (0 === $offset) { 370 | return $this->image; 371 | } 372 | if (!isset($this->layers[$offset])) { 373 | try { 374 | $this->layers[$offset] = new Image($this->resources[$offset], $this->image->palette(), new MetadataBag()); 375 | // unset resource, not needed anymore, directly from the image object fetched from now on 376 | unset($this->resources[$offset]); 377 | } catch (Exception $e) { 378 | throw new RuntimeException(sprintf('Failed to extract layer %d', $offset), $e->getCode(), $e); 379 | } 380 | } 381 | 382 | return $this->layers[$offset]; 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /lib/resources/colorprofiles/AdobeRGB1998.icc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rokka-io/imagine-vips/55ad2a6301db20e1c1ccc4e04dd02d3b128f4c2b/lib/resources/colorprofiles/AdobeRGB1998.icc -------------------------------------------------------------------------------- /lib/resources/colorprofiles/cmyk.icm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rokka-io/imagine-vips/55ad2a6301db20e1c1ccc4e04dd02d3b128f4c2b/lib/resources/colorprofiles/cmyk.icm -------------------------------------------------------------------------------- /lib/resources/colorprofiles/gray.icc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rokka-io/imagine-vips/55ad2a6301db20e1c1ccc4e04dd02d3b128f4c2b/lib/resources/colorprofiles/gray.icc -------------------------------------------------------------------------------- /lib/resources/colorprofiles/sRGB.icm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rokka-io/imagine-vips/55ad2a6301db20e1c1ccc4e04dd02d3b128f4c2b/lib/resources/colorprofiles/sRGB.icm -------------------------------------------------------------------------------- /tests/BasicImageTest.php: -------------------------------------------------------------------------------- 1 | create(new Box(10, 10)); 14 | $this->assertInstanceOf(\Imagine\Vips\Image::class, $image); 15 | $this->assertEquals($image->getSize()->getWidth(), 10); 16 | } 17 | 18 | public function testResizeImage() 19 | { 20 | $imagine = new \Imagine\Vips\Imagine(); 21 | $image = $imagine->create(new Box(200, 100)); 22 | $image->resize(new Box(50, 50)); 23 | $this->assertEquals($image->getSize()->getWidth(), 50); 24 | $this->assertEquals($image->getSize()->getHeight(), 50); 25 | } 26 | 27 | public function testSaveImage() 28 | { 29 | $imagine = new \Imagine\Vips\Imagine(); 30 | $image = $imagine->create(new Box(200, 100)); 31 | $image->save('foo.jpg'); 32 | $this->assertFileExists('foo.jpg'); 33 | $loaded = $imagine->open('foo.jpg'); 34 | $this->assertEquals($loaded->getSize()->getWidth(), 200); 35 | $this->assertEquals($loaded->getSize()->getHeight(), 100); 36 | unlink('foo.jpg'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/EnabledTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(\Imagine\Vips\Imagine::hasVipsInstalled()); 13 | } 14 | 15 | public function testCorrectLibrary() 16 | { 17 | // if we have the FFI::class, we need the ffi extension 18 | if (class_exists(FFI::class)) { 19 | $this->assertTrue(\extension_loaded('ffi'), 'The needed ffi extension was not installed'); 20 | // and check we get a string 21 | $this->assertIsString(\Jcupitt\Vips\Config::version()); 22 | 23 | return; 24 | } 25 | $this->assertTrue(\extension_loaded('vips'), 'The needed vips extension was not installed'); 26 | // and check we get a string 27 | $this->assertIsString(\Jcupitt\Vips\Config::version()); 28 | } 29 | } 30 | --------------------------------------------------------------------------------