diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..b343d199c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# editorconfig.org + +root = true + +[*.php] +indent_size = 4 +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitattributes b/.gitattributes index 880a0889b..0825cfd8d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,16 @@ * text=auto -/tests export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/.travis.yml export-ignore -/phpunit.xml export-ignore -/README.md export-ignore +/.editorconfig export-ignore +/.github export-ignore +/.well-known export-ignore +/CONTRIBUTING.md export-ignore +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpcs.xml.dist export-ignore +/phpunit.dist.xml export-ignore +/phpstan.dist.neon export-ignore +/docker-compose.yml export-ignore +/Dockerfile export-ignore +/entrypoint.sh export-ignore +/AGENTS.md export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..5db98ebc1 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: [Intervention] +ko_fi: interventionphp +custom: https://paypal.me/interventionio diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..b9c672553 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' +--- + +## Describe the bug +A clear and concise description of what the bug is. + +## Code Example +Code example to reproduce the behavior. + +## Expected behavior +A clear and concise description of what you expected to happen. + +## Images +If applicable, add problematic images or screenshots to help explain your problem. + +## Environment (please complete the following information): + - PHP Version: + - OS: + - Intervention Image Version: + - GD, Imagick or libvips: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..cecb21da4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,13 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' +--- + +## Describe the feature you'd like +A clear and concise description of what you want to happen. + +## Is your feature request related to a problem? Please describe. +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] diff --git a/.github/images/support.svg b/.github/images/support.svg new file mode 100644 index 000000000..af31794e1 --- /dev/null +++ b/.github/images/support.svg @@ -0,0 +1,24 @@ + + + + + + + + + Support me on Ko-fi + + + + + + Support me on Ko-fi + + + + + + + + + diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 000000000..a327133f0 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,105 @@ +name: Tests + +on: [ push, pull_request ] + +jobs: + run: + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + php: [ '8.3', '8.4', '8.5' ] + imagemagick: [ '6.9.13-40', '7.1.2-15' ] + imagick: [ '3.8.1' ] + + name: PHP ${{ matrix.php }} - ImageMagick ${{ matrix.imagemagick }} + + steps: + - name: Checkout project + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, gd + coverage: none + + - name: Prepare environment for Imagemagick + run: | + sudo apt remove -y imagemagick imagemagick-6-common libmagic-dev + sudo apt update --allow-releaseinfo-change + sudo apt update + sudo apt install -y libjpeg-dev libgif-dev libtiff-dev libpng-dev libwebp-dev libavif-dev libheif-dev libraqm-dev libmagickwand-dev + + - name: Cache ImageMagick + uses: actions/cache@v5 + id: cache-imagemagick + with: + path: /home/runner/im/imagemagick-${{ matrix.imagemagick }} + key: ${{ runner.os }}-ImageMagick-${{ matrix.imagemagick }}-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-ImageMagick-${{ matrix.imagemagick }}- + + - name: Check ImageMagick cache exists + uses: andstor/file-existence-action@v3 + id: cache-imagemagick-exists + with: + files: /home/runner/im/imagemagick-${{ matrix.imagemagick }} + + - name: Install ImageMagick + if: ( steps.cache-imagemagick.outputs.cache-hit != 'true' || steps.cache-imagemagick-exists.outputs.files_exists != 'true' ) + run: | + curl -o /tmp/ImageMagick.tar.xz -sL https://imagemagick.org/archive/releases/ImageMagick-${{ matrix.imagemagick }}.tar.xz + ( + cd /tmp || exit 1 + tar xf ImageMagick.tar.xz + cd ImageMagick-${{ matrix.imagemagick }} + sudo ./configure --prefix=/home/runner/im/imagemagick-${{ matrix.imagemagick }} + sudo make -j$(nproc) + sudo make install + ) + + - name: Install Imagick PHP extension + run: | + git clone --depth=1 --single-branch --branch master https://github.com/Imagick/imagick /tmp/imagick + ( + cd /tmp/imagick || exit 1 + phpize + sudo ./configure --with-imagick=/home/runner/im/imagemagick-${{ matrix.imagemagick }} + sudo make -j$(nproc) + sudo make install + ) + sudo bash -c 'echo "extension=imagick.so" >> /etc/php/${{ matrix.php }}/cli/php.ini' + php --ri imagick; + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache composer dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-php-${{ matrix.php }}- + + - name: Install dependencies + run: composer update --prefer-stable --prefer-dist --no-interaction + + - name: GD Version + run: php -r 'var_dump(gd_info());' + + - name: Imagick Version + run: php -r 'var_dump(Imagick::getVersion());' + + - name: Supported Imagick Formats + run: php -r 'var_dump(Imagick::queryFormats());' + + - name: Execute tests + run: vendor/bin/phpunit --no-coverage + + - name: Run analyzer + run: vendor/bin/phpstan + + - name: Validate coding standards + run: vendor/bin/phpcs diff --git a/.gitignore b/.gitignore index 830f4d6f4..596da9c50 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -.DS_Store -composer.lock +build/ vendor/ -dev/ \ No newline at end of file +/.phpunit.cache +composer.lock +phpunit.xml +phpcs.xml +phpstan.neon diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8dcadc253..000000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: php - -php: - - 5.4 - - 5.5 - - 5.6 - - 7.0 - - 7.1 - - nightly - - hhvm - -matrix: - allow_failures: - - php: nightly - - php: hhvm - -before_install: - - sudo add-apt-repository -y ppa:moti-p/cc - - sudo apt-get update - - sudo apt-get -y --reinstall install imagemagick - - yes | pecl install imagick-beta - - if [[ ${TRAVIS_PHP_VERSION:0:3} == "5.4" ]]; then echo "extension = imagick.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi - -before_script: - - composer self-update - - composer install --prefer-source --no-interaction --dev - -script: vendor/bin/phpunit diff --git a/.well-known/funding-manifest-urls b/.well-known/funding-manifest-urls new file mode 100644 index 000000000..807d47a84 --- /dev/null +++ b/.well-known/funding-manifest-urls @@ -0,0 +1,2 @@ +https://intervention.io/funding.json +https://image.intervention.io/funding.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..33cbcf79c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,122 @@ +# Intervention Image Agent Guide + +This document provides a guide for software engineering agents working on the Intervention Image codebase. + +## 1. Project Overview + +Intervention Image is a PHP image manipulation library. It provides an expressive, fluent interface to create, edit, and compose images. The library supports both the GD library and Imagick as underlying drivers. + +The source code is located in the `src` directory, and the project follows the PSR-4 autoloading standard. + +## 2. Development Environment + +The project uses Composer to manage dependencies. These dependencies are installed automatically when using the Docker development environment. + +## 3. Build, Lint, and Test Commands + +The following commands are used to ensure code quality and correctness. + +### 3.1. Testing (PHPUnit) + +The project uses PHPUnit for unit and feature testing. + +- **Run all tests:** + ```bash + docker compose run --rm tests + ``` + +- **Run a single test file:** + To run a specific test file, provide the path to the file. + ```bash + docker compose run --rm tests tests/Unit/ImageManagerTest.php + ``` + +- **Run a single test method:** + Use the `--filter` option to run a specific test method by its name. + ```bash + docker compose run --rm tests tests/Unit/ImageManagerTest.php --filter testMethodName + ``` + +- **Check test coverage:** + ```bash + docker compose run --rm coverage + ``` + +### 3.2. Static Analysis (PHPStan) + +PHPStan is used for static analysis to find potential bugs. + +- **Run static analysis:** + ```bash + docker compose run --rm analysis + ``` + +### 3.3. Coding Standards (PHP CodeSniffer) + +The project adheres to the PSR-12 coding standard with additional rules. PHP CodeSniffer is used to enforce these standards. + +- **Check for coding standard violations:** + ```bash + docker compose run --rm standards + ``` + +## 4. Code Style and Conventions + +Consistency is key. Adhere to the following guidelines when writing code. + +### 4.1. Formatting + +- **PSR-12:** The primary coding standard is PSR-12. +- **Indentation:** Use 4 spaces for indentation, not tabs. +- **Line Endings:** Use Unix-style line endings (LF). +- **Strict Types:** All PHP files must start with `declare(strict_types=1);`. +- **Class Structure:** Follow the ordering defined in `phpcs.xml.dist`: + 1. `uses` + 2. `enum cases` + 3. `constants` + 4. `static properties` + 5. `properties` + 6. `constructor` + 7. `static constructors` + 8. `methods` + 9. `magic methods` + +### 4.2. Naming Conventions + +- **Classes:** `PascalCase`. +- **Methods:** `camelCase`. +- **Variables:** `camelCase`. +- **Constants:** `UPPER_CASE` with underscore separators. +- **File Names:** File names must match the class name they contain (e.g., `MyClass.php` for `class MyClass`). + +### 4.3. Imports + +- **One class per `use` statement:** Do not group multiple classes in a single `use` statement. +- **No leading backslash:** `use` statements must not start with a backslash. +- **Order:** `use` statements should be ordered alphabetically. Unused imports must be removed. + +### 4.4. Types and Type Hinting + +- **Strict Typing:** All code should be strictly typed. +- **Parameter Types:** All method parameters must have a type hint. +- **Return Types:** All methods must have a return type hint. +- **Property Types:** All class properties must have a type hint. +- **Nullable Types:** Use nullable types (`?TypeName`) when a `null` value is explicitly allowed. + +### 4.5. Error Handling + +- Exceptions should be used for error handling. +- When catching exceptions, be as specific as possible. Avoid catching generic `\Exception` or `\Throwable`. +- Exception messages should be clear and descriptive. + +### 4.6. PHPDoc (DocBlocks) + +- PHPDoc blocks are required for all classes, properties, and methods. +- Follow the annotation order defined in `phpcs.xml.dist`. +- Use DocBlocks to provide context and explain complex logic. Do not restate the obvious from the code signature. + +## 5. Branching and Commits + +- **Branching:** Create new branches from the `develop` branch. Name branches descriptively (e.g., `feature/new-filter`, `bugfix/fix-resize-issue`). +- **Commits:** Write clear and concise commit messages. The first line should be a short summary (max 50 chars). A more detailed explanation can follow after a blank line. Always write the message in imperative and do NOT use any useless prefixes like "chore" or other. +- **Pull Requests:** Target the `develop` branch for all pull requests. Ensure all checks (tests, linting, analysis) are passing before submitting. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..5198342a7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +# Contributing to Intervention Image + +Thank you for your interest in contributing to the project. + +You can read the full contribution guide on https://intervention.io/contributing/. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..b295d42bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +FROM php:8.3-cli + +ARG IMAGEMAGICK_VERSION=7.1.2-15 + +# install dependencies for building ImageMagick and PHP extensions +RUN apt update \ + && apt install -y \ + libjpeg-dev \ + libgif-dev \ + libtiff-dev \ + libpng-dev \ + libwebp-dev \ + libavif-dev \ + libheif-dev \ + libraqm-dev \ + libopenjp2-7-dev \ + liblcms2-dev \ + git \ + zip \ + curl \ + xz-utils \ + && apt-get clean + +# build and install ImageMagick from source +RUN curl -o /tmp/ImageMagick.tar.xz -sL \ + "https://imagemagick.org/archive/releases/ImageMagick-${IMAGEMAGICK_VERSION}.tar.xz" \ + && cd /tmp \ + && tar xf ImageMagick.tar.xz \ + && cd "ImageMagick-${IMAGEMAGICK_VERSION}" \ + && ./configure \ + && make -j$(nproc) \ + && make install \ + && ldconfig \ + && cd / \ + && rm -rf /tmp/ImageMagick* + +# install PHP extensions +RUN pecl install imagick \ + && pecl install xdebug \ + && docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp --with-avif \ + && docker-php-ext-enable \ + imagick \ + xdebug \ + && docker-php-ext-install \ + gd \ + exif + +# install composer +COPY --from=composer /usr/bin/composer /usr/bin/composer + +# setup entrypoint +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/LICENSE b/LICENSE index bc444ba22..b8f19ffc8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,21 @@ The MIT License (MIT) -Copyright (c) 2014 Oliver Vogel +Copyright (c) 2013-present Oliver Vogel -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100755 index 40f96dc5d..000000000 --- a/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Intervention Image - -Intervention Image is a **PHP image handling and manipulation** library providing an easier and expressive way to create, edit, and compose images. The package includes ServiceProviders and Facades for easy **Laravel** integration. - -[![Build Status](https://travis-ci.org/Intervention/image.png?branch=master)](https://travis-ci.org/Intervention/image) - -## Requirements - -- PHP >=5.4 -- Fileinfo Extension - -## Supported Image Libraries - -- GD Library (>=2.0) -- Imagick PHP extension (>=6.5.7) - -## Getting started - -- [Installation](http://image.intervention.io/getting_started/installation) -- [Laravel Framework Integration](http://image.intervention.io/getting_started/installation#laravel) -- [Official Documentation](http://image.intervention.io/) - -## Code Examples - -```php -// open an image file -$img = Image::make('public/foo.jpg'); - -// resize image instance -$img->resize(320, 240); - -// insert a watermark -$img->insert('public/watermark.png'); - -// save image in desired format -$img->save('public/bar.jpg'); -``` - -Refer to the [documentation](http://image.intervention.io/) to learn more about Intervention Image. - -## Contributing - -Contributions to the Intervention Image library are welcome. Please note the following guidelines before submiting your pull request. - -- Follow [PSR-2](http://www.php-fig.org/psr/psr-2/) coding standards. -- Write tests for new functions and added features -- API calls should work consistently with both GD and Imagick drivers - -## License - -Intervention Image is licensed under the [MIT License](http://opensource.org/licenses/MIT). - -Copyright 2014 [Oliver Vogel](http://olivervogel.net/) diff --git a/composer.json b/composer.json index 3817a4b52..0a9493db1 100644 --- a/composer.json +++ b/composer.json @@ -1,39 +1,51 @@ { "name": "intervention/image", - "description": "Image handling and manipulation library with support for Laravel integration", - "homepage": "http://image.intervention.io/", - "keywords": ["image", "gd", "imagick", "laravel", "watermark", "thumbnail"], + "description": "PHP Image Processing", + "homepage": "https://image.intervention.io", + "keywords": [ + "image", + "gd", + "imagick", + "watermark", + "thumbnail", + "resize" + ], "license": "MIT", "authors": [ { "name": "Oliver Vogel", - "email": "oliver@olivervogel.net", - "homepage": "http://olivervogel.net/" + "email": "oliver@intervention.io", + "homepage": "https://intervention.io" } ], "require": { - "php": ">=5.4.0", - "ext-fileinfo": "*", - "guzzlehttp/psr7": "~1.1" + "php": "^8.3", + "ext-mbstring": "*", + "intervention/gif": "^5" }, "require-dev": { - "phpunit/phpunit": "3.*", - "mockery/mockery": "~0.9.2" + "phpunit/phpunit": "^12.0", + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^2.1", + "squizlabs/php_codesniffer": "^4", + "slevomat/coding-standard": "~8.0" }, "suggest": { - "ext-gd": "to use GD library based image processing.", - "ext-imagick": "to use Imagick based image processing.", - "intervention/imagecache": "Caching extension for the Intervention Image library" + "ext-exif": "Recommended to be able to read EXIF data properly." }, "autoload": { "psr-4": { - "Intervention\\Image\\": "src/Intervention/Image" + "Intervention\\Image\\": "src" } }, - "extra": { - "branch-alias": { - "dev-master": "2.3-dev" + "autoload-dev": { + "psr-4": { + "Intervention\\Image\\Tests\\": "tests" } }, - "minimum-stability": "stable" + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..c2bede8f4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +services: + tests: + build: ./ + working_dir: /project + entrypoint: ["/usr/local/bin/entrypoint.sh", "./vendor/bin/phpunit"] + volumes: + - ./:/project + coverage: + build: ./ + working_dir: /project + entrypoint: ["/usr/local/bin/entrypoint.sh", "./vendor/bin/phpunit", "--coverage-text"] + volumes: + - ./:/project + environment: + - XDEBUG_MODE=coverage + analysis: + build: ./ + working_dir: /project + entrypoint: ["/usr/local/bin/entrypoint.sh", "./vendor/bin/phpstan", "analyze", "--memory-limit=512M"] + volumes: + - ./:/project + standards: + build: ./ + working_dir: /project + entrypoint: ["/usr/local/bin/entrypoint.sh", "./vendor/bin/phpcs"] + volumes: + - ./:/project diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 000000000..3d3ba408b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e + +composer install --quiet + +exec "$@" diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 000000000..dd9979573 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,128 @@ + + + src/ + tests/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 000000000..dbeb85f30 --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,7 @@ +parameters: + level: 6 + paths: + - src + exceptions: + check: + missingCheckedExceptionInThrows: true diff --git a/phpunit.dist.xml b/phpunit.dist.xml new file mode 100644 index 000000000..616abbfe3 --- /dev/null +++ b/phpunit.dist.xml @@ -0,0 +1,30 @@ + + + + + ./tests/Unit + + + ./tests/Feature + + + + + src + + + diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index 3347b75b7..000000000 --- a/phpunit.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - ./tests/ - - - diff --git a/provides.json b/provides.json deleted file mode 100644 index a8cd1b6a5..000000000 --- a/provides.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "providers": [ - "Intervention\\Image\\ImageServiceProvider" - ], - "aliases": [ - { - "alias": "Image", - "facade": "Intervention\\Image\\Facades\\Image" - } - ] -} diff --git a/readme.md b/readme.md new file mode 100644 index 000000000..605947a9d --- /dev/null +++ b/readme.md @@ -0,0 +1,90 @@ +# Intervention Image +## PHP Image Processing + +[![Latest Version](https://img.shields.io/packagist/v/intervention/image.svg)](https://packagist.org/packages/intervention/image) +[![Build Status](https://github.com/Intervention/image/actions/workflows/run-tests.yml/badge.svg)](https://github.com/Intervention/image/actions) +[![Monthly Downloads](https://img.shields.io/packagist/dm/intervention/image.svg)](https://packagist.org/packages/intervention/image/stats) +[![Support me on Ko-fi](https://raw.githubusercontent.com/Intervention/image/develop/.github/images/support.svg)](https://ko-fi.com/interventionphp) + +Intervention Image is a **PHP image processing library** that provides a simple +and expressive way to create, edit, and compose images. It comes with a universal +interface for the popular PHP image manipulation extensions. You can +choose between the GD library, Imagick or libvips as the base layer for all operations. + +- Fluent interface for common image editing tasks +- Interchangeable driver architecture with support for **GD, Imagick and libvips** +- Support for animated images with all drivers +- Framework-agnostic + +## Installation + +Install this library using [Composer](https://getcomposer.org). Simply request the package with the following command: + +```bash +composer require intervention/image +``` + +## Getting Started + +Learn the [basics](https://image.intervention.io/v4/basics/instantiation/) on +how to use Intervention Image and more with the [official documentation](https://image.intervention.io/v4/). + +## Code Examples + +```php +use Intervention\Image\ImageManager; +use Intervention\Image\Drivers\Gd\Driver as GdDriver; +use Intervention\Image\Alignment; +use Intervention\Image\Color; +use Intervention\Image\Format; + +// create image manager instance using the preferred driver +$manager = ImageManager::usingDriver(GdDriver::class); + +// read image data from path +$image = $manager->decodePath('images/example.webp'); + +// scale image by height +$image->scale(height: 300); + +// insert a watermark +$image->insert('images/watermark.png', alignment: Alignment::BOTTOM_RIGHT); + +// encode edited image +$encoded = $image->encodeUsingFormat(Format::JPEG, quality: 65); + +// save encoded image +$encoded->save('images/example.jpg'); +``` + +## Requirements + +Before you begin with the installation make sure that your server environment +supports the following requirements. + +- PHP >= 8.3 +- Mbstring PHP Extension +- Image Processing PHP Extension (GD, Imagick or libvips) + +## Supported Image Libraries + +Depending on your environment Intervention Image lets you choose between +different image processing extensions. + +- GD Library +- Imagick PHP extension +- [libvips](https://github.com/Intervention/image-driver-vips) + +## Security + +If you discover any security related issues, please email oliver@intervention.io directly. + +## Authors + +This library is developed and maintained by [Oliver Vogel](https://intervention.io) + +Thanks to the community of [contributors](https://github.com/Intervention/image/graphs/contributors) who have helped to improve this project. + +## License + +Intervention Image is licensed under the [MIT License](LICENSE). diff --git a/src/Alignment.php b/src/Alignment.php new file mode 100644 index 000000000..0ad398cc1 --- /dev/null +++ b/src/Alignment.php @@ -0,0 +1,238 @@ + self::TOP, + + 'top_right', + 'topright', + 'right-top', + 'right_top', + 'righttop' => self::TOP_RIGHT, + + 'right-center', + 'right_center', + 'rightcenter', + 'center-right', + 'center_right', + 'centerright', + 'right-middle', + 'right_middle', + 'rightmiddle', + 'middle-right', + 'middle_right', + 'middleright' => self::RIGHT, + + 'bottom_right', + 'bottomright', + 'right-bottom', + 'right_bottom', + 'rightbottom' => self::BOTTOM_RIGHT, + + 'bottom-center', + 'bottom_center', + 'bottomcenter', + 'center-bottom', + 'center_bottom', + 'centerbottom', + 'bottom-middle', + 'bottom_middle', + 'bottommiddle', + 'middle-bottom', + 'middle_bottom', + 'middlebottom' => self::BOTTOM, + + 'bottom_left', + 'bottomleft', + 'left-bottom', + 'left_bottom', + 'leftbottom' => self::BOTTOM_LEFT, + + 'left-center', + 'left_center', + 'leftcenter', + 'center-left', + 'center_left', + 'centerleft', + 'left-middle', + 'left_middle', + 'leftmiddle', + 'middle-left', + 'middle_left', + 'middleleft' => self::LEFT, + + 'top_left', + 'topleft', + 'left-top', + 'left_top', + 'lefttop' => self::TOP_LEFT, + + 'middle', + 'center-center', + 'center_center', + 'centercenter', + 'center-middle', + 'center_middle', + 'centermiddle', + 'middle-center', + 'middle_center', + 'middlecenter' => self::CENTER, + + default => throw new InvalidArgumentException( + 'Unable to create ' . self::class . ' from "' . $identifier . '"', + ), + }; + } + + return $position; + } + + /** + * Try to create position from given identifier or return null on failure. + */ + public static function tryCreate(string|self $identifier): ?self + { + try { + return self::create($identifier); + } catch (InvalidArgumentException) { + return null; + } + } + + /** + * Change the current alignment by adjusting only the horizontal axis to the specified value. + */ + public function alignHorizontally(string|self $alignment): self + { + // handle "leftish" alignments + if (in_array($alignment, [self::LEFT, self::BOTTOM_LEFT, self::TOP_LEFT])) { + return match ($this) { + self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP_LEFT, + self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM_LEFT, + self::CENTER, self::LEFT, self::RIGHT => self::LEFT, + }; + } + + // handle "rightish" alignments + if (in_array($alignment, [self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT])) { + return match ($this) { + self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP_RIGHT, + self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM_RIGHT, + self::CENTER, self::LEFT, self::RIGHT => self::RIGHT, + }; + } + + // handle centering + if (in_array($alignment, [self::CENTER, self::TOP, self::BOTTOM])) { + return match ($this) { + self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP, + self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM, + self::CENTER, self::LEFT, self::RIGHT => self::CENTER, + }; + } + + return $this; + } + + /** + * Change the current alignment by adjusting only the vertical axis to the specified value. + */ + public function alignVertically(string|self $alignment): self + { + // handle "bottomish" alignments + if (in_array($alignment, [self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT])) { + return match ($this) { + self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::BOTTOM_LEFT, + self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::BOTTOM_RIGHT, + self::CENTER, self::TOP, self::BOTTOM => self::BOTTOM, + }; + } + + // handle "topish" alignments + if (in_array($alignment, [self::TOP, self::TOP_RIGHT, self::TOP_LEFT])) { + return match ($this) { + self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::TOP_LEFT, + self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::TOP_RIGHT, + self::CENTER, self::TOP, self::BOTTOM => self::TOP, + }; + } + + // handle centering + if (in_array($alignment, [self::CENTER, self::RIGHT, self::LEFT])) { + return match ($this) { + self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::LEFT, + self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::RIGHT, + self::CENTER, self::TOP, self::BOTTOM => self::CENTER, + }; + } + + return $this; + } + + /** + * Return only the horizontal alignment. + */ + public function horizontal(): self + { + return match ($this) { + self::TOP, self::CENTER, self::BOTTOM => self::CENTER, + self::RIGHT, self::TOP_RIGHT, self::BOTTOM_RIGHT => self::RIGHT, + self::LEFT, self::TOP_LEFT, self::BOTTOM_LEFT => self::LEFT, + }; + } + + /** + * Return only the vertical alignment. + */ + public function vertical(): self + { + return match ($this) { + self::CENTER, self::RIGHT, self::LEFT => self::CENTER, + self::TOP, self::TOP_RIGHT, self::TOP_LEFT => self::TOP, + self::BOTTOM, self::BOTTOM_RIGHT, self::BOTTOM_LEFT => self::BOTTOM, + }; + } +} diff --git a/src/Analyzers/ColorspaceAnalyzer.php b/src/Analyzers/ColorspaceAnalyzer.php new file mode 100644 index 000000000..e3cd1c1c9 --- /dev/null +++ b/src/Analyzers/ColorspaceAnalyzer.php @@ -0,0 +1,12 @@ + + */ + protected array $sources = []; + + /** + * Frame delays of animation frames in seconds. + * + * @var array + */ + protected array $delays = []; + + /** + * Frame processing call names. + * + * @var array + */ + protected array $processingCalls = []; + + /** + * Frame processing arguments of calls. + * + * @var array> + */ + protected array $processingArguments = []; + + /** + * Create new instance. + */ + public function __construct( + protected int $width, + protected int $height, + null|callable $animation = null, + ) { + if (is_callable($animation)) { + $animation($this); + } + } + + /** + * {@inheritdoc} + * + * @see AnimationFactoryInterface::build() + */ + public static function build( + int $width, + int $height, + callable $animation, + DriverInterface $driver, + ): ImageInterface { + return (new self($width, $height, $animation))->image($driver); + } + + /** + * {@inheritdoc} + * + * @see AnimationFactoryInterface::add() + */ + public function add(mixed $source, float $delay = 1): AnimationFactoryInterface + { + $this->currentFrameNumber++; + + $this->sources[$this->currentFrameNumber] = $source; + $this->delays[$this->currentFrameNumber] = $delay; + $this->processingCalls[$this->currentFrameNumber] = null; + $this->processingArguments[$this->currentFrameNumber] = null; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see AnimationFactoryInterface::image() + */ + public function image(DriverInterface $driver): ImageInterface + { + if (count($this->sources) === 0) { + return $driver->createImage($this->width, $this->height); + } + + $frames = array_map( + $this->buildFrame(...), + array_fill(0, count($this->sources), $driver), + $this->sources, + $this->delays, + $this->processingCalls, + $this->processingArguments, + ); + + return new Image($driver, $driver->createCore($frames)); + } + + /** + * Build frame from given image source and delay. + * + * @param null|array $processingArguments + */ + private function buildFrame( + DriverInterface $driver, + mixed $source, + float $delay, + ?string $processingCall = null, + ?array $processingArguments = null, + ): FrameInterface { + try { + // try to decode image source + $image = $driver->decodeImage($source); + } catch (DecoderException | FilesystemException) { + // create empty image with colored background + $image = $driver->createImage($this->width, $this->height) + ->fill($driver->decodeColor($source)); + } + + // adjust size if necessary + if ($image->width() !== $this->width || $image->height() !== $this->height) { + $image->cover($this->width, $this->height); + } + + // apply processing call if available + if ($processingCall !== null) { + call_user_func_array([$image, $processingCall], $processingArguments); + } + + // return ready-made frame with all attributes + return $image + ->core() + ->first() + ->setDelay($delay) + ->setDisposalMethod(DisposalMethod::BACKGROUND->value); + } + + /** + * Collect processing calls on frame images. + * + * @param array> $arguments + * @throws Error + */ + public function __call(string $name, array $arguments): self + { + if (!method_exists(Image::class, $name)) { + throw new Error('Call to undefined method ' . Image::class . '::' . $name . '()'); + } + + $this->processingCalls[$this->currentFrameNumber] = $name; + $this->processingArguments[$this->currentFrameNumber] = $arguments; + + return $this; + } +} diff --git a/src/Collection.php b/src/Collection.php new file mode 100644 index 000000000..e5faa11a3 --- /dev/null +++ b/src/Collection.php @@ -0,0 +1,234 @@ + + */ +class Collection implements CollectionInterface, IteratorAggregate, Countable +{ + /** + * Create new collection object. + * + * @param array $items + */ + public function __construct(protected array $items = []) + { + // + } + + /** + * Static constructor. + * + * @param array $items + * @return self + */ + public static function create(array $items = []): self + { + return new self($items); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::has() + */ + public function has(int|string $key): bool + { + return array_key_exists($key, $this->items); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::set() + */ + public function set(int|string $key, mixed $item): self + { + $this->items[$key] = $item; + + return $this; + } + + /** + * Returns Iterator. + * + * @return Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->items); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::toArray() + */ + public function toArray(): array + { + return $this->items; + } + + /** + * Count items in collection. + * + * @return int<0, max> + */ + public function count(): int + { + return count($this->items); + } + + /** + * Append new item to collection. + * + * @return CollectionInterface + */ + public function push(mixed $item): CollectionInterface + { + $this->items[] = $item; + + return $this; + } + + /** + * Return first item in collection. + */ + public function first(): mixed + { + if (count($this->items) === 0) { + return null; + } + + return reset($this->items); + } + + /** + * Returns last item in collection. + */ + public function last(): mixed + { + if (count($this->items) === 0) { + return null; + } + + return end($this->items); + } + + /** + * Return item at given position starting at 0. + */ + public function at(int $key = 0, mixed $default = null): mixed + { + if ($this->count() === 0) { + return $default; + } + + $positions = array_values($this->items); + if (!array_key_exists($key, $positions)) { + return $default; + } + + return $positions[$key]; + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::get() + */ + public function get(int|string $query, mixed $default = null): mixed + { + if ($this->count() === 0) { + return $default; + } + + if (is_int($query) && array_key_exists($query, $this->items)) { + return $this->items[$query]; + } + + if (is_string($query) && !str_contains($query, '.')) { + return array_key_exists($query, $this->items) ? $this->items[$query] : $default; + } + + $query = explode('.', (string) $query); + + $result = $default; + $items = $this->items; + foreach ($query as $key) { + if (!is_array($items) || !array_key_exists($key, $items)) { + $result = $default; + break; + } + + $result = $items[$key]; + $items = $result; + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::map() + */ + public function map(callable $callback): self + { + + return new self( + array_map( + fn(mixed $item) => $callback($item), + $this->items, + ) + ); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::map() + */ + public function filter(callable $callback): self + { + return new self( + array_filter( + $this->items, + fn(mixed $item) => $callback($item), + ) + ); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::clear() + */ + public function clear(): CollectionInterface + { + $this->items = []; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::slice() + */ + public function slice(int $offset, ?int $length = null): CollectionInterface + { + $this->items = array_slice($this->items, $offset, $length); + + return $this; + } +} diff --git a/src/Color.php b/src/Color.php new file mode 100644 index 000000000..632144217 --- /dev/null +++ b/src/Color.php @@ -0,0 +1,173 @@ +handle($input); + } catch (NotSupportedException | DriverException $e) { + throw new InvalidArgumentException( + 'Unable to parse RGB color from input "' . $input . '"', + previous: $e, + ); + } + + if (!$color instanceof ColorInterface) { + throw new ColorException('Result must be instance of ' . self::class . ', got ' . $color::class); + } + + return $color; + } + + /** + * Create new RGB color. + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public static function rgb(int|Red $r, int|Green $g, int|Blue $b, float|RgbAlpha $a = 1): RgbColor + { + return new RgbColor($r, $g, $b, $a); + } + + /** + * Create new CMYK color. + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public static function cmyk( + int|Cyan $c, + int|Magenta $m, + int|Yellow $y, + int|Key $k, + float|CmykAlpha $a = 1, + ): CmykColor { + return new CmykColor($c, $m, $y, $k, $a); + } + + /** + * Create new HSL color. + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public static function hsl(int|HslHue $h, int|HslSaturation $s, int|Luminance $l, float|HslAlpha $a = 1): HslColor + { + return new HslColor($h, $s, $l, $a); + } + + /** + * Create new HSV color. + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public static function hsv(int|Hue $h, int|Saturation $s, int|Value $v, float|HsvAlpha $a = 1): HsvColor + { + return new HsvColor($h, $s, $v, $a); + } + + /** + * Create new OKLAB color. + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public static function oklab( + float|OklabLightness $l, + float|A $a, + float|B $b, + float|OklabAlpha $alpha = 1, + ): OklabColor { + return new OklabColor($l, $a, $b, $alpha); + } + + /** + * Create new OKLCH color. + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public static function oklch( + float|OklchLightness $l, + float|Chroma $c, + float|Hue $h, + float|OklchAlpha $a = 1, + ): OklchColor { + return new OklchColor($l, $c, $h, $a); + } + + /** + * Create transparent RGB color. + */ + public static function transparent(): ColorInterface + { + // @phpstan-ignore missingType.checkedException + return new RgbColor(255, 255, 255, 0); + } +} diff --git a/src/Colors/AbstractColor.php b/src/Colors/AbstractColor.php new file mode 100644 index 000000000..409ca82cd --- /dev/null +++ b/src/Colors/AbstractColor.php @@ -0,0 +1,215 @@ + + */ + protected array $channels; + + /** + * {@inheritdoc} + * + * @see ColorInterface::channels() + */ + public function channels(): array + { + return $this->channels; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::channel() + * + * @throws InvalidArgumentException + */ + public function channel(string $classname): ColorChannelInterface + { + $channels = array_filter( + $this->channels(), + fn(ColorChannelInterface $channel): bool => $channel::class === $classname, + ); + + if (count($channels) === 0) { + throw new InvalidArgumentException('Color channel ' . $classname . ' could not be found'); + } + + return reset($channels); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toColorspace() + * + * @throws InvalidArgumentException + */ + public function toColorspace(string|ColorspaceInterface $colorspace): ColorInterface + { + if (is_string($colorspace) && !class_exists($colorspace)) { + throw new InvalidArgumentException('Unknown color space (' . $colorspace . ') as conversion target'); + } + + $colorspace = is_string($colorspace) ? new $colorspace() : $colorspace; + + if (!$colorspace instanceof ColorspaceInterface) { + throw new InvalidArgumentException('Given color space must implement ' . ColorspaceInterface::class); + } + + return $colorspace->importColor($this); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::isTransparent() + */ + public function isTransparent(): bool + { + return $this->alpha()->value() < $this->alpha()->max(); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::isClear() + */ + public function isClear(): bool + { + return floatval($this->alpha()->value()) === 0.0; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::withTransparency() + * + * @throws InvalidArgumentException + */ + public function withTransparency(float $transparency): ColorInterface + { + $color = clone $this; + + $color->channels = array_map( + fn(ColorChannelInterface $channel): ColorChannelInterface => + $channel instanceof AlphaChannel ? $channel::fromNormalized($transparency) : $channel, + $this->channels + ); + + return $color; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::withBrightness() + * + * @throws InvalidArgumentException + */ + public function withBrightness(int $level): ColorInterface + { + $hsl = clone $this->toColorspace(HslColorspace::class); + $hsl->channel(Luminance::class)->scale($level); + + return $hsl->toColorspace($this->colorspace()); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::withSaturation() + * + * @throws InvalidArgumentException + */ + public function withSaturation(int $level): ColorInterface + { + $hsl = clone $this->toColorspace(HslColorspace::class); + $hsl->channel(Saturation::class)->scale($level); + + return $hsl->toColorspace($this->colorspace()); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::withInversion() + * + * @throws ColorException + */ + public function withInversion(): ColorInterface + { + try { + $rgb = $this->toColorspace(RgbColorspace::class); + } catch (InvalidArgumentException) { + throw new ColorException('Failed to invert color'); + } + + try { + $inverted = new \Intervention\Image\Colors\Rgb\Color( + 255 - $rgb->channel(Red::class)->value(), + 255 - $rgb->channel(Green::class)->value(), + 255 - $rgb->channel(Blue::class)->value(), + $rgb->alpha()->normalized(), + ); + return $inverted->toColorspace($this->colorspace()); + } catch (InvalidArgumentException) { + throw new ColorException('Failed to invert color'); + } + } + + /** + * Show debug info for the current color. + * + * @return array + */ + public function __debugInfo(): array + { + return array_reduce($this->channels(), function (array $result, ColorChannelInterface $item) { + $key = strtolower((new ReflectionClass($item))->getShortName()); + $result[$key] = $item->toString(); + return $result; + }, []); + } + + /** + * Clone color. + */ + public function __clone(): void + { + foreach ($this->channels as $key => $channel) { + $this->channels[$key] = clone $channel; + } + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::__toString() + */ + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/src/Colors/AbstractColorChannel.php b/src/Colors/AbstractColorChannel.php new file mode 100644 index 000000000..83a5de24d --- /dev/null +++ b/src/Colors/AbstractColorChannel.php @@ -0,0 +1,89 @@ +value() - $this->min()) / ($this->max() - $this->min()), $precision); + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::scale() + * + * @throws InvalidArgumentException + */ + public function scale(int $percent): self + { + if ($percent === 0) { + return $this; + } + + if ($percent < -100 || $percent > 100) { + throw new InvalidArgumentException('Percentage value must be between -100 and 100'); + } + + $normalized = $this->normalized(); + $base = $percent >= 0 ? (1 - $normalized) : $normalized; + $scaled = min(1.0, max(0.0, $normalized + $base / 100 * $percent)); + $this->value = static::fromNormalized($scaled)->value(); + + return $this; + } + + /** + * Throw exception if the given value is not applicable for channel + * otherwise the value is returned unchanged. + * + * @throws InvalidArgumentException + */ + protected function validValueOrFail(int|float $value): mixed + { + if ($value < $this->min() || $value > $this->max()) { + throw new InvalidArgumentException( + 'Color channel ' . $this::class . ' value must be in range ' . $this->min() . ' to ' . $this->max(), + ); + } + + return $value; + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::toString() + */ + public function toString(): string + { + return (string) $this->value(); + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::__toString() + */ + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/src/Colors/AbstractColorspace.php b/src/Colors/AbstractColorspace.php new file mode 100644 index 000000000..d939d5161 --- /dev/null +++ b/src/Colors/AbstractColorspace.php @@ -0,0 +1,27 @@ + + */ + protected static array $channels = []; + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::channels() + */ + public static function channels(): array + { + return static::$channels; + } +} diff --git a/src/Colors/AlphaChannel.php b/src/Colors/AlphaChannel.php new file mode 100644 index 000000000..601a07951 --- /dev/null +++ b/src/Colors/AlphaChannel.php @@ -0,0 +1,76 @@ + 1) { + throw new InvalidArgumentException( + 'Color channel value of ' . static::class . ' must be in range 0 to 1', + ); + } + + $this->value = (int) $this->validValueOrFail(intval(round($value * $this->max()))); + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::fromNormalized() + * + * @throws InvalidArgumentException + */ + public static function fromNormalized(float $normalized): self + { + return new static($normalized); + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::value() + */ + public function value(): int + { + return $this->value; + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::min() + */ + public static function min(): float + { + return 0; + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::max() + */ + public static function max(): float + { + return 255; + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::toString() + */ + public function toString(): string + { + return strval($this->normalized(2)); + } +} diff --git a/src/Colors/Cmyk/Channels/Alpha.php b/src/Colors/Cmyk/Channels/Alpha.php new file mode 100644 index 000000000..95a86a076 --- /dev/null +++ b/src/Colors/Cmyk/Channels/Alpha.php @@ -0,0 +1,12 @@ +channels = [ + is_int($c) ? new Cyan($c) : $c, + is_int($m) ? new Magenta($m) : $m, + is_int($y) ? new Yellow($y) : $y, + is_int($k) ? new Key($k) : $k, + is_float($a) ? new Alpha($a) : $a, + ]; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::create() + * + * @throws InvalidArgumentException + */ + public static function create(int|Cyan $c, int|Magenta $m, int|Yellow $y, int|Key $k, float|Alpha $a = 1): self + { + return new self($c, $m, $y, $k, $a); + } + + /** + * Parse CMYK color from string. + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public static function parse(string $input): self + { + try { + $color = InputHandler::usingDecoders([ + StringColorDecoder::class, + ])->handle($input); + } catch (NotSupportedException | DriverException $e) { + throw new InvalidArgumentException( + 'Unable to parse CMYK color from input "' . $input . '"', + previous: $e, + ); + } + + if (!$color instanceof self) { + throw new ColorException('Result must be instance of ' . self::class); + } + + return $color; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::colorspace() + */ + public function colorspace(): ColorspaceInterface + { + return new Colorspace(); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toHex() + */ + public function toHex(bool $prefix = false): string + { + // @phpstan-ignore missingType.checkedException + return $this->toColorspace(Rgb::class)->toHex($prefix); + } + + /** + * Return the CMYK cyan channel. + */ + public function cyan(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Cyan::class); + } + + /** + * Return the CMYK magenta channel. + */ + public function magenta(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Magenta::class); + } + + /** + * Return the CMYK yellow channel. + */ + public function yellow(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Yellow::class); + } + + /** + * Return the CMYK key channel. + */ + public function key(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Key::class); + } + + /** + * Return the CMYK alpha channel. + */ + public function alpha(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Alpha::class); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toString() + */ + public function toString(): string + { + if ($this->isTransparent()) { + return sprintf( + 'cmyk(%d %d %d %d / %s)', + $this->cyan()->value(), + $this->magenta()->value(), + $this->yellow()->value(), + $this->key()->value(), + $this->alpha()->toString(), + ); + } + + return sprintf( + 'cmyk(%d %d %d %d)', + $this->cyan()->value(), + $this->magenta()->value(), + $this->yellow()->value(), + $this->key()->value() + ); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::isGrayscale() + */ + public function isGrayscale(): bool + { + return 0 === array_sum([ + $this->cyan()->value(), + $this->magenta()->value(), + $this->yellow()->value(), + ]); + } +} diff --git a/src/Colors/Cmyk/Colorspace.php b/src/Colors/Cmyk/Colorspace.php new file mode 100644 index 000000000..8c6ba3568 --- /dev/null +++ b/src/Colors/Cmyk/Colorspace.php @@ -0,0 +1,139 @@ + + */ + public static array $channels = [ + Channels\Cyan::class, + Channels\Magenta::class, + Channels\Yellow::class, + Channels\Key::class, + Channels\Alpha::class, + ]; + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::colorFromNormalized() + * + * @throws InvalidArgumentException + */ + public static function colorFromNormalized(array $normalized): CmykColor + { + if (!in_array(count($normalized), [4, 5])) { + throw new InvalidArgumentException('Number of color channels must be 4 or 5 for ' . static::class); + } + + // add alpha value if missing + $normalized = count($normalized) === 4 ? array_pad($normalized, 5, 1) : $normalized; + + return new Color(...array_map( + function (string $channel, null|float $normalized) { + try { + return $channel::fromNormalized($normalized); + } catch (TypeError $e) { + throw new InvalidArgumentException( + 'Normalized color value must be in range 0 to 1', + previous: $e + ); + } + }, + self::$channels, + $normalized + )); + } + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::importColor() + * + * @throws ColorException + */ + public function importColor(ColorInterface $color): CmykColor + { + return match ($color::class) { + OklchColor::class, + OklabColor::class, + HsvColor::class, + NamedColor::class, + HslColor::class => $this->importViaRgbColor($color), + RgbColor::class => $this->importRgbColor($color), + CmykColor::class => $color, + default => throw new ColorException( + 'Unable to import color ' . $color::class . ' to ' . $this::class, + ), + }; + } + + /** + * Import given RGB color to CMYK colorspace. + * + * @throws ColorException + */ + private function importRgbColor(RgbColor $color): CmykColor + { + $c = (255 - $color->red()->value()) / 255.0 * 100; + $m = (255 - $color->green()->value()) / 255.0 * 100; + $y = (255 - $color->blue()->value()) / 255.0 * 100; + $k = intval(round(min([$c, $m, $y]))); + + $c = intval(round($c - $k)); + $m = intval(round($m - $k)); + $y = intval(round($y - $k)); + + try { + return new CmykColor($c, $m, $y, $k, $color->alpha()->normalized()); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + } + + /** + * Import given color to CMYK colorspace by converting it to RGB first. + * + * @throws ColorException + */ + private function importViaRgbColor(NamedColor|OklabColor|OklchColor|HslColor|HsvColor $color): CmykColor + { + try { + $color = $color->toColorspace(RgbColorspace::class); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + + if (!$color instanceof RgbColor) { + throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class); + } + + return $this->importRgbColor($color); + } +} diff --git a/src/Colors/Cmyk/Decoders/StringColorDecoder.php b/src/Colors/Cmyk/Decoders/StringColorDecoder.php new file mode 100644 index 000000000..c1de46cba --- /dev/null +++ b/src/Colors/Cmyk/Decoders/StringColorDecoder.php @@ -0,0 +1,57 @@ +[0-9\.]+%?)((, ?)| )' . + '(?P[0-9\.]+%?)((, ?)| )' . + '(?P[0-9\.]+%?)((, ?)| )' . + '(?P[0-9\.]+%?)\)$/i'; + + /** + * {@inheritdoc} + * + * @see DecoderInterface::supports() + */ + public function supports(mixed $input): bool + { + if (!is_string($input)) { + return false; + } + + if (!str_starts_with(strtolower($input), 'cmyk')) { + return false; + } + + return true; + } + + /** + * Decode CMYK color strings + * + * @throws InvalidArgumentException + */ + public function decode(mixed $input): ColorInterface + { + if (preg_match(self::PATTERN, (string) $input, $matches) !== 1) { + throw new InvalidArgumentException('Invalid cmyk() color syntax "' . $input . '"'); + } + + $values = array_map(function (string $value): int { + return intval(round(floatval(trim(str_replace('%', '', $value))))); + }, [$matches['c'], $matches['m'], $matches['y'], $matches['k']]); + + return new Color(...$values); + } +} diff --git a/src/Colors/FloatColorChannel.php b/src/Colors/FloatColorChannel.php new file mode 100644 index 000000000..8080f3210 --- /dev/null +++ b/src/Colors/FloatColorChannel.php @@ -0,0 +1,46 @@ +value = (float) $this->validValueOrFail($value); + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::fromNormalized() + * + * @throws InvalidArgumentException + */ + public static function fromNormalized(float $normalized): self + { + if ($normalized < 0 || $normalized > 1) { + throw new InvalidArgumentException( + 'Normalized color channel value must be between 0 to 1', + ); + } + + return new static(static::min() + $normalized * (static::max() - static::min())); + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::value() + */ + public function value(): float + { + return (float) $this->value; + } +} diff --git a/src/Colors/Hsl/Channels/Alpha.php b/src/Colors/Hsl/Channels/Alpha.php new file mode 100644 index 000000000..006b991c5 --- /dev/null +++ b/src/Colors/Hsl/Channels/Alpha.php @@ -0,0 +1,12 @@ +channels = [ + is_int($h) ? new Hue($h) : $h, + is_int($s) ? new Saturation($s) : $s, + is_int($l) ? new Luminance($l) : $l, + is_float($a) ? new Alpha($a) : $a, + ]; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::create() + * + * @throws InvalidArgumentException + */ + public static function create(int|Hue $h, int|Saturation $s, int|Luminance $l, float|Alpha $a = 1): self + { + return new self($h, $s, $l, $a); + } + + /** + * Parse HSL color from string. + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public static function parse(string $input): self + { + try { + $color = InputHandler::usingDecoders([ + StringColorDecoder::class, + ])->handle($input); + } catch (NotSupportedException | DriverException $e) { + throw new InvalidArgumentException( + 'Unable to parse HSL color from input "' . $input . '"', + previous: $e, + ); + } + + if (!$color instanceof self) { + throw new ColorException('Result must be instance of ' . self::class); + } + + return $color; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::colorspace() + */ + public function colorspace(): ColorspaceInterface + { + return new Colorspace(); + } + + /** + * Return the Hue channel + */ + public function hue(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Hue::class); + } + + /** + * Return the Saturation channel. + */ + public function saturation(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Saturation::class); + } + + /** + * Return the Luminance channel. + */ + public function luminance(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Luminance::class); + } + + /** + * Return the alpha channel. + */ + public function alpha(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Alpha::class); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toHex() + * + * @throws NotSupportedException + */ + public function toHex(bool $prefix = false): string + { + // @phpstan-ignore missingType.checkedException + return $this->toColorspace(Rgb::class)->toHex($prefix); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toString() + */ + public function toString(): string + { + if ($this->isTransparent()) { + return sprintf( + 'hsl(%d %d%% %d%% / %s)', + $this->hue()->value(), + $this->saturation()->value(), + $this->luminance()->value(), + $this->alpha()->toString(), + ); + } + + return sprintf( + 'hsl(%d %d%% %d%%)', + $this->hue()->value(), + $this->saturation()->value(), + $this->luminance()->value() + ); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::isGrayscale() + */ + public function isGrayscale(): bool + { + return floatval($this->saturation()->value()) === 0.0; + } +} diff --git a/src/Colors/Hsl/Colorspace.php b/src/Colors/Hsl/Colorspace.php new file mode 100644 index 000000000..4f4f2d5a4 --- /dev/null +++ b/src/Colors/Hsl/Colorspace.php @@ -0,0 +1,204 @@ + + */ + public static array $channels = [ + Channels\Hue::class, + Channels\Saturation::class, + Channels\Luminance::class, + Channels\Alpha::class, + ]; + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::colorFromNormalized() + * + * @throws InvalidArgumentException + */ + public static function colorFromNormalized(array $normalized): HslColor + { + if (!in_array(count($normalized), [3, 4])) { + throw new InvalidArgumentException('Number of color channels must be 3 or 4 for ' . static::class); + } + + // add alpha value if missing + $normalized = count($normalized) === 3 ? array_pad($normalized, 4, 1) : $normalized; + + return new Color(...array_map( + function (string $channel, null|float $normalized) { + try { + return $channel::fromNormalized($normalized); + } catch (TypeError $e) { + throw new InvalidArgumentException( + 'Normalized color value must be in range 0 to 1', + previous: $e + ); + } + }, + self::$channels, + $normalized + )); + } + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::importColor() + * + * @throws ColorException + */ + public function importColor(ColorInterface $color): HslColor + { + return match ($color::class) { + HslColor::class => $color, + OklchColor::class, + OklabColor::class, + NamedColor::class, + CmykColor::class => $this->importViaRgbColor($color), + RgbColor::class => $this->importRgbColor($color), + HsvColor::class => $this->importHsvColor($color), + default => throw new ColorException( + 'Unable to import color ' . $color::class . ' to ' . $this::class, + ), + }; + } + + /** + * Import given RGB color to HSL colorspace. + * + * @throws ColorException + */ + private function importRgbColor(RgbColor $color): HslColor + { + // normalized values of rgb channels + $values = array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ); + + // take only RGB + $values = array_slice($values, 0, 3); + + // calculate Luminance + $min = min(...$values); + $max = max(...$values); + $luminance = ($max + $min) / 2; + $delta = $max - $min; + + // calculate saturation + $saturation = $delta === 0.0 ? 0 : $delta / (1 - abs(2 * $luminance - 1)); + + // calculate hue + [$r, $g, $b] = $values; + $hue = match (true) { + ($delta === 0.0) => 0, + ($max === $r) => 60 * fmod((($g - $b) / $delta), 6), + ($max === $g) => 60 * ((($b - $r) / $delta) + 2), + ($max === $b) => 60 * ((($r - $g) / $delta) + 4), + default => 0, + }; + + $hue = (round($hue) + 360) % 360; // normalize hue + + try { + return new Color( + intval(round($hue)), + intval(round($saturation * 100)), + intval(round($luminance * 100)), + $color->alpha()->normalized(), + ); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + } + + /** + * Import given HSV color to HSL colorspace. + * + * @throws ColorException + */ + private function importHsvColor(HsvColor $color): HslColor + { + // normalized values of hsv channels + [$h, $s, $v] = array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ); + + // calculate Luminance + $luminance = (2.0 - $s) * $v / 2.0; + + // calculate Saturation + $saturation = match (true) { + $luminance === 0.0 => $s, + $luminance === 1.0 => 0, + $luminance < .5 => $s * $v / ($luminance * 2), + default => $s * $v / (2 - $luminance * 2), + }; + + try { + return new Color( + intval(round($h * 360)), + intval(round($saturation * 100)), + intval(round($luminance * 100)), + $color->alpha()->normalized(), + ); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + } + + /** + * Import given color to HSL color space by converting it to RGB first. + * + * @throws ColorException + */ + private function importViaRgbColor(NamedColor|CmykColor|OklabColor|OklchColor $color): HslColor + { + try { + $color = $color->toColorspace(Rgb::class); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + + if (!$color instanceof RgbColor) { + throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class); + } + + return $this->importRgbColor($color); + } +} diff --git a/src/Colors/Hsl/Decoders/StringColorDecoder.php b/src/Colors/Hsl/Decoders/StringColorDecoder.php new file mode 100644 index 000000000..64efffa26 --- /dev/null +++ b/src/Colors/Hsl/Decoders/StringColorDecoder.php @@ -0,0 +1,71 @@ +[0-9\.]+)(?:deg)?((, ?)| )' . + '(?P[0-9\.]+%?)((, ?)| )' . + '(?P[0-9\.]+%?)' . + '(?:(?:(?: ?\/ ?)|(?:[, ]) ?)' . + '(?(?:0\.[0-9]+)|1\.0|\.[0-9]+|[0-9]{1,3}%|1|0))?' . + ' ?\)$/i'; + + /** + * {@inheritdoc} + * + * @see DecoderInterface::supports() + */ + public function supports(mixed $input): bool + { + if (!is_string($input)) { + return false; + } + + if (!str_starts_with(strtolower($input), 'hsl')) { + return false; + } + + return true; + } + + /** + * Decode hsl color strings. + * + * @throws InvalidArgumentException + */ + public function decode(mixed $input): ColorInterface + { + if (preg_match(self::PATTERN, $input, $matches) !== 1) { + throw new InvalidArgumentException('Invalid hsl() color syntax "' . $input . '"'); + } + + $values = array_map(fn(string $value): int => match (strpos($value, '%')) { + false => intval(trim($value)), + default => intval(trim(str_replace('%', '', $value))), + }, [$matches['h'], $matches['s'], $matches['l']]); + + // alpha value + if (array_key_exists('a', $matches)) { + $values[] = match (strpos($matches['a'], '%')) { + false => floatval(trim($matches['a'])), + default => floatval(trim(str_replace('%', '', $matches['a']))) / 100, + }; + } + + return new Color(...$values); + } +} diff --git a/src/Colors/Hsv/Channels/Alpha.php b/src/Colors/Hsv/Channels/Alpha.php new file mode 100644 index 000000000..f4df93229 --- /dev/null +++ b/src/Colors/Hsv/Channels/Alpha.php @@ -0,0 +1,12 @@ +channels = [ + is_int($h) ? new Hue($h) : $h, + is_int($s) ? new Saturation($s) : $s, + is_int($v) ? new Value($v) : $v, + is_float($a) ? new Alpha($a) : $a, + ]; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::create() + * + * @throws InvalidArgumentException + */ + public static function create(int|Hue $h, int|Saturation $s, int|Value $v, float|Alpha $a = 1): self + { + return new self($h, $s, $v, $a); + } + + /** + * Parse HSV color from string. + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public static function parse(string $input): self + { + try { + $color = InputHandler::usingDecoders([ + StringColorDecoder::class, + ])->handle($input); + } catch (NotSupportedException | DriverException $e) { + throw new InvalidArgumentException( + 'Unable to parse HSV color from input "' . $input . '"', + previous: $e, + ); + } + + if (!$color instanceof self) { + throw new ColorException('Result must be instance of ' . self::class); + } + + return $color; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::colorspace() + */ + public function colorspace(): ColorspaceInterface + { + return new Colorspace(); + } + + /** + * Return the Hue channel. + */ + public function hue(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Hue::class); + } + + /** + * Return the Saturation channel. + */ + public function saturation(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Saturation::class); + } + + /** + * Return the Value channel. + */ + public function value(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Value::class); + } + + /** + * Return alpha channel. + */ + public function alpha(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Alpha::class); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toHex() + */ + public function toHex(bool $prefix = false): string + { + // @phpstan-ignore missingType.checkedException + return $this->toColorspace(Rgb::class)->toHex($prefix); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toString() + */ + public function toString(): string + { + if ($this->isTransparent()) { + return sprintf( + 'hsv(%d %d%% %d%% / %s)', + $this->hue()->value(), + $this->saturation()->value(), + $this->value()->value(), + $this->alpha()->toString(), + ); + } + + return sprintf( + 'hsv(%d %d%% %d%%)', + $this->hue()->value(), + $this->saturation()->value(), + $this->value()->value() + ); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::isGrayscale() + */ + public function isGrayscale(): bool + { + return floatval($this->saturation()->value()) === 0.0; + } +} diff --git a/src/Colors/Hsv/Colorspace.php b/src/Colors/Hsv/Colorspace.php new file mode 100644 index 000000000..4f32e6b93 --- /dev/null +++ b/src/Colors/Hsv/Colorspace.php @@ -0,0 +1,199 @@ + + */ + public static array $channels = [ + Channels\Hue::class, + Channels\Saturation::class, + Channels\Value::class, + Channels\Alpha::class, + ]; + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::colorFromNormalized() + * + * @throws InvalidArgumentException + */ + public static function colorFromNormalized(array $normalized): HsvColor + { + if (!in_array(count($normalized), [3, 4])) { + throw new InvalidArgumentException('Number of color channels must be 3 or 4 for ' . static::class); + } + + // add alpha value if missing + $normalized = count($normalized) === 3 ? array_pad($normalized, 4, 1) : $normalized; + + return new Color(...array_map( + function (string $channel, null|float $normalized) { + try { + return $channel::fromNormalized($normalized); + } catch (TypeError $e) { + throw new InvalidArgumentException( + 'Normalized color value must be in range 0 to 1', + previous: $e + ); + } + }, + self::$channels, + $normalized + )); + } + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::importColor() + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public function importColor(ColorInterface $color): HsvColor + { + return match ($color::class) { + CmykColor::class, + OklchColor::class, + NamedColor::class, + OklabColor::class => $this->importViaRgbColor($color), + RgbColor::class => $this->importRgbColor($color), + HslColor::class => $this->importHslColor($color), + HsvColor::class => $color, + default => throw new ColorException( + 'Unable to import color ' . $color::class . ' to ' . $this::class, + ), + }; + } + + /** + * Import given RGB color to HSV colorspace. + * + * @throws ColorException + */ + private function importRgbColor(RgbColor $color): HsvColor + { + // normalized values of rgb channels + $values = array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ); + + // take only RGB + $values = array_slice($values, 0, 3); + + // calculate chroma + $min = min(...$values); + $max = max(...$values); + $chroma = $max - $min; + + // calculate value + $v = 100 * $max; + + if ($chroma === 0.0) { + // grayscale color + try { + return new Color(0, 0, intval(round($v)), $color->alpha()->normalized()); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + } + + // calculate saturation + $s = 100 * ($chroma / $max); + + // calculate hue + [$r, $g, $b] = $values; + $h = match (true) { + ($r === $min) => 3 - (($g - $b) / $chroma), + ($b === $min) => 1 - (($r - $g) / $chroma), + default => 5 - (($b - $r) / $chroma), + } * 60; + + try { + return new Color( + intval(round($h)), + intval(round($s)), + intval(round($v)), + $color->alpha()->normalized(), + ); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + } + + /** + * Import given HSL color to HSV colorspace. + * + * @throws InvalidArgumentException + */ + protected function importHslColor(ColorInterface $color): HsvColor + { + if (!$color instanceof HslColor) { + throw new InvalidArgumentException('Color must be of type ' . HslColor::class); + } + + // normalized values of hsl channels + [$h, $s, $l] = array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels() + ); + + $v = $l + $s * min($l, 1 - $l); + $s = ($v === 0.0) ? 0 : 2 * (1 - $l / $v); + + return $this->colorFromNormalized([$h, $s, $v, $color->alpha()->normalized()]); + } + + /** + * Import given color to HSV color space by converting it to RGB first. + * + * @throws ColorException + */ + private function importViaRgbColor(NamedColor|CmykColor|OklchColor|OklabColor $color): HsvColor + { + try { + $color = $color->toColorspace(RgbColorspace::class); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + + if (!$color instanceof RgbColor) { + throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class); + } + + return $this->importRgbColor($color); + } +} diff --git a/src/Colors/Hsv/Decoders/StringColorDecoder.php b/src/Colors/Hsv/Decoders/StringColorDecoder.php new file mode 100644 index 000000000..12a5d776e --- /dev/null +++ b/src/Colors/Hsv/Decoders/StringColorDecoder.php @@ -0,0 +1,71 @@ +[0-9\.]+)(?:deg)?((, ?)| )' . + '(?P[0-9\.]+%?)((, ?)| )' . + '(?P[0-9\.]+%?)' . + '(?:(?:(?: ?\/ ?)|(?:[, ]) ?)' . + '(?(?:0\.[0-9]+)|1\.0|\.[0-9]+|[0-9]{1,3}%|1|0))?' . + ' ?\)$/i'; + + /** + * {@inheritdoc} + * + * @see DecoderInterface::supports() + */ + public function supports(mixed $input): bool + { + if (!is_string($input)) { + return false; + } + + if (preg_match('/^hs(v|b)/i', $input) !== 1) { + return false; + } + + return true; + } + + /** + * Decode hsv/hsb color strings. + * + * @throws InvalidArgumentException + */ + public function decode(mixed $input): ColorInterface + { + if (preg_match(self::PATTERN, $input, $matches) !== 1) { + throw new InvalidArgumentException('Invalid hsv() or hsb() color syntax "' . $input . '"'); + } + + $values = array_map(fn(string $value): int => match (strpos($value, '%')) { + false => intval(trim($value)), + default => intval(trim(str_replace('%', '', $value))), + }, [$matches['h'], $matches['s'], $matches['v']]); + + // alpha value + if (array_key_exists('a', $matches)) { + $values[] = match (strpos($matches['a'], '%')) { + false => floatval(trim($matches['a'])), + default => floatval(trim(str_replace('%', '', $matches['a']))) / 100, + }; + } + + return new Color(...$values); + } +} diff --git a/src/Colors/IntegerColorChannel.php b/src/Colors/IntegerColorChannel.php new file mode 100644 index 000000000..f703f53c9 --- /dev/null +++ b/src/Colors/IntegerColorChannel.php @@ -0,0 +1,46 @@ +value = (int) $this->validValueOrFail($value); + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::fromNormalized() + * + * @throws InvalidArgumentException + */ + public static function fromNormalized(float $normalized): self + { + if ($normalized < 0 || $normalized > 1) { + throw new InvalidArgumentException( + 'Normalized color channel value must be between 0 to 1', + ); + } + + return new static(intval(round($normalized * static::max()))); + } + + /** + * {@inheritdoc} + * + * @see ColorChannelInterface::value() + */ + public function value(): int + { + return (int) $this->value; + } +} diff --git a/src/Colors/Oklab/Channels/A.php b/src/Colors/Oklab/Channels/A.php new file mode 100644 index 000000000..b824f5ef7 --- /dev/null +++ b/src/Colors/Oklab/Channels/A.php @@ -0,0 +1,30 @@ +channels = [ + is_float($l) ? new Lightness($l) : $l, + is_float($a) ? new A($a) : $a, + is_float($b) ? new B($b) : $b, + is_float($alpha) ? new Alpha($alpha) : $alpha, + ]; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::create() + * + * @throws InvalidArgumentException + */ + public static function create(float|Lightness $l, float|A $a, float|B $b, float|Alpha $alpha = 1): self + { + return new self($l, $a, $b, $alpha); + } + + /** + * Parse OKLAB color from string. + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public static function parse(string $input): self + { + try { + $color = InputHandler::usingDecoders([ + StringColorDecoder::class, + ])->handle($input); + } catch (NotSupportedException | DriverException $e) { + throw new InvalidArgumentException( + 'Unable to parse OKLAB color from input "' . $input . '"', + previous: $e, + ); + } + + if (!$color instanceof self) { + throw new ColorException('Result must be instance of ' . self::class); + } + + return $color; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::colorspace() + */ + public function colorspace(): ColorspaceInterface + { + return new Colorspace(); + } + + /** + * Return the Lightness channel. + */ + public function lightness(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Lightness::class); + } + + /** + * Return the a axis (green-red) channel. + */ + public function a(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(A::class); + } + + /** + * Return the b axis (blue-yellow) channel. + */ + public function b(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(B::class); + } + + /** + * Return alpha channel. + */ + public function alpha(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Alpha::class); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toHex() + */ + public function toHex(bool $prefix = false): string + { + // @phpstan-ignore missingType.checkedException + return $this->toColorspace(Rgb::class)->toHex($prefix); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toString() + */ + public function toString(): string + { + if ($this->isTransparent()) { + return sprintf( + 'oklab(%s %s %s / %s)', + $this->lightness()->value(), + $this->a()->value(), + $this->b()->value(), + $this->alpha()->toString(), + ); + } + + return sprintf( + 'oklab(%s %s %s)', + $this->lightness()->value(), + $this->a()->value(), + $this->b()->value(), + ); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::isGrayscale() + */ + public function isGrayscale(): bool + { + return $this->a()->value() === 0.0 && $this->b()->value() === 0.0; + } +} diff --git a/src/Colors/Oklab/Colorspace.php b/src/Colors/Oklab/Colorspace.php new file mode 100644 index 000000000..06e18ab14 --- /dev/null +++ b/src/Colors/Oklab/Colorspace.php @@ -0,0 +1,177 @@ + + */ + public static array $channels = [ + Channels\Lightness::class, + Channels\A::class, + Channels\B::class, + Channels\Alpha::class, + ]; + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::colorFromNormalized() + * + * @throws InvalidArgumentException + */ + public static function colorFromNormalized(array $normalized): OklabColor + { + if (!in_array(count($normalized), [3, 4])) { + throw new InvalidArgumentException('Number of color channels must be 3 or 4 for ' . static::class); + } + + // add alpha value if missing + $normalized = count($normalized) === 3 ? array_pad($normalized, 4, 1) : $normalized; + + return new Color(...array_map( + function (string $channel, null|float $normalized) { + try { + return $channel::fromNormalized($normalized); + } catch (TypeError $e) { + throw new InvalidArgumentException( + 'Normalized color value must be in range 0 to 1', + previous: $e + ); + } + }, + self::$channels, + $normalized + )); + } + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::importColor() + * + * @throws ColorException + */ + public function importColor(ColorInterface $color): OklabColor + { + return match ($color::class) { + CmykColor::class, + HsvColor::class, + NamedColor::class, + HslColor::class => $this->importViaRgbColor($color), + RgbColor::class => $this->importRgbColor($color), + OklchColor::class => $this->importOklchColor($color), + OklabColor::class => $color, + default => throw new ColorException( + 'Unable to import color ' . $color::class . ' to ' . $this::class, + ), + }; + } + + /** + * Import given RGB color OKLAB colorspace. + * + * @throws ColorException + */ + private function importRgbColor(RgbColor $color): OklabColor + { + $cbrt = fn(float $x): float => $x < 0 ? -abs($x) ** (1 / 3) : $x ** (1 / 3); + $rgbToLinear = fn(float $x): float => $x <= 0.04045 ? $x / 12.92 : (($x + 0.055) / 1.055) ** 2.4; + + $r = $color->red()->normalized(); + $g = $color->green()->normalized(); + $b = $color->blue()->normalized(); + + $r = $rgbToLinear($r); + $g = $rgbToLinear($g); + $b = $rgbToLinear($b); + + $l = 0.4122214708 * $r + 0.5363325363 * $g + 0.0514459929 * $b; + $m = 0.2119034982 * $r + 0.6806995451 * $g + 0.1073969566 * $b; + $s = 0.0883024619 * $r + 0.2817188376 * $g + 0.6299787005 * $b; + + $l = $cbrt($l); + $m = $cbrt($m); + $s = $cbrt($s); + + try { + return new Color( + 0.2104542553 * $l + 0.7936177850 * $m - 0.0040720468 * $s, + 1.9779984951 * $l - 2.4285922050 * $m + 0.4505937099 * $s, + 0.0259040371 * $l + 0.7827717662 * $m - 0.8086757660 * $s, + $color->alpha()->normalized(), + ); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + } + + /** + * Import given OKLCH color OKLAB colorspace. + * + * @throws ColorException + */ + private function importOklchColor(OklchColor $color): OklabColor + { + $hRad = deg2rad($color->hue()->value()); + + try { + return new Color( + $color->lightness()->value(), + $color->chroma()->value() * cos($hRad), + $color->chroma()->value() * sin($hRad), + $color->alpha()->normalized(), + ); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + } + + /** + * Import given color to OKLAB color space by converting it to RGB first. + * + * @throws ColorException + */ + private function importViaRgbColor(NamedColor|CmykColor|HslColor|HsvColor $color): OklabColor + { + try { + $color = $color->toColorspace(Rgb::class)->toColorspace($this::class); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + + if (!$color instanceof OklabColor) { + throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class); + } + + return $color; + } +} diff --git a/src/Colors/Oklab/Decoders/StringColorDecoder.php b/src/Colors/Oklab/Decoders/StringColorDecoder.php new file mode 100644 index 000000000..a93317ccd --- /dev/null +++ b/src/Colors/Oklab/Decoders/StringColorDecoder.php @@ -0,0 +1,93 @@ +(1|0|0?\.[0-9]+)|[0-9\.]+%)((, ?)|( ))' . + '(?P(-?0|-?0?\.[0-9\.]+)|(-?[0-9\.]+%))((, ?)|( ))' . + '(?P(-?0|-?0?\.[0-9\.]+)|(-?[0-9\.]+%))' . + '(?:(?:(?: ?\/ ?)|(?:[, ]) ?)' . + '(?(?:0\.[0-9]+)|1\.0|\.[0-9]+|[0-9]{1,3}%|1|0))?' . + ' ?\)$/i'; + + /** + * {@inheritdoc} + * + * @see DecoderInterface::supports() + */ + public function supports(mixed $input): bool + { + if (!is_string($input)) { + return false; + } + + if (!str_starts_with(strtolower($input), 'oklab')) { + return false; + } + + return true; + } + + /** + * Decode hsl color strings. + * + * @throws InvalidArgumentException + */ + public function decode(mixed $input): ColorInterface + { + if (preg_match(self::PATTERN, $input, $matches) !== 1) { + throw new InvalidArgumentException('Invalid oklab() color syntax "' . $input . '"'); + } + + $values = [ + $this->decodeChannelValue($matches['l'], Lightness::class), + $this->decodeChannelValue($matches['a'], A::class), + $this->decodeChannelValue($matches['b'], B::class), + ]; + + // alpha value + if (array_key_exists('alpha', $matches)) { + $values[] = $this->decodeAlphaChannelValue($matches['alpha']); + } + + return new Color(...$values); + } + + /** + * Decode channel value. + */ + private function decodeChannelValue(string $value, string $channel): float + { + if (strpos($value, '%')) { + return floatval(trim(str_replace('%', '', $value))) * $channel::max() / 100; + } + + return floatval(trim($value)); + } + + private function decodeAlphaChannelValue(string $value): float + { + if (strpos($value, '%')) { + return floatval(trim(str_replace('%', '', $value))) / 100; + } + + return floatval(trim($value)); + } +} diff --git a/src/Colors/Oklch/Channels/Alpha.php b/src/Colors/Oklch/Channels/Alpha.php new file mode 100644 index 000000000..c4a963ca9 --- /dev/null +++ b/src/Colors/Oklch/Channels/Alpha.php @@ -0,0 +1,12 @@ +channels = [ + is_float($l) ? new Lightness($l) : $l, + is_float($c) ? new Chroma($c) : $c, + is_float($h) ? new Hue($h) : $h, + is_float($a) ? new Alpha($a) : $a, + ]; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::create() + * + * @throws InvalidArgumentException + */ + public static function create(float|Lightness $l, float|Chroma $c, float|Hue $h, float|Alpha $a = 1): self + { + return new self($l, $c, $h, $a); + } + + /** + * Parse OKLCH color from string. + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public static function parse(string $input): self + { + try { + $color = InputHandler::usingDecoders([ + StringColorDecoder::class, + ])->handle($input); + } catch (NotSupportedException | DriverException $e) { + throw new InvalidArgumentException( + 'Unable to parse OKLCH color from input "' . $input . '"', + previous: $e, + ); + } + + if (!$color instanceof self) { + throw new ColorException('Result must be instance of ' . self::class); + } + + return $color; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::colorspace() + */ + public function colorspace(): ColorspaceInterface + { + return new Colorspace(); + } + + /** + * Return the Lightness channel. + */ + public function lightness(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Lightness::class); + } + + /** + * Return the chroma channel. + */ + public function chroma(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Chroma::class); + } + + /** + * Return the hue channel. + */ + public function hue(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Hue::class); + } + + /** + * Return the alpha channel. + */ + public function alpha(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Alpha::class); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toHex() + */ + public function toHex(bool $prefix = false): string + { + // @phpstan-ignore missingType.checkedException + return $this->toColorspace(Rgb::class)->toHex($prefix); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toString() + */ + public function toString(): string + { + if ($this->isTransparent()) { + return sprintf( + 'oklch(%s %s %s / %s)', + $this->lightness()->value(), + $this->chroma()->value(), + $this->hue()->value(), + $this->alpha()->toString(), + ); + } + + return sprintf( + 'oklch(%s %s %s)', + $this->lightness()->value(), + $this->chroma()->value(), + $this->hue()->value(), + ); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::isGrayscale() + */ + public function isGrayscale(): bool + { + return $this->chroma()->value() === 0.0; + } +} diff --git a/src/Colors/Oklch/Colorspace.php b/src/Colors/Oklch/Colorspace.php new file mode 100644 index 000000000..109168b97 --- /dev/null +++ b/src/Colors/Oklch/Colorspace.php @@ -0,0 +1,164 @@ + + */ + public static array $channels = [ + Lightness::class, + Chroma::class, + Hue::class, + Alpha::class, + ]; + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::colorFromNormalized() + * + * @throws InvalidArgumentException + */ + public static function colorFromNormalized(array $normalized): OklchColor + { + if (!in_array(count($normalized), [3, 4])) { + throw new InvalidArgumentException('Number of color channels must be 3 or 4 for ' . static::class); + } + + // add alpha value if missing + $normalized = count($normalized) === 3 ? array_pad($normalized, 4, 1) : $normalized; + + return new Color(...array_map( + function (string $channel, null|float $normalized) { + try { + return $channel::fromNormalized($normalized); + } catch (TypeError $e) { + throw new InvalidArgumentException( + 'Normalized color value must be in range 0 to 1', + previous: $e + ); + } + }, + self::$channels, + $normalized + )); + } + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::importColor() + * + * @throws ColorException + */ + public function importColor(ColorInterface $color): OklchColor + { + return match ($color::class) { + CmykColor::class, + HsvColor::class, + NamedColor::class, + HslColor::class => $this->importViaRgbColor($color), + OklabColor::class => $this->importOklabColor($color), + RgbColor::class => $this->importRgbColor($color), + OklchColor::class => $color, + default => throw new ColorException( + 'Unable to import color ' . $color::class . ' to ' . $this::class, + ), + }; + } + + /** + * Import given OKLAB color OKLCH colorspace. + * + * @throws ColorException + */ + private function importOklabColor(OklabColor $color): OklchColor + { + $a = $color->a()->value(); + $b = $color->b()->value(); + + $c = sqrt($a * $a + $b * $b); + $h = rad2deg(atan2($b, $a)); + $h = $h < 0 ? $h + 360 : $h; + + try { + return new Color($color->lightness()->value(), $c, $h, $color->alpha()->normalized()); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + } + + /** + * Import given RGB color to OKLCH color space. + * + * @throws ColorException + */ + private function importRgbColor(RgbColor $color): OklchColor + { + try { + $color = $color->toColorspace(Oklab::class); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + + if (!$color instanceof OklabColor) { + throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class); + } + + return $this->importOklabColor($color); + } + + /** + * Import given color to OKLCH color space by converting it to RGB first. + * + * @throws ColorException + */ + private function importViaRgbColor(NamedColor|HslColor|HsvColor|CmykColor $color): OklchColor + { + try { + $color = $color->toColorspace(Rgb::class)->toColorspace($this::class); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + + if (!$color instanceof OklchColor) { + throw new ColorException('Failed to import color ' . $color::class . ' to ' . $this::class); + } + + return $color; + } +} diff --git a/src/Colors/Oklch/Decoders/StringColorDecoder.php b/src/Colors/Oklch/Decoders/StringColorDecoder.php new file mode 100644 index 000000000..a448acca4 --- /dev/null +++ b/src/Colors/Oklch/Decoders/StringColorDecoder.php @@ -0,0 +1,93 @@ +(1|0|0?\.[0-9]+)|[0-9\.]+%)((, ?)|( ))' . + '(?P(-?0|-?0?\.[0-9\.]+)|(-?[0-9\.]+%))((, ?)|( ))' . + '(?P[0-9\.]+)' . + '(?:(?:(?: ?\/ ?)|(?:[, ]) ?)' . + '(?(?:0\.[0-9]+)|1\.0|\.[0-9]+|[0-9]{1,3}%|1|0))?' . + ' ?\)$/i'; + + /** + * {@inheritdoc} + * + * @see DecoderInterface::supports() + */ + public function supports(mixed $input): bool + { + if (!is_string($input)) { + return false; + } + + if (!str_starts_with(strtolower($input), 'oklch')) { + return false; + } + + return true; + } + + /** + * Decode hsl color string. + * + * @throws InvalidArgumentException + */ + public function decode(mixed $input): ColorInterface + { + if (preg_match(self::PATTERN, $input, $matches) !== 1) { + throw new InvalidArgumentException('Invalid oklch() color syntax "' . $input . '"'); + } + + $values = [ + $this->decodeChannelValue($matches['l'], Lightness::class), + $this->decodeChannelValue($matches['c'], Chroma::class), + $this->decodeChannelValue($matches['h'], Hue::class), + ]; + + // alpha value + if (array_key_exists('a', $matches)) { + $values[] = $this->decodeAlphaChannelValue($matches['a']); + } + + return new Color(...$values); + } + + /** + * Decode channel value. + */ + private function decodeChannelValue(string $value, string $channel): float + { + if (strpos($value, '%')) { + return floatval(trim(str_replace('%', '', $value))) * $channel::max() / 100; + } + + return floatval(trim($value)); + } + + private function decodeAlphaChannelValue(string $value): float + { + if (strpos($value, '%')) { + return floatval(trim(str_replace('%', '', $value))) / 100; + } + + return floatval(trim($value)); + } +} diff --git a/src/Colors/Profile.php b/src/Colors/Profile.php new file mode 100644 index 000000000..ad5cd2a02 --- /dev/null +++ b/src/Colors/Profile.php @@ -0,0 +1,21 @@ +channels = [ + is_int($r) ? new Red($r) : $r, + is_int($g) ? new Green($g) : $g, + is_int($b) ? new Blue($b) : $b, + is_float($a) ? new Alpha($a) : $a, + ]; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::create() + * + * @throws InvalidArgumentException + */ + public static function create(int|Red $r, int|Green $g, int|Blue $b, float|Alpha $a = 1): self + { + return new self($r, $g, $b, $a); + } + + /** + * Parse RGB color from string. + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public static function parse(string $input): self + { + try { + $color = InputHandler::usingDecoders([ + StringColorDecoder::class, + NamedColorDecoder::class, + HexColorDecoder::class, + ])->handle($input); + } catch (NotSupportedException | DriverException $e) { + throw new InvalidArgumentException( + 'Unable to parse RGB color from input "' . $input . '"', + previous: $e, + ); + } + + if (!$color instanceof self) { + throw new ColorException('Result must be instance of ' . self::class); + } + + return $color; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::colorspace() + */ + public function colorspace(): ColorspaceInterface + { + return new Colorspace(); + } + + /** + * Return the RGB red color channel. + */ + public function red(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Red::class); + } + + /** + * Return the RGB green color channel. + */ + public function green(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Green::class); + } + + /** + * Return the RGB blue color channel. + */ + public function blue(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Blue::class); + } + + /** + * Return the colors alpha channel. + */ + public function alpha(): ColorChannelInterface + { + /** @throws void */ + return $this->channel(Alpha::class); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toHex() + */ + public function toHex(bool $prefix = false): string + { + if ($this->isTransparent()) { + return sprintf( + '%s%02x%02x%02x%02x', + $prefix ? '#' : '', + $this->red()->value(), + $this->green()->value(), + $this->blue()->value(), + $this->alpha()->value() + ); + } + + return sprintf( + '%s%02x%02x%02x', + $prefix ? '#' : '', + $this->red()->value(), + $this->green()->value(), + $this->blue()->value() + ); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toString() + */ + public function toString(): string + { + if ($this->isTransparent()) { + return sprintf( + 'rgb(%d %d %d / %s)', + $this->red()->value(), + $this->green()->value(), + $this->blue()->value(), + $this->alpha()->toString(), + ); + } + + return sprintf( + 'rgb(%d %d %d)', + $this->red()->value(), + $this->green()->value(), + $this->blue()->value() + ); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::isGrayscale() + */ + public function isGrayscale(): bool + { + $values = [$this->red()->value(), $this->green()->value(), $this->blue()->value()]; + + return count(array_unique($values, SORT_REGULAR)) === 1; + } +} diff --git a/src/Colors/Rgb/Colorspace.php b/src/Colors/Rgb/Colorspace.php new file mode 100644 index 000000000..aa7ed9d8a --- /dev/null +++ b/src/Colors/Rgb/Colorspace.php @@ -0,0 +1,286 @@ + + */ + public static array $channels = [ + Channels\Red::class, + Channels\Green::class, + Channels\Blue::class, + Channels\Alpha::class + ]; + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::colorFromNormalized() + * + * @throws InvalidArgumentException + */ + public static function colorFromNormalized(array $normalized): RgbColor + { + if (!in_array(count($normalized), [3, 4])) { + throw new InvalidArgumentException('Number of color channels must be 3 or 4 for ' . static::class); + } + + // add alpha value if missing + $normalized = count($normalized) === 3 ? array_pad($normalized, 4, 1) : $normalized; + + return new Color(...array_map( + function (string $channel, null|float $normalized) { + try { + return $channel::fromNormalized($normalized); + } catch (TypeError $e) { + throw new InvalidArgumentException( + 'Normalized color value must be in range 0 to 1', + previous: $e + ); + } + }, + self::$channels, + $normalized + )); + } + + /** + * {@inheritdoc} + * + * @see ColorspaceInterface::importColor() + * + * @throws ColorException + */ + public function importColor(ColorInterface $color): RgbColor + { + return match ($color::class) { + CmykColor::class => $this->importCmykColor($color), + HsvColor::class => $this->importHsvColor($color), + HslColor::class => $this->importHslColor($color), + OklabColor::class => $this->importOklabColor($color), + OklchColor::class => $this->importOklchColor($color), + NamedColor::class => $this->importNamedColor($color), + RgbColor::class => $color, + default => throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + ), + }; + } + + /** + * @throws ColorException + */ + private function importCmykColor(CmykColor $color): RgbColor + { + try { + return new Color( + (int) (255 * (1 - $color->cyan()->normalized()) * (1 - $color->key()->normalized())), + (int) (255 * (1 - $color->magenta()->normalized()) * (1 - $color->key()->normalized())), + (int) (255 * (1 - $color->yellow()->normalized()) * (1 - $color->key()->normalized())), + $color->alpha()->normalized(), + ); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + } + + /** + * Import given HSV color to RGB color space. + * + * @throws ColorException + */ + private function importHsvColor(HsvColor $color): RgbColor + { + $chroma = $color->value()->normalized() * $color->saturation()->normalized(); + $hue = $color->hue()->normalized() * 6; + $x = $chroma * (1 - abs(fmod($hue, 2) - 1)); + + // connect channel values + $values = match (true) { + $hue < 1 => [$chroma, $x, 0], + $hue < 2 => [$x, $chroma, 0], + $hue < 3 => [0, $chroma, $x], + $hue < 4 => [0, $x, $chroma], + $hue < 5 => [$x, 0, $chroma], + default => [$chroma, 0, $x], + }; + + // add to each value + $values = array_map( + fn(float|int $value): float => max(0.0, min(1.0, $value + $color->value()->normalized() - $chroma)), + $values, + ); + + $values[] = $color->alpha()->normalized(); // append alpha channel value + + try { + return $this->colorFromNormalized($values); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + } + + /** + * Import given HSL color to RGB color space. + * + * @throws ColorException + */ + private function importHslColor(HslColor $color): RgbColor + { + // normalized values of hsl channels + [$h, $s, $l] = array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels() + ); + + $c = (1 - abs(2 * $l - 1)) * $s; + $x = $c * (1 - abs(fmod($h * 6, 2) - 1)); + $m = $l - $c / 2; + + $values = match (true) { + $h < 1 / 6 => [$c, $x, 0], + $h < 2 / 6 => [$x, $c, 0], + $h < 3 / 6 => [0, $c, $x], + $h < 4 / 6 => [0, $x, $c], + $h < 5 / 6 => [$x, 0, $c], + default => [$c, 0, $x], + }; + + $values = array_map(fn(float|int $value): float => max(0.0, min(1.0, $value + $m)), $values); + $values[] = $color->alpha()->normalized(); // append alpha channel value + + try { + $color = $this->colorFromNormalized($values); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + + return $color; + } + + /** + * Import given OKLAB color to RGB color space. + * + * @throws ColorException + */ + private function importOklabColor(OklabColor $color): RgbColor + { + $linearToRgb = function (float $c): float { + $c = max(0.0, min(1.0, $c)); + + if ($c <= 0.0031308) { + return 12.92 * $c; + } + + return 1.055 * ($c ** (1 / 2.4)) - 0.055; + }; + + $l = $color->lightness()->value() + 0.3963377774 * $color->a()->value() + 0.2158037573 * $color->b()->value(); + $m = $color->lightness()->value() - 0.1055613458 * $color->a()->value() - 0.0638541728 * $color->b()->value(); + $s = $color->lightness()->value() - 0.0894841775 * $color->a()->value() - 1.2914855480 * $color->b()->value(); + + $l = $l ** 3; + $m = $m ** 3; + $s = $s ** 3; + + $r = +4.0767416621 * $l - 3.3077115913 * $m + 0.2309699292 * $s; + $g = -1.2684380046 * $l + 2.6097574011 * $m - 0.3413193965 * $s; + $b = -0.0041960863 * $l - 0.7034186147 * $m + 1.7076147010 * $s; + + $r = $linearToRgb($r); + $g = $linearToRgb($g); + $b = $linearToRgb($b); + + try { + return new Color( + (int) round($r * 255), + (int) round($g * 255), + (int) round($b * 255), + $color->alpha()->normalized(), + ); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + } + + /** + * Import given OKLCH color to RGB color space. + * + * @throws ColorException + */ + private function importOklchColor(OklchColor $color): RgbColor + { + try { + $color = $color->toColorspace(Oklab::class); + } catch (InvalidArgumentException $e) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + previous: $e, + ); + } + + if (!$color instanceof OklabColor) { + throw new ColorException( + 'Failed to import color ' . $color::class . ' to ' . $this::class, + ); + } + + return $this->importOklabColor($color); + } + + /** + * Import given named color to RGB color space. + * + * @throws ColorException + */ + private function importNamedColor(NamedColor $color): RgbColor + { + try { + $output = InputHandler::usingDecoders([ + HexColorDecoder::class, + ])->handle($color->toHex()); + } catch (InvalidArgumentException | NotSupportedException | DriverException $e) { + throw new ColorException('Failed to import named color to rgb color space', previous: $e); + } + + return $output instanceof RgbColor + ? $output + : throw new ColorException('Failed to import named color to rgb color space'); + } +} diff --git a/src/Colors/Rgb/Decoders/HexColorDecoder.php b/src/Colors/Rgb/Decoders/HexColorDecoder.php new file mode 100644 index 000000000..bf5a02733 --- /dev/null +++ b/src/Colors/Rgb/Decoders/HexColorDecoder.php @@ -0,0 +1,78 @@ +[a-f\d]{3}(?:[a-f\d]?|(?:[a-f\d]{3}(?:[a-f\d]{2})?)?)\b)$/i'; + + /** + * {@inheritdoc} + * + * @see DecoderInterface::supports() + */ + public function supports(mixed $input): bool + { + if (!is_string($input)) { + return false; + } + + if (str_starts_with($input, '#')) { + return true; + } + + // matching max. length & only hexadecimal + if (strlen($input) <= 8 && preg_match('/^[a-f\d]+$/i', $input) === 1) { + return true; + } + + return preg_match(static::PATTERN, $input) === 1; + } + + /** + * Decode hexadecimal rgb colors with and without transparency. + * + * @throws InvalidArgumentException + * @throws ColorDecoderException + */ + public function decode(mixed $input): ColorInterface + { + if (preg_match(static::PATTERN, $input, $matches) !== 1) { + throw new InvalidArgumentException('Hex color has an invalid format'); + } + + // split into hex chunks + $values = match (strlen($matches['hex'])) { + 3, 4 => str_split($matches['hex']), + 6, 8 => str_split($matches['hex'], 2), + default => throw new InvalidArgumentException('Hex color has an incorrect length'), + }; + + // convert to decimal + $values = array_map(function (string $value): int { + return match (strlen($value)) { + 1 => (int) hexdec($value . $value), + 2 => (int) hexdec($value), + default => throw new ColorDecoderException('Failed to decode hex color'), + }; + }, $values); + + // normalize + $values = count($values) === 3 ? array_pad($values, 4, 255) : $values; + $values = array_map(fn(int $value): float => $value / 255, $values); + + return Rgb::colorFromNormalized($values); + } +} diff --git a/src/Colors/Rgb/Decoders/NamedColorDecoder.php b/src/Colors/Rgb/Decoders/NamedColorDecoder.php new file mode 100644 index 000000000..92c8a6b21 --- /dev/null +++ b/src/Colors/Rgb/Decoders/NamedColorDecoder.php @@ -0,0 +1,39 @@ +stringToColorName($input) === null ? false : true; + } + + /** + * Decode html color names. + */ + public function decode(mixed $input): ColorInterface + { + return parent::decode($this->stringToColorName($input)?->toHex()); + } + + private function stringToColorName(string $input): ?NamedColor + { + return NamedColor::tryFrom(strtolower($input)); + } +} diff --git a/src/Colors/Rgb/Decoders/StringColorDecoder.php b/src/Colors/Rgb/Decoders/StringColorDecoder.php new file mode 100644 index 000000000..85ba9f71b --- /dev/null +++ b/src/Colors/Rgb/Decoders/StringColorDecoder.php @@ -0,0 +1,78 @@ +[0-9]{1,3})([, ]) ?' . + '(?P[0-9]{1,3})\2 ?' . + '(?[0-9]{1,3})' . + '(?:(?:(?: ?\/ ?)|(?:[, ]) ?)' . + '(?(?:0\.[0-9]+)|1\.0|\.[0-9]+|[0-9]{1,3}%|1|0))?' . + ' ?\)$/i'; + + /** + * {@inheritdoc} + * + * @see DecoderInterface::supports() + */ + public function supports(mixed $input): bool + { + if (!is_string($input)) { + return false; + } + + if (preg_match('/^s?rgb/i', $input) !== 1) { + return false; + } + + return true; + } + + /** + * Decode rgb color strings. + * + * @throws InvalidArgumentException + * @throws ColorDecoderException + */ + public function decode(mixed $input): ColorInterface + { + if (preg_match(self::PATTERN, $input, $matches) !== 1) { + throw new InvalidArgumentException('Invalid rgb() color syntax "' . $input . '"'); + } + + // rgb values + $values = array_map(fn(string $value): int => match (strpos($value, '%')) { + false => intval(trim($value)), + default => intval(round(floatval(trim(str_replace('%', '', $value))) / 100 * 255)), + }, [$matches['r'], $matches['g'], $matches['b']]); + + // alpha value + if (array_key_exists('a', $matches)) { + $values[] = match (strpos($matches['a'], '%')) { + false => floatval(trim($matches['a'])), + default => floatval(trim(str_replace('%', '', $matches['a']))) / 100, + }; + } + + try { + return new Color(...$values); + } catch (InvalidArgumentException $e) { + throw new ColorDecoderException('Failed to decode RGB color string', previous: $e); + } + } +} diff --git a/src/Colors/Rgb/NamedColor.php b/src/Colors/Rgb/NamedColor.php new file mode 100644 index 000000000..d017f2728 --- /dev/null +++ b/src/Colors/Rgb/NamedColor.php @@ -0,0 +1,502 @@ +value; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toHex() + */ + public function toHex(bool $prefix = false): string + { + return ($prefix ? '#' : '') . match ($this) { + self::ALICEBLUE => 'f0f8ff', + self::ANTIQUEWHITE => 'faebd7', + self::AQUA => '00ffff', + self::AQUAMARINE => '7fffd4', + self::AZURE => 'f0ffff', + self::BEIGE => 'f5f5dc', + self::BISQUE => 'ffe4c4', + self::BLACK => '000000', + self::BLANCHEDALMOND => 'ffebcd', + self::BLUE => '0000ff', + self::BLUEVIOLET => '8a2be2', + self::BROWN => 'a52a2a', + self::BURLYWOOD => 'deb887', + self::CADETBLUE => '5f9ea0', + self::CHARTREUSE => '7fff00', + self::CHOCOLATE => 'd2691e', + self::CORAL => 'ff7f50', + self::CORNFLOWERBLUE => '6495ed', + self::CORNSILK => 'fff8dc', + self::CRIMSON => 'dc143c', + self::CYAN => '00ffff', + self::DARKBLUE => '00008b', + self::DARKCYAN => '008b8b', + self::DARKGRAY => 'a9a9a9', + self::DARKGREEN => '006400', + self::DARKKHAKI => 'bdb76b', + self::DARKMAGENTA => '8b008b', + self::DARKOLIVEGREEN => '556b2f', + self::DARKORANGE => 'ff8c00', + self::DARKORCHID => '9932cc', + self::DARKRED => '8b0000', + self::DARKSALMON => 'e9967a', + self::DARKSEAGREEN => '8fbc8f', + self::DARKSLATEBLUE => '483d8b', + self::DARKSLATEGRAY => '2f4f4f', + self::DARKTURQUOISE => '00ced1', + self::DARKVIOLET => '9400d3', + self::DEEPPINK => 'ff1493', + self::DEEPSKYBLUE => '00bfff', + self::DIMGRAY => '696969', + self::DODGERBLUE => '1e90ff', + self::FIREBRICK => 'b22222', + self::FLORALWHITE => 'fffaf0', + self::FORESTGREEN => '228b22', + self::FUCHSIA => 'ff00ff', + self::GAINSBORO => 'dcdcdc', + self::GHOSTWHITE => 'f8f8ff', + self::GOLD => 'ffd700', + self::GOLDENROD => 'daa520', + self::GRAY => '808080', + self::GREEN => '008000', + self::GREENYELLOW => 'adff2f', + self::HONEYDEW => 'f0fff0', + self::HOTPINK => 'ff69b4', + self::INDIANRED => 'cd5c5c', + self::INDIGO => '4b0082', + self::IVORY => 'fffff0', + self::KHAKI => 'f0e68c', + self::LAVENDER => 'e6e6fa', + self::LAVENDERBLUSH => 'fff0f5', + self::LAWNGREEN => '7cfc00', + self::LEMONCHIFFON => 'fffacd', + self::LIGHTBLUE => 'add8e6', + self::LIGHTCORAL => 'f08080', + self::LIGHTCYAN => 'e0ffff', + self::LIGHTGOLDENRODYELLOW => 'fafad2', + self::LIGHTGRAY => 'd3d3d3', + self::LIGHTGREEN => '90ee90', + self::LIGHTPINK => 'ffb6c1', + self::LIGHTSALMON => 'ffa07a', + self::LIGHTSEAGREEN => '20b2aa', + self::LIGHTSKYBLUE => '87cefa', + self::LIGHTSLATEGRAY => '778899', + self::LIGHTSTEELBLUE => 'b0c4de', + self::LIGHTYELLOW => 'ffffe0', + self::LIME => '00ff00', + self::LIMEGREEN => '32cd32', + self::LINEN => 'faf0e6', + self::MAGENTA => 'ff00ff', + self::MAROON => '800000', + self::MEDIUMAQUAMARINE => '66cdaa', + self::MEDIUMBLUE => '0000cd', + self::MEDIUMORCHID => 'ba55d3', + self::MEDIUMPURPLE => '9370db', + self::MEDIUMSEAGREEN => '3cb371', + self::MEDIUMSLATEBLUE => '7b68ee', + self::MEDIUMSPRINGGREEN => '00fa9a', + self::MEDIUMTURQUOISE => '48d1cc', + self::MEDIUMVIOLETRED => 'c71585', + self::MIDNIGHTBLUE => '191970', + self::MINTCREAM => 'f5fffa', + self::MISTYROSE => 'ffe4e1', + self::MOCCASIN => 'ffe4b5', + self::NAVAJOWHITE => 'ffdead', + self::NAVY => '000080', + self::OLDLACE => 'fdf5e6', + self::OLIVE => '808000', + self::OLIVEDRAB => '6b8e23', + self::ORANGE => 'ffa500', + self::ORANGERED => 'ff4500', + self::ORCHID => 'da70d6', + self::PALEGOLDENROD => 'eee8aa', + self::PALEGREEN => '98fb98', + self::PALETURQUOISE => 'afeeee', + self::PALEVIOLETRED => 'db7093', + self::PAPAYAWHIP => 'ffefd5', + self::PEACHPUFF => 'ffdab9', + self::PERU => 'cd853f', + self::PINK => 'ffc0cb', + self::PLUM => 'dda0dd', + self::POWDERBLUE => 'b0e0e6', + self::PURPLE => '800080', + self::RED => 'ff0000', + self::ROSYBROWN => 'bc8f8f', + self::ROYALBLUE => '4169e1', + self::SADDLEBROWN => '8b4513', + self::SALMON => 'fa8072', + self::SANDYBROWN => 'f4a460', + self::SEAGREEN => '2e8b57', + self::SEASHELL => 'fff5ee', + self::SIENNA => 'a0522d', + self::SILVER => 'c0c0c0', + self::SKYBLUE => '87ceeb', + self::SLATEBLUE => '6a5acd', + self::SLATEGRAY => '708090', + self::SNOW => 'fffafa', + self::SPRINGGREEN => '00ff7f', + self::STEELBLUE => '4682b4', + self::TAN => 'd2b48c', + self::TEAL => '008080', + self::THISTLE => 'd8bfd8', + self::TOMATO => 'ff6347', + self::TURQUOISE => '40e0d0', + self::VIOLET => 'ee82ee', + self::WHEAT => 'f5deb3', + self::WHITE => 'ffffff', + self::WHITESMOKE => 'f5f5f5', + self::YELLOW => 'ffff00', + self::YELLOWGREEN => '9acd32', + }; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::channels() + * + * @throws ColorException + */ + public function channels(): array + { + return $this->toRgbColor()->channels(); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::channel() + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public function channel(string $classname): ColorChannelInterface + { + return $this->toRgbColor()->channel($classname); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::alpha() + */ + public function alpha(): ColorChannelInterface + { + // @phpstan-ignore missingType.checkedException + return new Alpha(); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::toColorspace() + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public function toColorspace(string|ColorspaceInterface $colorspace): ColorInterface + { + return $this->toRgbColor()->toColorspace($colorspace); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::isGrayscale() + * + * @throws ColorException + */ + public function isGrayscale(): bool + { + return $this->toRgbColor()->isGrayscale(); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::isTransparent() + */ + public function isTransparent(): bool + { + return false; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::isClear() + */ + public function isClear(): bool + { + return false; + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::withTransparency() + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public function withTransparency(float $transparency): ColorInterface + { + return $this->toRgbColor()->withTransparency($transparency); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::withBrightness() + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public function withBrightness(int $level): ColorInterface + { + return $this->toRgbColor()->withBrightness($level); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::withSaturation() + * + * @throws InvalidArgumentException + * @throws ColorException + */ + public function withSaturation(int $level): ColorInterface + { + return $this->toRgbColor()->withSaturation($level); + } + + /** + * {@inheritdoc} + * + * @see ColorInterface::withInversion() + * + * @throws ColorException + */ + public function withInversion(): ColorInterface + { + return $this->toRgbColor()->withInversion(); + } + + /** + * Convert current named color to rgb color object. + * + * @throws ColorException + */ + private function toRgbColor(): RgbColor + { + $color = $this->colorspace()->importColor($this); + + return $color instanceof RgbColor + ? $color + : throw new ColorException('Failed to convert named color to rgb object'); + } +} diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 000000000..ec8fc7506 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,69 @@ +prepareOptions($options) as $name => $value) { + if (!property_exists($this, $name)) { + throw new InvalidArgumentException('Property ' . $name . ' does not exists for ' . $this::class); + } + + $this->{$name} = $value; + } + + return $this; + } + + /** + * This method makes it possible to call self::setOptions() with a single + * array instead of named parameters. + * + * @param array $options + * @return array + */ + private function prepareOptions(array $options): array + { + if ($options === []) { + return $options; + } + + if (count($options) > 1) { + return $options; + } + + if (!array_key_exists(0, $options)) { + return $options; + } + + if (!is_array($options[0])) { + return $options; + } + + return $options[0]; + } +} diff --git a/src/DataUri.php b/src/DataUri.php new file mode 100644 index 000000000..244d00850 --- /dev/null +++ b/src/DataUri.php @@ -0,0 +1,286 @@ +\w+\/[-+.\w]+)?" . + "(?P(;[-\w]+=[-\w]+)*)(?P;base64)?,(?P.*)/"; + + /** + * Media type of data uri output. + */ + protected ?string $mediaType = null; + + /** + * Parameters of data uri output. + * + * @var array + */ + protected array $parameters = []; + + /** + * Create new data uri instance. + * + * @param array $parameters + */ + public function __construct( + protected string|Stringable $data = '', + null|string|MediaType $mediaType = null, + array $parameters = [], + protected bool $base64 = true + ) { + $this->setMediaType($mediaType); + $this->setParameters($parameters); + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::create() + */ + public static function create( + string $data, + null|string|MediaType $mediaType = null, + array $parameters = [], + bool $base64 = true + ): self { + return new self( + data: $data, + mediaType: $mediaType, + parameters: $parameters, + base64: $base64, + ); + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::parse() + * + * @throws InvalidArgumentException + */ + public static function parse(string|Stringable $dataUriScheme): self + { + $result = preg_match(self::PATTERN, (string) $dataUriScheme, $matches); + + if ($result === false || $result === 0) { + throw new InvalidArgumentException('Invalid data uri scheme'); + } + + $isBase64Encoded = $matches['base64'] !== ''; + + $datauri = new self( + data: $isBase64Encoded ? base64_decode($matches['data'], strict: true) : rawurldecode($matches['data']), + mediaType: $matches['mediaType'], + base64: $isBase64Encoded, + ); + + if ($matches['parameters'] !== '') { + $parameters = explode(';', $matches['parameters']); + $parameters = array_filter($parameters, fn(string $value): bool => $value !== ''); + $parameters = array_map(fn(string $value): array => explode('=', $value), $parameters); + foreach ($parameters as $parameter) { + $datauri->setParameter(...$parameter); + } + } + + return $datauri; + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::data() + */ + public function data(): string + { + return (string) $this->data; + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::setData() + */ + public function setData(string|Stringable $data): self + { + $this->data = (string) $data; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::mediaType() + */ + public function mediaType(): ?string + { + return $this->mediaType; + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::setMediaType() + */ + public function setMediaType(null|string|MediaType $mediaType): self + { + $this->mediaType = $mediaType instanceof MediaType ? $mediaType->value : $mediaType; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::parameters() + */ + public function parameters(): array + { + return $this->parameters; + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::setParameters() + */ + public function setParameters(array $parameters): self + { + $this->parameters = $parameters; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::appendParameters() + */ + public function appendParameters(array $parameters): self + { + foreach ($parameters as $key => $value) { + $this->setParameter((string) $key, (string) $value); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::parameter() + */ + public function parameter(string $key): ?string + { + return $this->parameters[$key] ?? null; + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::setParameter() + */ + public function setParameter(string $key, string $value): self + { + $this->parameters[$key] = $value; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::charset() + */ + public function charset(): ?string + { + return $this->parameter('charset'); + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::setCharset() + */ + public function setCharset(string $charset): self + { + $this->setParameter('charset', $charset); + + return $this; + } + + /** + * Prepare data for output. + */ + private function encodedData(): string + { + return $this->base64 === true ? (string) base64_encode($this->data) : rawurlencode((string) $this->data); + } + + /** + * Prepare all set parameters for output. + */ + private function encodedParameters(): string + { + if (count($this->parameters) === 0 && $this->base64 === false) { + return ''; + } + + $parameters = array_map(function (mixed $key, mixed $value) { + return $key . '=' . $value; + }, array_keys($this->parameters), $this->parameters); + + $parameterString = count($parameters) > 0 ? ';' . implode(';', $parameters) : ''; + + if ($this->base64 === true) { + $parameterString .= ';base64'; + } + + return $parameterString; + } + + /** + * {@inheritdoc} + * + * @see DataUriInterface::toString() + */ + public function toString(): string + { + return 'data:' . $this->mediaType() . $this->encodedParameters() . ',' . $this->encodedData(); + } + + /** + * {@inheritdoc} + * + * @see Stringable::__toString() + */ + public function __toString(): string + { + return $this->toString(); + } + + /** + * Show debug info for the current data uri scheme. + * + * @return array + */ + public function __debugInfo(): array + { + return [ + 'mediaType' => $this->mediaType, + 'size' => strlen($this->data), + ]; + } +} diff --git a/src/Decoders/Base64ImageDecoder.php b/src/Decoders/Base64ImageDecoder.php new file mode 100644 index 000000000..6dffb2b8e --- /dev/null +++ b/src/Decoders/Base64ImageDecoder.php @@ -0,0 +1,12 @@ + + */ + protected function extractExifData(string $input): CollectionInterface + { + if (!function_exists('exif_read_data')) { + return new Collection(); + } + + try { + // source might be file path + $source = self::readableFilePathOrFail($input); + } catch (Throwable) { + try { + // source might be stream resource + $source = self::buildStreamOrFail($input); + } catch (RuntimeException) { + return new Collection(); + } + } + + try { + // extract exif data + $data = @exif_read_data($source, null, true); + if (is_resource($source)) { + fclose($source); + } + } catch (Exception) { + $data = []; + } + + return new Collection(is_array($data) ? $data : []); + } + + /** + * Decodes given base64 encoded data. + * + * @throws InvalidArgumentException + * @throws DecoderException + */ + protected function decodeBase64Data(mixed $input): string + { + if (!is_string($input) && !$input instanceof Stringable) { + throw new InvalidArgumentException( + 'Base64-encoded data must be either of type string or instance of Stringable', + ); + } + + $decoded = base64_decode((string) $input, true); + + if ($decoded === false) { + throw new DecoderException('Input is not valid Base64-encoded data'); + } + + if (base64_encode($decoded) !== str_replace(["\n", "\r"], '', (string) $input)) { + throw new DecoderException('Input is not valid Base64-encoded data'); + } + + return $decoded; + } +} diff --git a/src/Drivers/AbstractDriver.php b/src/Drivers/AbstractDriver.php new file mode 100644 index 000000000..6d9b9d1be --- /dev/null +++ b/src/Drivers/AbstractDriver.php @@ -0,0 +1,199 @@ +checkHealth(); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::config() + */ + public function config(): Config + { + return $this->config; + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::decodeImage() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + */ + public function decodeImage(mixed $input, ?array $decoders = null): ImageInterface + { + $decoders = $decoders === null ? InputHandler::IMAGE_DECODERS : $decoders; + + if (count($decoders) === 0) { + throw new InvalidArgumentException('No decoders in array'); + } + + try { + $result = InputHandler::usingDecoders($decoders, $this)->handle($input); + } catch (NotSupportedException) { + $type = is_object($input) ? $input::class : gettype($input); + throw new InvalidArgumentException('Unsupported image source type "' . $type . '"'); + } + + if (!$result instanceof ImageInterface) { + throw new ImageDecoderException('Result must be instance of ' . ImageInterface::class); + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::decodeColor() + * + * @throws InvalidArgumentException + * @throws ColorDecoderException + * @throws DriverException + */ + public function decodeColor(mixed $input, ?array $decoders = null): ColorInterface + { + $decoders = $decoders === null ? InputHandler::COLOR_DECODERS : $decoders; + + if (count($decoders) === 0) { + throw new InvalidArgumentException('No decoders in array'); + } + + try { + $result = InputHandler::usingDecoders($decoders, $this)->handle($input); + } catch (NotSupportedException) { + throw new ColorDecoderException('Unknown color format'); + } + + if (!$result instanceof ColorInterface) { + throw new ColorDecoderException('Result must be instance of ' . ColorInterface::class); + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::specializeModifier() + * + * @throws NotSupportedException + */ + public function specializeModifier(ModifierInterface $modifier): ModifierInterface + { + return $this->specialize($modifier); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::specializeAnalyzer() + * + * @throws NotSupportedException + */ + public function specializeAnalyzer(AnalyzerInterface $analyzer): AnalyzerInterface + { + return $this->specialize($analyzer); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::specializeEncoder() + * + * @throws NotSupportedException + */ + public function specializeEncoder(EncoderInterface $encoder): EncoderInterface + { + return $this->specialize($encoder); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::specializeDecoder() + * + * @throws NotSupportedException + */ + public function specializeDecoder(DecoderInterface $decoder): DecoderInterface + { + return $this->specialize($decoder); + } + + /** + * @throws NotSupportedException + */ + private function specialize( + ModifierInterface|AnalyzerInterface|EncoderInterface|DecoderInterface $object + ): ModifierInterface|AnalyzerInterface|EncoderInterface|DecoderInterface { + // return object directly if no specializing is possible + if (!$object instanceof SpecializableInterface) { + return $object; + } + + // return directly and only attach driver if object is already specialized + if ($object instanceof SpecializedInterface) { + $object->setDriver($this); + + return $object; + } + + // resolve classname for specializable object + $objectShortname = substr($object::class, (int) strrpos($object::class, '\\') + 1); + + $specializedClassname = implode("\\", [ + substr($this::class, 0, (int) strrpos($this::class, '\\')), // driver's namespace + match (true) { + $object instanceof ModifierInterface => 'Modifiers', + $object instanceof AnalyzerInterface => 'Analyzers', + $object instanceof EncoderInterface => 'Encoders', + $object instanceof DecoderInterface => 'Decoders', + }, + $objectShortname, + ]); + + // fail if driver specialized classname does not exists + if (!class_exists($specializedClassname)) { + throw new NotSupportedException( + "Class '" . $objectShortname . "' is not supported by " . $this->id() . " driver" + ); + } + + // create a driver specialized object with the specializable properties of generic object + $specialized = new $specializedClassname(...$object->specializationArguments()); + + // attach driver + return $specialized->setDriver($this); + } +} diff --git a/src/Drivers/AbstractEncoder.php b/src/Drivers/AbstractEncoder.php new file mode 100644 index 000000000..13bee1e06 --- /dev/null +++ b/src/Drivers/AbstractEncoder.php @@ -0,0 +1,78 @@ +encode($this); + } + + /** + * Build new stream, run callback with it and return result as encoded image. + * + * @throws InvalidArgumentException + * @throws StreamException + */ + protected function createEncodedImage(callable $callback, ?string $mediaType = null): EncodedImage + { + $stream = self::buildStreamOrFail(); + $callback($stream); + + return is_string($mediaType) ? new EncodedImage($stream, $mediaType) : new EncodedImage($stream); + } + + /** + * {@inheritdoc} + * + * @see EncoderInterface::setOptions() + * + * @throws InvalidArgumentException + */ + public function setOptions(mixed ...$options): self + { + foreach ($options as $key => $value) { + if (!property_exists($this, (string) $key)) { + throw new InvalidArgumentException( + 'Option $' . $key . ' does not exist on ' . $this::class, + ); + } + $this->{$key} = $value; + } + + return $this; + } +} diff --git a/src/Drivers/AbstractFontProcessor.php b/src/Drivers/AbstractFontProcessor.php new file mode 100644 index 000000000..09dbb9ae3 --- /dev/null +++ b/src/Drivers/AbstractFontProcessor.php @@ -0,0 +1,173 @@ +wrapTextBlock(new TextBlock($text), $font); + $pivot = $this->buildPivot($lines, $font, $position); + + $leading = $this->leading($font); + $blockWidth = $this->boxSize((string) $lines->longestLine(), $font)->width(); + + $x = $pivot->x(); + $y = $font->hasFile() ? $pivot->y() + $this->capHeight($font) : $pivot->y(); + $xAdjustment = 0; + + // adjust line positions according to alignment + $horizontalAlignment = $font->alignmentHorizontal(); + foreach ($lines as $line) { + $lineBoxSize = $this->boxSize((string) $line, $font); + $lineWidth = $lineBoxSize->width() + $lineBoxSize->pivot()->x(); + $xAdjustment = $horizontalAlignment === Alignment::LEFT ? 0 : $blockWidth - $lineWidth; + $xAdjustment = $horizontalAlignment === Alignment::RIGHT ? intval(round($xAdjustment)) : $xAdjustment; + $xAdjustment = $horizontalAlignment === Alignment::CENTER ? intval(round($xAdjustment / 2)) : $xAdjustment; + $position = new Point($x + $xAdjustment, $y); + $position->rotate($font->angle(), $pivot); + $line->setPosition($position); + $y += $leading; + } + + return $lines; + } + + /** + * {@inheritdoc} + * + * @see FontProcessorInterface::nativeFontSize() + */ + public function nativeFontSize(FontInterface $font): float + { + return $font->size(); + } + + /** + * {@inheritdoc} + * + * @see FontProcessorInterface::typographicalSize() + */ + public function typographicalSize(FontInterface $font): int + { + return $this->boxSize('Hy', $font)->height(); + } + + /** + * {@inheritdoc} + * + * @see FontProcessorInterface::capHeight() + */ + public function capHeight(FontInterface $font): int + { + return $this->boxSize('T', $font)->height(); + } + + /** + * {@inheritdoc} + * + * @see FontProcessorInterface::leading() + */ + public function leading(FontInterface $font): int + { + return intval(round($this->typographicalSize($font) * $font->lineHeight())); + } + + /** + * Reformat a text block by wrapping each line before the given maximum width. + */ + protected function wrapTextBlock(TextBlock $block, FontInterface $font): TextBlock + { + $newLines = []; + foreach ($block as $line) { + foreach ($this->wrapLine($line, $font) as $newLine) { + $newLines[] = $newLine; + } + } + + return $block->setLines($newLines); + } + + /** + * Check if a line exceeds the given maximum width and wrap it if necessary. + * The output will be an array of formatted lines that are all within the + * maximum width. + * + * @return array + */ + protected function wrapLine(Line $line, FontInterface $font): array + { + // no wrap width - no wrapping + if (is_null($font->wrapWidth())) { + return [$line]; + } + + $wrapped = []; + $formattedLine = new Line(); + + foreach ($line as $word) { + // calculate width of newly formatted line + $lineWidth = $this->boxSize(match ($formattedLine->count()) { + 0 => $word, + default => $formattedLine . ' ' . $word, + }, $font)->width(); + + // decide if word fits on current line or a new line must be created + if ($line->count() === 1 || $lineWidth <= $font->wrapWidth()) { + $formattedLine->add($word); + } else { + if ($formattedLine->count() !== 0) { + $wrapped[] = $formattedLine; + } + $formattedLine = new Line($word); + } + } + + $wrapped[] = $formattedLine; + + return $wrapped; + } + + /** + * Build pivot point of textblock according to the font settings and based on given position. + * + * @throws InvalidArgumentException + */ + protected function buildPivot(TextBlock $block, FontInterface $font, PointInterface $position): PointInterface + { + // bounding box + $box = new Size( + $this->boxSize((string) $block->longestLine(), $font)->width(), + $this->leading($font) * ($block->count() - 1) + $this->capHeight($font) + ); + + // set position + $box->setPivot($position); + + // alignment + $box->alignHorizontally($font->alignmentHorizontal()); + $box->alignVertically($font->alignmentVertical()); + $box->rotate($font->angle()); + + return $box->last(); + } +} diff --git a/src/Drivers/AbstractFrame.php b/src/Drivers/AbstractFrame.php new file mode 100644 index 000000000..71384c228 --- /dev/null +++ b/src/Drivers/AbstractFrame.php @@ -0,0 +1,25 @@ + + */ + public function __debugInfo(): array + { + return [ + 'delay' => $this->delay(), + 'left' => $this->offsetLeft(), + 'top' => $this->offsetTop(), + 'disposalMethod' => $this->disposalMethod(), + ]; + } +} diff --git a/src/Drivers/Gd/Analyzers/ColorspaceAnalyzer.php b/src/Drivers/Gd/Analyzers/ColorspaceAnalyzer.php new file mode 100644 index 000000000..73057f72a --- /dev/null +++ b/src/Drivers/Gd/Analyzers/ColorspaceAnalyzer.php @@ -0,0 +1,23 @@ +core()->native()); + } +} diff --git a/src/Drivers/Gd/Analyzers/PixelColorAnalyzer.php b/src/Drivers/Gd/Analyzers/PixelColorAnalyzer.php new file mode 100644 index 000000000..afa0ea772 --- /dev/null +++ b/src/Drivers/Gd/Analyzers/PixelColorAnalyzer.php @@ -0,0 +1,61 @@ +driver()->colorProcessor($image); + + return $this->colorAt($colorProcessor, $image->core()->frame($this->frame)); + } + + /** + * @throws InvalidArgumentException + * @throws AnalyzerException + */ + protected function colorAt(ColorProcessorInterface $processor, FrameInterface $frame): ColorInterface + { + $gd = $frame->native(); + $index = @imagecolorat($gd, $this->x, $this->y); + + if (!is_int($index)) { + throw new InvalidArgumentException( + 'The specified position (' . $this->x . ', ' . $this->y . ') is not within the image area', + ); + } + + try { + $index = imagecolorsforindex($gd, $index); + } catch (ValueError) { + throw new AnalyzerException( + 'The specified index is outside of the range', + ); + } + + return $processor->import($index); + } +} diff --git a/src/Drivers/Gd/Analyzers/PixelColorsAnalyzer.php b/src/Drivers/Gd/Analyzers/PixelColorsAnalyzer.php new file mode 100644 index 000000000..52a3e7eda --- /dev/null +++ b/src/Drivers/Gd/Analyzers/PixelColorsAnalyzer.php @@ -0,0 +1,30 @@ +driver()->colorProcessor($image); + + foreach ($image as $frame) { + $colors->push( + parent::colorAt($colorProcessor, $frame) + ); + } + + return $colors; + } +} diff --git a/src/Drivers/Gd/Analyzers/ResolutionAnalyzer.php b/src/Drivers/Gd/Analyzers/ResolutionAnalyzer.php new file mode 100644 index 000000000..2d58d7dfe --- /dev/null +++ b/src/Drivers/Gd/Analyzers/ResolutionAnalyzer.php @@ -0,0 +1,219 @@ +core()->native()); + + if (!is_array($result)) { + throw new AnalyzerException('Failed to read image resolution'); + } + + // GD returns 96x96 as resolution by default even if the image has no resolution. + // This is problematic because it is impossible to tell whether the image + // really has this resolution or whether it just corresponds to the default value. + // + // If GD's default resolution is returned here and the resolution is still unchanged + // we will make an attempt to find the resolution from origin. + if ($this->isGdDefaultResolution($result) && $image->core()->meta()->get('resolutionChanged') !== true) { + try { + $alternativeResoltion = $this->readResolutionFromOrigin($image->origin()); + } catch (Throwable) { + $alternativeResoltion = [96, 96]; + } + + $result = $alternativeResoltion !== $result ? $alternativeResoltion : $result; + } + + return new Resolution(...$result); + } + + /** + * @param array $resolution + */ + private function isGdDefaultResolution(array $resolution): bool + { + return intval($resolution[0] ?? 0) === 96 && intval($resolution[1] ?? 0) === 96; + } + + /** + * @throws AnalyzerException + * @throws InvalidArgumentException + * @throws StreamException + * @return array + */ + private function readResolutionFromOrigin(OriginInterface $origin): array + { + $handle = self::buildStreamOrFail(file_get_contents($origin->filePath())); + + try { + try { + return $this->resolutionFromJfifHeader($handle); + } catch (Throwable) { + # code ... + } + + try { + return $this->resolutionFromExifHeader($handle); + } catch (Throwable) { + # code ... + } + + try { + return $this->resolutionFromPngPhys($handle); + } catch (Throwable) { + # code ... + } + + throw new AnalyzerException('Unable to read resolution from path'); + } finally { + fclose($handle); + } + } + + /** + * @param resource $handle + * @throws AnalyzerException + * @return array + */ + private function resolutionFromJfifHeader($handle): array + { + // read first 20 bytes + rewind($handle); + $header = fread($handle, 20); + + // find the JFIF segment + $offset = strpos($header, 'JFIF'); + if ($offset === false) { + throw new AnalyzerException('Unable to read JFIF header'); + } + + // read bytes at known offsets relative to JFIF + $units = ord($header[$offset + 7]); + $x = unpack('n', substr($header, $offset + 8, 2))[1]; + $y = unpack('n', substr($header, $offset + 10, 2))[1]; + + if ($units === 2) { // unit is dots per cm → convert to DPI + return [round($x * 2.54), round($y * 2.54)]; + } + + return [$x, $y]; // unit is DPI or no unit + } + + /** + * @param resource $handle + * @throws MissingDependencyException + * @throws AnalyzerException + * @return array + */ + private function resolutionFromExifHeader($handle): array + { + if (!function_exists('exif_read_data')) { + throw new MissingDependencyException('Unable to read exif data'); + } + + rewind($handle); + $data = @exif_read_data($handle, null, true); + + if ($data === false) { + throw new AnalyzerException('Unable to read exif data'); + } + + if (isset($data['XResolution']) && isset($data['YResolution'])) { + $resolution = [$data['XResolution'], $data['YResolution']]; + } + + if (isset($data['IFD0']) && isset($data['IFD0']['XResolution']) && isset($data['IFD0']['YResolution'])) { + $resolution = [$data['IFD0']['XResolution'], $data['IFD0']['YResolution']]; + } + + if (!isset($resolution)) { + throw new AnalyzerException('Unable to read exif data'); + } + + return array_map(function (mixed $value): int|float { + if (strpos($value, '/') === false) { + return $value; + } + + $values = array_map(fn(string $value): int => intval($value), explode('/', $value)); + + if ($values[1] === 0) { + throw new AnalyzerException('Unable to read exif data, division by zero'); + } + + return $values[0] / $values[1]; + }, $resolution); + } + + /** + * @param resource $handle + * @throws AnalyzerException + * @return array + */ + private function resolutionFromPngPhys($handle): array + { + rewind($handle); + $signature = fread($handle, 8); + + // no PNG content + if ($signature !== "\x89PNG\x0D\x0A\x1A\x0A") { + throw new AnalyzerException('Input must be PNG format'); + } + + $marker = ''; + + while (!feof($handle)) { + $marker = strlen($marker) < 4 ? $marker . fread($handle, 1) : substr($marker, 1) . fread($handle, 1); + + // find pHYs chunk + if ($marker === 'pHYs') { + // find length + fseek($handle, -8, SEEK_CUR); + $length = fread($handle, 4); + $length = unpack('N', $length)[1]; + fseek($handle, 4, SEEK_CUR); + + // read data + $data = fread($handle, $length); + + $x = unpack('N', substr($data, 0, 4))[1]; + $y = unpack('N', substr($data, 4, 4))[1]; + + return [ + round($x * .0254), + round($y * .0254), + ]; + } + } + + return [0, 0]; + } +} diff --git a/src/Drivers/Gd/Analyzers/WidthAnalyzer.php b/src/Drivers/Gd/Analyzers/WidthAnalyzer.php new file mode 100644 index 000000000..9c6b793b2 --- /dev/null +++ b/src/Drivers/Gd/Analyzers/WidthAnalyzer.php @@ -0,0 +1,22 @@ +core()->native()); + } +} diff --git a/src/Drivers/Gd/Cloner.php b/src/Drivers/Gd/Cloner.php new file mode 100644 index 000000000..c6eea2e50 --- /dev/null +++ b/src/Drivers/Gd/Cloner.php @@ -0,0 +1,100 @@ +width() < 1 || $size->height() < 1) { + throw new InvalidArgumentException('Invalid image size'); + } + + // create new gd image with same size or new given size + $clone = imagecreatetruecolor($size->width(), $size->height()); + if ($clone === false) { + throw new DriverException('Failed to create new image while cloning'); + } + + // copy resolution to clone + $resolution = imageresolution($gd); + if (is_array($resolution) && array_key_exists(0, $resolution) && array_key_exists(1, $resolution)) { + imageresolution($clone, $resolution[0], $resolution[1]); + } + + // fill with background + $processor = new ColorProcessor(); + + imagefill($clone, 0, 0, $processor->export($background)); + imagealphablending($clone, true); + imagesavealpha($clone, true); + + // set background image as transparent if alpha channel value if color is below .5 + // comes into effect when the end format only supports binary transparency (like GIF) + if ($background->alpha()->value() < .5) { + imagecolortransparent($clone, $processor->export($background)); + } + + return $clone; + } + + /** + * Create a clone of an GdImage that is positioned on the specified background color. + * Possible transparent areas are mixed with this color. + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public static function cloneBlended(GdImage $gd, Color $background): GdImage + { + // create empty canvas with same size + $clone = static::cloneEmpty($gd, background: $background); + + // transfer actual image to clone + imagecopy($clone, $gd, 0, 0, 0, 0, imagesx($gd), imagesy($gd)); + + return $clone; + } +} diff --git a/src/Drivers/Gd/ColorProcessor.php b/src/Drivers/Gd/ColorProcessor.php new file mode 100644 index 000000000..0842bb68b --- /dev/null +++ b/src/Drivers/Gd/ColorProcessor.php @@ -0,0 +1,156 @@ +toColorspace($this->colorspace()); + + // gd only supports rgb so the channels can be accessed directly + $r = $color->channel(Red::class)->value(); + $g = $color->channel(Green::class)->value(); + $b = $color->channel(Blue::class)->value(); + $a = $color->channel(Alpha::class)->value(); + + try { + // convert alpha value to gd alpha + // ([opaque]1-0[transparent]) to ([opaque]0-127[transparent]) + $a = (int) round(self::convertRange($a, Alpha::min(), Alpha::max(), 127, 0)); + } catch (RuntimeException $e) { + throw new DriverException('Failed to export color', previous: $e); + } + + return ($a << 24) + ($r << 16) + ($g << 8) + $b; + } + + /** + * {@inheritdoc} + * + * @see ColorProcessorInterface::import() + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public function import(mixed $color): ColorInterface + { + if (!is_int($color) && !is_array($color)) { + throw new InvalidArgumentException('GD driver can only decode colors in integer or array format'); + } + + if (is_array($color)) { + // array conversion + if (!$this->isValidArrayColor($color)) { + throw new InvalidArgumentException( + 'GD driver can only decode array color format array{red: int, green: int, blue: int, alpha: int}', + ); + } + + $r = $color['red']; + $g = $color['green']; + $b = $color['blue']; + $a = $color['alpha']; + } else { + // integer conversion + $a = ($color >> 24) & 0xFF; + $r = ($color >> 16) & 0xFF; + $g = ($color >> 8) & 0xFF; + $b = $color & 0xFF; + } + + try { + // convert gd apha integer to intervention alpha integer + // ([opaque]0-127[transparent]) to ([opaque]1-0[transparent]) + $a = self::convertRange($a, 127, 0, 0, 1); + } catch (RuntimeException $e) { + throw new DriverException('Failed to import color', previous: $e); + } + + try { + return new Color($r, $g, $b, $a); + } catch (InvalidArgumentException $e) { + throw new DriverException('Failed to import color', previous: $e); + } + } + + /** + * Check if given array is valid color format + * array{red: int, green: int, blue: int, alpha: int} + * i.e. result of imagecolorsforindex() + * + * @param array $color + */ + private function isValidArrayColor(array $color): bool + { + if (!array_key_exists('red', $color)) { + return false; + } + + if (!array_key_exists('green', $color)) { + return false; + } + + if (!array_key_exists('blue', $color)) { + return false; + } + + if (!array_key_exists('alpha', $color)) { + return false; + } + + if (!is_int($color['red'])) { + return false; + } + + if (!is_int($color['green'])) { + return false; + } + + if (!is_int($color['blue'])) { + return false; + } + + if (!is_int($color['alpha'])) { + return false; + } + + return true; + } +} diff --git a/src/Drivers/Gd/Core.php b/src/Drivers/Gd/Core.php new file mode 100644 index 000000000..ca4562c73 --- /dev/null +++ b/src/Drivers/Gd/Core.php @@ -0,0 +1,145 @@ + $items + */ + public function __construct(array $items = []) + { + parent::__construct($items); + + $this->meta = new Collection(); + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::add() + */ + public function add(FrameInterface $frame): CoreInterface + { + $this->push($frame); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::native() + */ + public function native(): mixed + { + return $this->first()->native(); + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::setNative() + */ + public function setNative(mixed $native): CoreInterface + { + $this->clear()->push(new Frame($native)); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::frame() + * + * @throws InvalidArgumentException + */ + public function frame(int $position): FrameInterface + { + $frame = $this->at($position); + + if ($frame === null || $position < 0 || $position > $this->count()) { + throw new InvalidArgumentException('Frame #' . $position . ' could not be found in the image'); + } + + return $frame; + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::loops() + */ + public function loops(): int + { + return $this->loops; + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::setLoops() + */ + public function setLoops(int $loops): CoreInterface + { + $this->loops = $loops; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::first() + */ + public function first(): FrameInterface + { + return parent::first(); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::last() + */ + public function last(): FrameInterface + { + return parent::last(); + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::meta() + */ + public function meta(): CollectionInterface + { + return $this->meta; + } + + /** + * Clone instance + */ + public function __clone(): void + { + $this->meta = clone $this->meta; + + foreach ($this->items as $key => $frame) { + $this->items[$key] = clone $frame; + } + } +} diff --git a/src/Drivers/Gd/Decoders/AbstractDecoder.php b/src/Drivers/Gd/Decoders/AbstractDecoder.php new file mode 100644 index 000000000..12ca8ed28 --- /dev/null +++ b/src/Drivers/Gd/Decoders/AbstractDecoder.php @@ -0,0 +1,93 @@ +couldBeBase64Data($input); + } + + /** + * {@inheritdoc} + * + * @see DecoderInterface::decode() + */ + public function decode(mixed $input): ImageInterface + { + try { + $data = $this->decodeBase64Data($input); + } catch (DecoderException) { + throw new ImageDecoderException('Unable to Base64-decode image from string'); + } + + try { + return parent::decode($data); + } catch (DecoderException) { + throw new ImageDecoderException('Base64-encoded data contains unsupported image type'); + } + } +} diff --git a/src/Drivers/Gd/Decoders/BinaryImageDecoder.php b/src/Drivers/Gd/Decoders/BinaryImageDecoder.php new file mode 100644 index 000000000..469e805d2 --- /dev/null +++ b/src/Drivers/Gd/Decoders/BinaryImageDecoder.php @@ -0,0 +1,99 @@ +couldBeBinaryData($input); + } + + /** + * {@inheritdoc} + * + * @see DecoderInterface::decode() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + * @throws StateException + * @throws NotSupportedException + */ + public function decode(mixed $input): ImageInterface + { + if (!is_string($input) && !$input instanceof Stringable) { + throw new InvalidArgumentException( + 'Image source must be binary data of type string or instance of ' . Stringable::class, + ); + } + + $input = (string) $input; + + if ($input === '') { + throw new InvalidArgumentException('Unable to decode binary data from empty string'); + } + + return $this->isGifFormat($input) ? $this->decodeGif($input) : $this->decodeBinary($input); + } + + /** + * Decode image from given binary data + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + * @throws StateException + * @throws NotSupportedException + */ + private function decodeBinary(string $input): ImageInterface + { + $gd = @imagecreatefromstring($input); + + if ($gd === false) { + throw new ImageDecoderException('Failed to decode unsupported image format from binary data'); + } + + // create image instance + $image = parent::decode($gd); + + // get media type + $mediaType = $this->mediaTypeByBinary($input); + + // extract & set exif data for appropriate formats + if (in_array($mediaType->format(), [Format::JPEG, Format::TIFF])) { + $image->setExif($this->extractExifData($input)); + } + + // set mediaType on origin + $image->origin()->setMediaType($mediaType); + + // adjust image orientation + if ($this->driver()->config()->autoOrientation) { + $image->modify(new OrientModifier()); + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Decoders/DataUriImageDecoder.php b/src/Drivers/Gd/Decoders/DataUriImageDecoder.php new file mode 100644 index 000000000..7d0c7fc75 --- /dev/null +++ b/src/Drivers/Gd/Decoders/DataUriImageDecoder.php @@ -0,0 +1,65 @@ +couldBeDataUrl($input); + } + + /** + * {@inheritdoc} + * + * @see DecoderInterface::decode() + * + * @throws InvalidArgumentException + * @throws DriverException + * @throws ImageDecoderException + * @throws StateException + * @throws NotSupportedException + */ + public function decode(mixed $input): ImageInterface + { + if ($input instanceof DataUri) { + try { + return parent::decode($input->data()); + } catch (DecoderException) { + throw new ImageDecoderException('Data Uri contains unsupported image type'); + } + } + + if (!is_string($input)) { + throw new InvalidArgumentException( + 'Image source must be data uri scheme of type string or ' . DataUri::class, + ); + } + + try { + return parent::decode(DataUri::parse($input)->data()); + } catch (DecoderException) { + throw new ImageDecoderException('Data Uri contains unsupported image type'); + } + } +} diff --git a/src/Drivers/Gd/Decoders/EncodedImageObjectDecoder.php b/src/Drivers/Gd/Decoders/EncodedImageObjectDecoder.php new file mode 100644 index 000000000..7a144b85d --- /dev/null +++ b/src/Drivers/Gd/Decoders/EncodedImageObjectDecoder.php @@ -0,0 +1,52 @@ +toString()); + } catch (DecoderException) { + throw new ImageDecoderException(EncodedImage::class . ' contains unsupported image type'); + } + } +} diff --git a/src/Drivers/Gd/Decoders/FilePathImageDecoder.php b/src/Drivers/Gd/Decoders/FilePathImageDecoder.php new file mode 100644 index 000000000..08e8e2be2 --- /dev/null +++ b/src/Drivers/Gd/Decoders/FilePathImageDecoder.php @@ -0,0 +1,119 @@ +couldBeFilePath($input); + } + + /** + * {@inheritdoc} + * + * @see DecoderInterface::decode() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + * @throws StateException + * @throws FileNotFoundException + * @throws FileNotReadableException + * @throws DirectoryNotFoundException + */ + public function decode(mixed $input): ImageInterface + { + // make sure path is valid + $path = self::readableFilePathOrFail($input); + + try { + // detect media (mime) type + $mediaType = $this->mediaTypeByFilePath($path); + } catch (Throwable) { + throw new ImageDecoderException('File contains unsupported image format'); + } + + $image = match ($mediaType->format()) { + // gif files might be animated and therefore cannot + // be handled by the standard GD decoder. + Format::GIF => $this->decodeGif($path), + default => $this->decodeDefault($path, $mediaType), + }; + + // set file path & mediaType on origin + $image->origin()->setFilePath($path); + $image->origin()->setMediaType($mediaType); + + // extract exif for the appropriate formats + if ($mediaType->format() === Format::JPEG) { + $image->setExif($this->extractExifData($path)); + } + + // adjust image orientation + if ($this->driver()->config()->autoOrientation) { + $image->modify(new OrientModifier()); + } + + return $image; + } + + /** + * Try to decode data from file path as given image format + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws StateException + * @throws DriverException + */ + private function decodeDefault(string $path, MediaType $mediaType): ImageInterface + { + $gdImage = match ($mediaType->format()) { + Format::JPEG => @imagecreatefromjpeg($path), + Format::WEBP => @imagecreatefromwebp($path), + Format::PNG => @imagecreatefrompng($path), + Format::AVIF => @imagecreatefromavif($path), + Format::BMP => @imagecreatefrombmp($path), + default => throw new ImageDecoderException('File contains unsupported image format'), + }; + + if ($gdImage === false) { + throw new ImageDecoderException( + 'Failed to decode data from file "' . $path . '" as image format "' . $mediaType->value . '"', + ); + } + + try { + return parent::decode($gdImage); + } catch (DecoderException) { + throw new ImageDecoderException( + 'Failed to decode data from file "' . $path . '" as image format "' . $mediaType->value . '"', + ); + } + } +} diff --git a/src/Drivers/Gd/Decoders/NativeObjectDecoder.php b/src/Drivers/Gd/Decoders/NativeObjectDecoder.php new file mode 100644 index 000000000..7be014eef --- /dev/null +++ b/src/Drivers/Gd/Decoders/NativeObjectDecoder.php @@ -0,0 +1,117 @@ +driver(), + new Core([ + new Frame($input) + ]) + ); + } + + /** + * Decode image from given GIF source which can be either a file path or binary data. + * + * Depending on the configuration, this is taken over by the native GD function + * or, if animations are required, by our own extended decoder. + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + * @throws StateException + */ + protected function decodeGif(mixed $input): ImageInterface + { + // create non-animated image depending on config + if ($this->driver()->config()->decodeAnimation === false) { + $native = $this->isGifFormat($input) ? @imagecreatefromstring($input) : @imagecreatefromgif($input); + + if ($native === false) { + throw new ImageDecoderException('Failed to decode GIF format'); + } + + $image = self::decode($native); + $image->origin()->setMediaType('image/gif'); + + return $image; + } + + try { + // create empty core + $core = new Core(); + + // add frames to core + $splitter = GifSplitter::decode($input) + ->split() + ->flatten() + ->each(function (GdImage $native, int $delay) use ($core): void { + $core->push(new Frame($native, $delay / 100)); + }); + + // set loops on core + $core->setLoops($splitter->loops()); + } catch (GifException $e) { + throw new ImageDecoderException('Failed to decode GIF format', previous: $e); + } + + // create (possibly) animated image + $image = new Image($this->driver(), $core); + + // set media type + $image->origin()->setMediaType('image/gif'); + + return $image; + } +} diff --git a/src/Drivers/Gd/Decoders/SplFileInfoImageDecoder.php b/src/Drivers/Gd/Decoders/SplFileInfoImageDecoder.php new file mode 100644 index 000000000..1a5c9be4f --- /dev/null +++ b/src/Drivers/Gd/Decoders/SplFileInfoImageDecoder.php @@ -0,0 +1,59 @@ +'); + } + + // build new transparent GDImage + $data = imagecreatetruecolor($width, $height); + if (!$data instanceof GDImage) { + throw new DriverException('Failed to create new image'); + } + + imagesavealpha($data, true); + $background = imagecolorallocatealpha($data, 255, 255, 255, 127); + + imagealphablending($data, false); + imagefill($data, 0, 0, $background); + imagecolortransparent($data, $background); + imageresolution($data, 72, 72); + + return new Image($this, new Core([new Frame($data)])); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::createCore() + */ + public function createCore(array $frames): CoreInterface + { + return new Core($frames); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::colorProcessor() + */ + public function colorProcessor(ImageInterface $image): ColorProcessorInterface + { + return new ColorProcessor(); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::fontProcessor() + */ + public function fontProcessor(): FontProcessorInterface + { + return new FontProcessor(); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::supports() + */ + public function supports(string|Format|FileExtension|MediaType $identifier): bool + { + return match (Format::tryCreate($identifier)) { + Format::JPEG => boolval(imagetypes() & IMG_JPEG), + Format::WEBP => boolval(imagetypes() & IMG_WEBP), + Format::GIF => boolval(imagetypes() & IMG_GIF), + Format::PNG => boolval(imagetypes() & IMG_PNG), + Format::AVIF => boolval(imagetypes() & IMG_AVIF), + Format::BMP => boolval(imagetypes() & IMG_BMP), + default => false, + }; + } + + /** + * Return version of GD library + */ + public function version(): string + { + return gd_info()['GD Version']; + } +} diff --git a/src/Drivers/Gd/Encoders/AvifEncoder.php b/src/Drivers/Gd/Encoders/AvifEncoder.php new file mode 100644 index 000000000..0b09a89b9 --- /dev/null +++ b/src/Drivers/Gd/Encoders/AvifEncoder.php @@ -0,0 +1,30 @@ +createEncodedImage(function ($stream) use ($image): void { + imageavif($image->core()->native(), $stream, $this->quality); + }, 'image/avif'); + } +} diff --git a/src/Drivers/Gd/Encoders/BmpEncoder.php b/src/Drivers/Gd/Encoders/BmpEncoder.php new file mode 100644 index 000000000..145abe9e3 --- /dev/null +++ b/src/Drivers/Gd/Encoders/BmpEncoder.php @@ -0,0 +1,30 @@ +createEncodedImage(function ($stream) use ($image): void { + imagebmp($image->core()->native(), $stream, false); + }, 'image/bmp'); + } +} diff --git a/src/Drivers/Gd/Encoders/GifEncoder.php b/src/Drivers/Gd/Encoders/GifEncoder.php new file mode 100644 index 000000000..adc12a6cc --- /dev/null +++ b/src/Drivers/Gd/Encoders/GifEncoder.php @@ -0,0 +1,75 @@ +isAnimated()) { + return $this->encodeAnimated($image); + } + + $gd = Cloner::clone($image->core()->native()); + + return $this->createEncodedImage(function ($stream) use ($gd): void { + imageinterlace($gd, $this->interlaced); + imagegif($gd, $stream); + }, 'image/gif'); + } + + /** + * @throws InvalidArgumentException + * @throws EncoderException + * @throws DriverException + */ + protected function encodeAnimated(ImageInterface $image): EncodedImageInterface + { + try { + $builder = GifBuilder::canvas( + $image->width(), + $image->height() + ); + + foreach ($image as $frame) { + $builder->addFrame( + source: $this->encode($frame->toImage($image->driver()))->toStream(), + delay: $frame->delay(), + interlaced: $this->interlaced + ); + } + + $builder->setLoops($image->loops()); + + return new EncodedImage($builder->encode(), 'image/gif'); + } catch (GifException | FilesystemException $e) { + throw new EncoderException('Failed to encode image to GIF format', previous: $e); + } + } +} diff --git a/src/Drivers/Gd/Encoders/JpegEncoder.php b/src/Drivers/Gd/Encoders/JpegEncoder.php new file mode 100644 index 000000000..4becad279 --- /dev/null +++ b/src/Drivers/Gd/Encoders/JpegEncoder.php @@ -0,0 +1,54 @@ +driver()->decodeColor( + $this->driver()->config()->backgroundColor + )->toColorspace(Rgb::class); + + + if (!$backgroundColor instanceof RgbColor) { + throw new ModifierException('Failed to normalize background color to rgb color space'); + } + + $output = Cloner::cloneBlended( + $image->core()->native(), + background: $backgroundColor + ); + + return $this->createEncodedImage(function ($stream) use ($output): void { + imageinterlace($output, $this->progressive); + imagejpeg($output, $stream, $this->quality); + }, 'image/jpeg'); + } +} diff --git a/src/Drivers/Gd/Encoders/PngEncoder.php b/src/Drivers/Gd/Encoders/PngEncoder.php new file mode 100644 index 000000000..c8355e9c8 --- /dev/null +++ b/src/Drivers/Gd/Encoders/PngEncoder.php @@ -0,0 +1,55 @@ +prepareOutput($image); + + return $this->createEncodedImage(function ($stream) use ($output): void { + imageinterlace($output, $this->interlaced); + imagepng($output, $stream, -1); + }, 'image/png'); + } + + /** + * Prepare given image instance for PNG format output according to encoder settings + * + * @throws InvalidArgumentException + * @throws DriverException + */ + private function prepareOutput(ImageInterface $image): GdImage + { + if ($this->indexed) { + $output = clone $image; + $output->reduceColors(256); + + return $output->core()->native(); + } + + return Cloner::clone($image->core()->native()); + } +} diff --git a/src/Drivers/Gd/Encoders/WebpEncoder.php b/src/Drivers/Gd/Encoders/WebpEncoder.php new file mode 100644 index 000000000..a32633f41 --- /dev/null +++ b/src/Drivers/Gd/Encoders/WebpEncoder.php @@ -0,0 +1,32 @@ +quality === 100 && defined('IMG_WEBP_LOSSLESS') ? IMG_WEBP_LOSSLESS : $this->quality; + + return $this->createEncodedImage(function ($stream) use ($image, $quality): void { + imagewebp($image->core()->native(), $stream, $quality); + }, 'image/webp'); + } +} diff --git a/src/Drivers/Gd/FontProcessor.php b/src/Drivers/Gd/FontProcessor.php new file mode 100644 index 000000000..73232ed67 --- /dev/null +++ b/src/Drivers/Gd/FontProcessor.php @@ -0,0 +1,106 @@ +hasFile()) { + // font size to gd's internal fonts (1-5) + $gdFont = (int) $font->size(); + + // calculate box size from gd font + $box = new Size(0, 0); + $chars = mb_strlen($text); + if ($chars > 0) { + $box->setWidth( + $chars * $this->gdCharacterWidth($gdFont) + ); + $box->setHeight( + $this->gdCharacterHeight($gdFont) + ); + } + return $box; + } + + // calculate box size from ttf font file with angle 0 + $box = imageftbbox( + size: $this->nativeFontSize($font), + angle: 0, + font_filename: $font->filepath(), + string: $text, + ); + + if ($box === false) { + throw new DriverException('Unable to calculate box size of font'); + } + + // build size from points + return new Size( + width: intval(abs($box[6] - $box[4])), // difference of upper-left-x and upper-right-x + height: intval(abs($box[7] - $box[1])), // difference if upper-left-y and lower-left-y + pivot: new Point($box[6], $box[7]), // position of upper-left corner + ); + } + + /** + * {@inheritdoc} + * + * @see FontProcessorInterface::nativeFontSize() + */ + public function nativeFontSize(FontInterface $font): float + { + return floatval(round($font->size() * .76, 6)); + } + + /** + * {@inheritdoc} + * + * @see FontProcessorInterface::leading() + */ + public function leading(FontInterface $font): int + { + return (int) round(parent::leading($font) * .8); + } + + /** + * Return width of a single character + */ + protected function gdCharacterWidth(int $gdfont): int + { + return $gdfont + 4; + } + + /** + * Return height of a single character + */ + protected function gdCharacterHeight(int $gdfont): int + { + return match ($gdfont) { + 2, 3 => 14, + 4, 5 => 16, + default => 8, + }; + } +} diff --git a/src/Drivers/Gd/Frame.php b/src/Drivers/Gd/Frame.php new file mode 100644 index 000000000..0877e3e6b --- /dev/null +++ b/src/Drivers/Gd/Frame.php @@ -0,0 +1,202 @@ +native = $native; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FrameInterface::native() + */ + public function native(): GdImage + { + return $this->native; + } + + /** + * {@inheritdoc} + * + * @see FrameInterface::size() + * + * @throws InvalidArgumentException + */ + public function size(): SizeInterface + { + return new Size(imagesx($this->native), imagesy($this->native)); + } + + /** + * {@inheritdoc} + * + * @see FrameInterface::delay() + */ + public function delay(): float + { + return $this->delay; + } + + /** + * {@inheritdoc} + * + * @see FrameInterface::setDelay() + */ + public function setDelay(float $delay): FrameInterface + { + $this->delay = $delay; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FrameInterface::disposalMethod() + */ + public function disposalMethod(): int + { + return $this->disposalMethod; + } + + /** + * {@inheritdoc} + * + * @see FrameInterface::setDisposalMethod() + * + * @throws InvalidArgumentException + */ + public function setDisposalMethod(int $method): FrameInterface + { + if (!in_array($method, [0, 1, 2, 3])) { + throw new InvalidArgumentException('Value for disposal method "$method" must be 0, 1, 2 or 3'); + } + + $this->disposalMethod = $method; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FrameInterface::setOffset() + */ + public function setOffset(int $left, int $top): FrameInterface + { + $this->offsetLeft = $left; + $this->offsetTop = $top; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FrameInterface::offsetLeft() + */ + public function offsetLeft(): int + { + return $this->offsetLeft; + } + + /** + * {@inheritdoc} + * + * @see FrameInterface::setOffsetLeft() + */ + public function setOffsetLeft(int $offset): FrameInterface + { + $this->offsetLeft = $offset; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FrameInterface::offsetTop() + */ + public function offsetTop(): int + { + return $this->offsetTop; + } + + /** + * {@inheritdoc} + * + * @see FrameInterface::setOffsetTop() + */ + public function setOffsetTop(int $offset): FrameInterface + { + $this->offsetTop = $offset; + + return $this; + } + + /** + * This workaround helps cloning GdImages which is currently not possible. + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public function __clone(): void + { + $this->native = Cloner::clone($this->native); + } +} diff --git a/src/Drivers/Gd/Modifiers/BlurModifier.php b/src/Drivers/Gd/Modifiers/BlurModifier.php new file mode 100644 index 000000000..ff99e9b19 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/BlurModifier.php @@ -0,0 +1,36 @@ +level; $i++) { + $result = imagefilter($frame->native(), IMG_FILTER_GAUSSIAN_BLUR); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to process blur effect', + ); + } + } + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/BrightnessModifier.php b/src/Drivers/Gd/Modifiers/BrightnessModifier.php new file mode 100644 index 000000000..41319d28d --- /dev/null +++ b/src/Drivers/Gd/Modifiers/BrightnessModifier.php @@ -0,0 +1,34 @@ +native(), IMG_FILTER_BRIGHTNESS, intval($this->level * 2.55)); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set image brightness', + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/ColorizeModifier.php b/src/Drivers/Gd/Modifiers/ColorizeModifier.php new file mode 100644 index 000000000..b045ed166 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/ColorizeModifier.php @@ -0,0 +1,37 @@ +red * 2.55); + $green = (int) round($this->green * 2.55); + $blue = (int) round($this->blue * 2.55); + + foreach ($image as $frame) { + $result = imagefilter($frame->native(), IMG_FILTER_COLORIZE, $red, $green, $blue); + if ($result === false) { + throw new ModifierException('Failed to apply colorize effect'); + } + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/ColorspaceModifier.php b/src/Drivers/Gd/Modifiers/ColorspaceModifier.php new file mode 100644 index 000000000..61bcb5a6b --- /dev/null +++ b/src/Drivers/Gd/Modifiers/ColorspaceModifier.php @@ -0,0 +1,32 @@ +targetColorspace() instanceof RgbColorspace) { + throw new NotSupportedException( + 'Only RGB colorspace is supported by GD driver' + ); + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/ContainDownModifier.php b/src/Drivers/Gd/Modifiers/ContainDownModifier.php new file mode 100644 index 000000000..223a0bd98 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/ContainDownModifier.php @@ -0,0 +1,27 @@ +size() + ->containDown( + $this->width, + $this->height + ) + ->alignPivotTo( + $this->resizeSize($image), + $this->alignment + ); + } +} diff --git a/src/Drivers/Gd/Modifiers/ContainModifier.php b/src/Drivers/Gd/Modifiers/ContainModifier.php new file mode 100644 index 000000000..5bc754ce9 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/ContainModifier.php @@ -0,0 +1,103 @@ +cropSize($image); + $resize = $this->resizeSize($image); + $backgroundColor = $this->backgroundColor()->toColorspace(Rgb::class); + + if (!$backgroundColor instanceof RgbColor) { + throw new ModifierException('Failed to normalize background color to RGB color space'); + } + + foreach ($image as $frame) { + $this->modify($frame, $crop, $resize, $backgroundColor); + } + + return $image; + } + + /** + * @throws InvalidArgumentException + * @throws ModifierException + * @throws DriverException + */ + private function modify( + FrameInterface $frame, + SizeInterface $crop, + SizeInterface $resize, + RgbColor $backgroundColor + ): void { + // create new gd image + $modified = Cloner::cloneEmpty($frame->native(), $resize, $backgroundColor); + + // make image area transparent to keep transparency + // even if background-color is set + $transparent = imagecolorallocatealpha( + $modified, + $backgroundColor->red()->value(), + $backgroundColor->green()->value(), + $backgroundColor->blue()->value(), + 127, + ); + + imagealphablending($modified, false); // do not blend / just overwrite + + imagecolortransparent($modified, $transparent); + imagefilledrectangle( + $modified, + $crop->pivot()->x(), + $crop->pivot()->y(), + $crop->pivot()->x() + $crop->width() - 1, + $crop->pivot()->y() + $crop->height() - 1, + $transparent + ); + + // copy image from original with background alpha + imagealphablending($modified, true); + imagecopyresampled( + $modified, + $frame->native(), + $crop->pivot()->x(), + $crop->pivot()->y(), + 0, + 0, + $crop->width(), + $crop->height(), + $frame->size()->width(), + $frame->size()->height() + ); + + // set new content as resource + $frame->setNative($modified); + } +} diff --git a/src/Drivers/Gd/Modifiers/ContrastModifier.php b/src/Drivers/Gd/Modifiers/ContrastModifier.php new file mode 100644 index 000000000..976b3038f --- /dev/null +++ b/src/Drivers/Gd/Modifiers/ContrastModifier.php @@ -0,0 +1,32 @@ +native(), IMG_FILTER_CONTRAST, ($this->level * -1)); + if ($result === false) { + throw new ModifierException('Failed to set image contrast'); + } + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/CoverDownModifier.php b/src/Drivers/Gd/Modifiers/CoverDownModifier.php new file mode 100644 index 000000000..2d2218dc9 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/CoverDownModifier.php @@ -0,0 +1,18 @@ +resizeDown($this->width, $this->height); + } +} diff --git a/src/Drivers/Gd/Modifiers/CoverModifier.php b/src/Drivers/Gd/Modifiers/CoverModifier.php new file mode 100644 index 000000000..b7746cb60 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/CoverModifier.php @@ -0,0 +1,67 @@ +cropSize($image); + $resize = $this->resizeSize($crop); + + foreach ($image as $frame) { + $this->modifyFrame($frame, $crop, $resize); + } + + return $image; + } + + /** + * @throws InvalidArgumentException + * @throws ModifierException + * @throws DriverException + */ + protected function modifyFrame(FrameInterface $frame, SizeInterface $crop, SizeInterface $resize): void + { + // create new image + $modified = Cloner::cloneEmpty($frame->native(), $resize); + + // copy content from resource + imagecopyresampled( + $modified, + $frame->native(), + 0, + 0, + $crop->pivot()->x(), + $crop->pivot()->y(), + $resize->width(), + $resize->height(), + $crop->width(), + $crop->height() + ); + + // set new content as resource + $frame->setNative($modified); + } +} diff --git a/src/Drivers/Gd/Modifiers/CropModifier.php b/src/Drivers/Gd/Modifiers/CropModifier.php new file mode 100644 index 000000000..284a72bb2 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/CropModifier.php @@ -0,0 +1,93 @@ +size(); + $crop = $this->crop($image); + $background = $this->backgroundColor()->toColorspace(RgbColorspace::class); + + if (!$background instanceof RgbColor) { + throw new ModifierException('Failed to normalize background color to RGB color space'); + } + + foreach ($image as $frame) { + $this->cropFrame($frame, $originalSize, $crop, $background); + } + + return $image; + } + + /** + * @throws InvalidArgumentException + * @throws ModifierException + * @throws DriverException + */ + private function cropFrame( + FrameInterface $frame, + SizeInterface $originalSize, + SizeInterface $resizeTo, + RgbColor $background + ): void { + // create new image with transparent background + $modified = Cloner::cloneEmpty($frame->native(), $resizeTo, $background); + + // define offset + $offsetX = $resizeTo->pivot()->x() + $this->x; + $offsetY = $resizeTo->pivot()->y() + $this->y; + + // define target width & height + $targetWidth = min($resizeTo->width(), $originalSize->width()); + $targetHeight = min($resizeTo->height(), $originalSize->height()); + $targetWidth = $targetWidth < $originalSize->width() ? $targetWidth + $offsetX : $targetWidth; + $targetHeight = $targetHeight < $originalSize->height() ? $targetHeight + $offsetY : $targetHeight; + + // don't alpha blend for copy operation to keep transparent areas of original image + imagealphablending($modified, false); + + // copy content from resource + imagecopyresampled( + $modified, + $frame->native(), + $offsetX * -1, + $offsetY * -1, + 0, + 0, + $targetWidth, + $targetHeight, + $targetWidth, + $targetHeight + ); + + // set new content as resource + $frame->setNative($modified); + } +} diff --git a/src/Drivers/Gd/Modifiers/DrawBezierModifier.php b/src/Drivers/Gd/Modifiers/DrawBezierModifier.php new file mode 100644 index 000000000..9b9e0c6bf --- /dev/null +++ b/src/Drivers/Gd/Modifiers/DrawBezierModifier.php @@ -0,0 +1,270 @@ +drawable->count() !== 3 && $this->drawable->count() !== 4) { + throw new InvalidArgumentException('You must specify either 3 or 4 points to create a bezier curve'); + } + + [$polygon, $polygonBorderSegments] = $this->calculateBezierPoints(); + + if ($this->drawable->hasBackgroundColor() || $this->drawable->hasBorder()) { + $result = imagealphablending($frame->native(), true); + $this->abortUnless($result, 'Unable to set alpha blending'); + + $result = imageantialias($frame->native(), true); + $this->abortUnless($result, 'Unable to set image antialias option'); + } + + if ($this->drawable->hasBackgroundColor()) { + $backgroundColor = $this->driver()->colorProcessor($image)->export( + $this->backgroundColor() + ); + + $result = imagesetthickness($frame->native(), 0); + $this->abortUnless($result, 'Unable to set line thickness'); + + $result = imagefilledpolygon( + $frame->native(), + $polygon, + $backgroundColor + ); + + $this->abortUnless($result, 'Unable to draw line on image'); + } + + if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 0) { + $borderColor = $this->driver()->colorProcessor($image)->export( + $this->borderColor() + ); + + if ($this->drawable->borderSize() === 1) { + $result = imagesetthickness($frame->native(), $this->drawable->borderSize()); + $this->abortUnless($result, 'Unable to set line thickness'); + + $count = count($polygon); + for ($i = 0; $i < $count; $i += 2) { + if (array_key_exists($i + 2, $polygon) && array_key_exists($i + 3, $polygon)) { + $result = imageline( + $frame->native(), + $polygon[$i], + $polygon[$i + 1], + $polygon[$i + 2], + $polygon[$i + 3], + $borderColor + ); + + $this->abortUnless($result, 'Unable to draw line on image'); + } + } + } else { + $polygonBorderSegmentsTotal = count($polygonBorderSegments); + + for ($i = 0; $i < $polygonBorderSegmentsTotal; $i += 1) { + $result = imagefilledpolygon( + $frame->native(), + $polygonBorderSegments[$i], + $borderColor + ); + + $this->abortUnless($result, 'Unable to draw line on image'); + } + } + } + } + + return $image; + } + + /** + * Calculate interpolation points for quadratic beziers using the Bernstein polynomial form + * + * @return array{'x': float, 'y': float} + */ + private function calculateQuadraticBezierInterpolationPoint(float $t = 0.05): array + { + $remainder = 1 - $t; + $controlPoint1Multiplier = $remainder * $remainder; + $controlPoint2Multiplier = $remainder * $t * 2; + $controlPoint3Multiplier = $t * $t; + + $x = ( + $this->drawable->first()->x() * $controlPoint1Multiplier + + $this->drawable->second()->x() * $controlPoint2Multiplier + + $this->drawable->last()->x() * $controlPoint3Multiplier + ); + $y = ( + $this->drawable->first()->y() * $controlPoint1Multiplier + + $this->drawable->second()->y() * $controlPoint2Multiplier + + $this->drawable->last()->y() * $controlPoint3Multiplier + ); + + return ['x' => $x, 'y' => $y]; + } + + /** + * Calculate interpolation points for cubic beziers using the Bernstein polynomial form + * + * @return array{'x': float, 'y': float} + */ + private function calculateCubicBezierInterpolationPoint(float $t = 0.05): array + { + $remainder = 1 - $t; + $tSquared = $t * $t; + $remainderSquared = $remainder * $remainder; + $controlPoint1Multiplier = $remainderSquared * $remainder; + $controlPoint2Multiplier = $remainderSquared * $t * 3; + $controlPoint3Multiplier = $tSquared * $remainder * 3; + $controlPoint4Multiplier = $tSquared * $t; + + $x = ( + $this->drawable->first()->x() * $controlPoint1Multiplier + + $this->drawable->second()->x() * $controlPoint2Multiplier + + $this->drawable->third()->x() * $controlPoint3Multiplier + + $this->drawable->last()->x() * $controlPoint4Multiplier + ); + $y = ( + $this->drawable->first()->y() * $controlPoint1Multiplier + + $this->drawable->second()->y() * $controlPoint2Multiplier + + $this->drawable->third()->y() * $controlPoint3Multiplier + + $this->drawable->last()->y() * $controlPoint4Multiplier + ); + + return ['x' => $x, 'y' => $y]; + } + + /** + * Calculate the points needed to draw a quadratic or cubic bezier with optional border/stroke + * + * @throws InvalidArgumentException + * @throws ModifierException + * @return array{0: array, 1: array} + */ + private function calculateBezierPoints(): array + { + if ($this->drawable->count() !== 3 && $this->drawable->count() !== 4) { + throw new InvalidArgumentException('You must specify either 3 or 4 points to create a bezier curve'); + } + + $polygon = []; + $innerPolygon = []; + $outerPolygon = []; + $polygonBorderSegments = []; + + // define ratio t; equivalent to 5 percent distance along edge + $t = 0.05; + + $polygon[] = $this->drawable->first()->x(); + $polygon[] = $this->drawable->first()->y(); + for ($i = $t; $i < 1; $i += $t) { + if ($this->drawable->count() === 3) { + $ip = $this->calculateQuadraticBezierInterpolationPoint($i); + } elseif ($this->drawable->count() === 4) { + $ip = $this->calculateCubicBezierInterpolationPoint($i); + } + $polygon[] = (int) $ip['x']; + $polygon[] = (int) $ip['y']; + } + $polygon[] = $this->drawable->last()->x(); + $polygon[] = $this->drawable->last()->y(); + + if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 1) { + // create the border/stroke effect by calculating two new curves with offset positions + // from the main polygon and then connecting the inner/outer curves to create separate + // 4-point polygon segments + $polygonTotalPoints = count($polygon); + $offset = ($this->drawable->borderSize() / 2); + + for ($i = 0; $i < $polygonTotalPoints; $i += 2) { + if (array_key_exists($i + 2, $polygon) && array_key_exists($i + 3, $polygon)) { + $dx = $polygon[$i + 2] - $polygon[$i]; + $dy = $polygon[$i + 3] - $polygon[$i + 1]; + $dxySqrt = sqrt($dx * $dx + $dy * $dy); + + // prevent division by zero + if ($dxySqrt === 0.0) { + throw new ModifierException('Failed to apply ' . self::class . ', division by zero'); + } + + // inner polygon + $scale = $offset / $dxySqrt; + $ox = -$dy * $scale; + $oy = $dx * $scale; + + $innerPolygon[] = $ox + $polygon[$i]; + $innerPolygon[] = $oy + $polygon[$i + 1]; + $innerPolygon[] = $ox + $polygon[$i + 2]; + $innerPolygon[] = $oy + $polygon[$i + 3]; + + // outer polygon + $scale = -$offset / $dxySqrt; + $ox = -$dy * $scale; + $oy = $dx * $scale; + + $outerPolygon[] = $ox + $polygon[$i]; + $outerPolygon[] = $oy + $polygon[$i + 1]; + $outerPolygon[] = $ox + $polygon[$i + 2]; + $outerPolygon[] = $oy + $polygon[$i + 3]; + } + } + + $innerPolygonTotalPoints = count($innerPolygon); + + for ($i = 0; $i < $innerPolygonTotalPoints; $i += 2) { + if (array_key_exists($i + 2, $innerPolygon) && array_key_exists($i + 3, $innerPolygon)) { + $polygonBorderSegments[] = [ + $innerPolygon[$i], + $innerPolygon[$i + 1], + $outerPolygon[$i], + $outerPolygon[$i + 1], + $outerPolygon[$i + 2], + $outerPolygon[$i + 3], + $innerPolygon[$i + 2], + $innerPolygon[$i + 3], + ]; + } + } + } + + return [$polygon, $polygonBorderSegments]; + } + + /** + * Throw ModifierException with given message if result is 'false' + * + * @throws ModifierException + */ + private function abortUnless(mixed $result, string $message): void + { + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', ' . $message + ); + } + } +} diff --git a/src/Drivers/Gd/Modifiers/DrawEllipseModifier.php b/src/Drivers/Gd/Modifiers/DrawEllipseModifier.php new file mode 100644 index 000000000..e7c40c381 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/DrawEllipseModifier.php @@ -0,0 +1,82 @@ +drawable->hasBorder()) { + imagealphablending($frame->native(), true); + + // slightly smaller ellipse to keep 1px bordered edges clean + if ($this->drawable->hasBackgroundColor()) { + imagefilledellipse( + $frame->native(), + $this->drawable()->position()->x(), + $this->drawable->position()->y(), + $this->drawable->width() - 1, + $this->drawable->height() - 1, + $this->driver()->colorProcessor($image)->export( + $this->backgroundColor() + ) + ); + } + + // gd's imageellipse ignores imagesetthickness + // so i use imagearc with 360 degrees instead. + imagesetthickness( + $frame->native(), + $this->drawable->borderSize(), + ); + + imagearc( + $frame->native(), + $this->drawable()->position()->x(), + $this->drawable()->position()->y(), + $this->drawable->width(), + $this->drawable->height(), + 0, + 360, + $this->driver()->colorProcessor($image)->export( + $this->borderColor() + ) + ); + } elseif ($this->drawable->hasBackgroundColor()) { + imagealphablending($frame->native(), true); + imagesetthickness($frame->native(), 0); + imagefilledellipse( + $frame->native(), + $this->drawable()->position()->x(), + $this->drawable()->position()->y(), + $this->drawable->width(), + $this->drawable->height(), + $this->driver()->colorProcessor($image)->export( + $this->backgroundColor() + ) + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/DrawLineModifier.php b/src/Drivers/Gd/Modifiers/DrawLineModifier.php new file mode 100644 index 000000000..09cac1fe5 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/DrawLineModifier.php @@ -0,0 +1,58 @@ +drawable->hasBackgroundColor()) { + return $image; + } + + $color = $this->driver()->colorProcessor($image)->export( + $this->backgroundColor() + ); + + foreach ($image as $frame) { + $this->modifyFrame($frame, $color); + } + + return $image; + } + + /** + * Draw current line on given frame + * + * @throws ModifierException + */ + private function modifyFrame(FrameInterface $frame, int $color): void + { + imagealphablending($frame->native(), true); + imageantialias($frame->native(), true); + imagesetthickness($frame->native(), $this->drawable->width()); + imageline( + $frame->native(), + $this->drawable->start()->x(), + $this->drawable->start()->y(), + $this->drawable->end()->x(), + $this->drawable->end()->y(), + $color + ); + } +} diff --git a/src/Drivers/Gd/Modifiers/DrawPixelModifier.php b/src/Drivers/Gd/Modifiers/DrawPixelModifier.php new file mode 100644 index 000000000..ced1efb5e --- /dev/null +++ b/src/Drivers/Gd/Modifiers/DrawPixelModifier.php @@ -0,0 +1,41 @@ +driver()->colorProcessor($image)->export($this->color()); + + foreach ($image as $frame) { + imagealphablending($frame->native(), true); + imagesetpixel( + $frame->native(), + $this->position->x(), + $this->position->y(), + $color + ); + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/DrawPolygonModifier.php b/src/Drivers/Gd/Modifiers/DrawPolygonModifier.php new file mode 100644 index 000000000..d5990e0da --- /dev/null +++ b/src/Drivers/Gd/Modifiers/DrawPolygonModifier.php @@ -0,0 +1,55 @@ +drawable->hasBackgroundColor()) { + imagealphablending($frame->native(), true); + imagesetthickness($frame->native(), 0); + imagefilledpolygon( + $frame->native(), + $this->drawable->toArray(), + $this->driver()->colorProcessor($image)->export( + $this->backgroundColor() + ) + ); + } + + if ($this->drawable->hasBorder()) { + imagealphablending($frame->native(), true); + imagesetthickness($frame->native(), $this->drawable->borderSize()); + imagepolygon( + $frame->native(), + $this->drawable->toArray(), + $this->driver()->colorProcessor($image)->export( + $this->borderColor() + ) + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/DrawRectangleModifier.php b/src/Drivers/Gd/Modifiers/DrawRectangleModifier.php new file mode 100644 index 000000000..3f9445d79 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/DrawRectangleModifier.php @@ -0,0 +1,65 @@ +drawable->position(); + + foreach ($image as $frame) { + // draw background + if ($this->drawable->hasBackgroundColor()) { + imagealphablending($frame->native(), true); + imagesetthickness($frame->native(), 0); + imagefilledrectangle( + $frame->native(), + $position->x(), + $position->y(), + $position->x() + $this->drawable->width(), + $position->y() + $this->drawable->height(), + $this->driver()->colorProcessor($image)->export( + $this->backgroundColor() + ) + ); + } + + // draw border + if ($this->drawable->hasBorder()) { + imagealphablending($frame->native(), true); + imagesetthickness($frame->native(), $this->drawable->borderSize()); + imagerectangle( + $frame->native(), + $position->x(), + $position->y(), + $position->x() + $this->drawable->width(), + $position->y() + $this->drawable->height(), + $this->driver()->colorProcessor($image)->export( + $this->borderColor() + ) + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/FillModifier.php b/src/Drivers/Gd/Modifiers/FillModifier.php new file mode 100644 index 000000000..8c9016145 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/FillModifier.php @@ -0,0 +1,71 @@ +driver()->colorProcessor($image)->export( + $this->color() + ); + + foreach ($image as $frame) { + if ($this->hasPosition()) { + $this->floodFillWithColor($frame, $color); + } else { + $this->fillAllWithColor($frame, $color); + } + } + + return $image; + } + + /** + * @throws ModifierException + */ + private function floodFillWithColor(FrameInterface $frame, int $color): void + { + imagefill( + $frame->native(), + $this->position->x(), + $this->position->y(), + $color + ); + } + + /** + * @throws ModifierException + */ + private function fillAllWithColor(FrameInterface $frame, int $color): void + { + imagealphablending($frame->native(), true); + imagefilledrectangle( + $frame->native(), + 0, + 0, + $frame->size()->width() - 1, + $frame->size()->height() - 1, + $color + ); + } +} diff --git a/src/Drivers/Gd/Modifiers/FillTransparentAreasModifier.php b/src/Drivers/Gd/Modifiers/FillTransparentAreasModifier.php new file mode 100644 index 000000000..0fb3ca479 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/FillTransparentAreasModifier.php @@ -0,0 +1,51 @@ +backgroundColor($this->driver())->toColorspace(RgbColorspace::class); + + if (!$backgroundColor instanceof RgbColor) { + throw new ModifierException('Failed to normalize background color to RGB color space'); + } + + foreach ($image as $frame) { + // create new canvas with background color as background + $modified = Cloner::cloneBlended( + $frame->native(), + background: $backgroundColor + ); + + // set new gd image + $frame->setNative($modified); + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/FlipModifier.php b/src/Drivers/Gd/Modifiers/FlipModifier.php new file mode 100644 index 000000000..f750f4041 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/FlipModifier.php @@ -0,0 +1,32 @@ +direction === Direction::HORIZONTAL ? IMG_FLIP_HORIZONTAL : IMG_FLIP_VERTICAL; + + foreach ($image as $frame) { + imageflip($frame->native(), $direction); + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/GammaModifier.php b/src/Drivers/Gd/Modifiers/GammaModifier.php new file mode 100644 index 000000000..face3e8dd --- /dev/null +++ b/src/Drivers/Gd/Modifiers/GammaModifier.php @@ -0,0 +1,29 @@ +native(), 1, $this->gamma); + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/GrayscaleModifier.php b/src/Drivers/Gd/Modifiers/GrayscaleModifier.php new file mode 100644 index 000000000..95f51761a --- /dev/null +++ b/src/Drivers/Gd/Modifiers/GrayscaleModifier.php @@ -0,0 +1,34 @@ +native(), IMG_FILTER_GRAYSCALE); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to transform image to grayscale', + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/InsertModifier.php b/src/Drivers/Gd/Modifiers/InsertModifier.php new file mode 100644 index 000000000..fad79845f --- /dev/null +++ b/src/Drivers/Gd/Modifiers/InsertModifier.php @@ -0,0 +1,129 @@ +driver()->decodeImage($this->image); + $position = $this->position($image, $watermark); + + foreach ($image as $frame) { + imagealphablending($frame->native(), true); + + if ($this->transparency === 1.0) { + $this->insertOpaque($frame, $watermark, $position); + } else { + $this->insertTransparent($frame, $watermark, $position); + } + } + + return $image; + } + + /** + * Insert watermark with 100% opacity + * + * @throws ModifierException + */ + private function insertOpaque(FrameInterface $frame, ImageInterface $watermark, PointInterface $position): void + { + imagecopy( + $frame->native(), + $watermark->core()->native(), + $position->x(), + $position->y(), + 0, + 0, + $watermark->width(), + $watermark->height() + ); + } + + /** + * Insert watermark transparent with current transparency + * + * Unfortunately, the original PHP function imagecopymerge does not work reliably. + * For example, any transparency of the image to be inserted is not applied correctly. + * For this reason, a new GDImage is created into which the original image is inserted + * in the first step and the watermark is inserted with 100% opacity in the second + * step. This combination is then transferred to the original image again with the + * respective opacity. + * + * Please note: Unfortunately, there is still an edge case, when a transparent image + * is inserted on a transparent background, the "double" transparent areas appear opaque! + * + * @throws ModifierException + */ + private function insertTransparent(FrameInterface $frame, ImageInterface $watermark, PointInterface $position): void + { + $cut = imagecreatetruecolor($watermark->width(), $watermark->height()); + + if ($cut === false) { + throw new ModifierException('Failed to insert image'); + } + + imagecopy( + $cut, + $frame->native(), + 0, + 0, + $position->x(), + $position->y(), + imagesx($cut), + imagesy($cut) + ); + + imagecopy( + $cut, + $watermark->core()->native(), + 0, + 0, + 0, + 0, + imagesx($cut), + imagesy($cut) + ); + + try { + $transparency = (int) round(self::convertRange($this->transparency, 0, 1, 0, 100)); + } catch (RuntimeException $e) { + throw new ModifierException('Failed to convert transparency', previous: $e); + } + + imagecopymerge( + $frame->native(), + $cut, + $position->x(), + $position->y(), + 0, + 0, + $watermark->width(), + $watermark->height(), + $transparency, + ); + } +} diff --git a/src/Drivers/Gd/Modifiers/InvertModifier.php b/src/Drivers/Gd/Modifiers/InvertModifier.php new file mode 100644 index 000000000..0310d4297 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/InvertModifier.php @@ -0,0 +1,32 @@ +native(), IMG_FILTER_NEGATE); + if ($result === false) { + throw new ModifierException('Failed to invert image colors'); + } + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/OrientModifier.php b/src/Drivers/Gd/Modifiers/OrientModifier.php new file mode 100644 index 000000000..cdf3a19b3 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/OrientModifier.php @@ -0,0 +1,62 @@ +orientation($image)) { + 2 => $image->flip(), + 3 => $image->rotate(180), + 4 => $image->rotate(180)->flip(), + 5 => $image->rotate(90)->flip(), + 6 => $image->rotate(90), + 7 => $image->rotate(270)->flip(), + 8 => $image->rotate(270), + default => $image + }; + + return $this->markAligned($image); + } + + /** + * Return exif information about image orientation. + */ + private function orientation(ImageInterface $image): int + { + $orientation = $image->exif('IFD0.Orientation'); + + return is_numeric($orientation) ? (int) $orientation : 0; + } + + /** + * Set exif data of image to top-left orientation, marking the image as + * aligned and making sure the rotation correction process is not + * performed again. + */ + private function markAligned(ImageInterface $image): ImageInterface + { + $exif = $image->exif()->map(function ($item) { + if (is_array($item) && array_key_exists('Orientation', $item)) { + $item['Orientation'] = 1; + return $item; + } + + return $item; + }); + + return $image->setExif($exif); + } +} diff --git a/src/Drivers/Gd/Modifiers/PixelateModifier.php b/src/Drivers/Gd/Modifiers/PixelateModifier.php new file mode 100644 index 000000000..111ccf489 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/PixelateModifier.php @@ -0,0 +1,34 @@ +native(), IMG_FILTER_PIXELATE, $this->size, true); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to process pixelation effect', + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/ProfileModifier.php b/src/Drivers/Gd/Modifiers/ProfileModifier.php new file mode 100644 index 000000000..7e013af44 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/ProfileModifier.php @@ -0,0 +1,27 @@ +limit <= 0) { + throw new InvalidArgumentException('Quantization limit must be greater than 0'); + } + + // no color reduction if the limit is higher than the colors in the img + $colorCount = imagecolorstotal($image->core()->native()); + if ($colorCount > 0 && $this->limit > $colorCount) { + return $image; + } + + $width = $image->width(); + $height = $image->height(); + $backgroundColor = $this->backgroundColor($image); + + if (!$backgroundColor instanceof RgbColor) { + throw new ModifierException('Failed to convert background color to RGB color space'); + } + + $nativeBackgroundColor = $this->driver() + ->colorProcessor($image) + ->export($backgroundColor); + + foreach ($image as $frame) { + // create new image for color quantization + $reduced = Cloner::cloneEmpty($frame->native(), background: $backgroundColor); + + // fill with background + imagefill($reduced, 0, 0, $nativeBackgroundColor); + + // set transparency + imagecolortransparent($reduced, $nativeBackgroundColor); + + // copy original image (colors are limited automatically in the copy process) + imagecopy($reduced, $frame->native(), 0, 0, 0, 0, $width, $height); + + // gd library does not support color quantization directly therefore the + // colors are decrease by transforming the image to a palette version + imagetruecolortopalette($reduced, true, $this->limit); + + $frame->setNative($reduced); + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/RemoveAnimationModifier.php b/src/Drivers/Gd/Modifiers/RemoveAnimationModifier.php new file mode 100644 index 000000000..1b5b03b40 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/RemoveAnimationModifier.php @@ -0,0 +1,29 @@ +core()->setNative( + $this->selectedFrame($image)->native() + ); + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/RemoveProfileModifier.php b/src/Drivers/Gd/Modifiers/RemoveProfileModifier.php new file mode 100644 index 000000000..e42bf598e --- /dev/null +++ b/src/Drivers/Gd/Modifiers/RemoveProfileModifier.php @@ -0,0 +1,24 @@ +cropSize($image); + + $image->modify(new CropModifier( + $cropSize->width(), + $cropSize->height(), + $cropSize->pivot()->x(), + $cropSize->pivot()->y(), + $this->backgroundColor(), + )); + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/ResizeCanvasRelativeModifier.php b/src/Drivers/Gd/Modifiers/ResizeCanvasRelativeModifier.php new file mode 100644 index 000000000..53e375ffe --- /dev/null +++ b/src/Drivers/Gd/Modifiers/ResizeCanvasRelativeModifier.php @@ -0,0 +1,16 @@ +size()->resizeDown($this->width, $this->height); + } +} diff --git a/src/Drivers/Gd/Modifiers/ResizeModifier.php b/src/Drivers/Gd/Modifiers/ResizeModifier.php new file mode 100644 index 000000000..f979f883c --- /dev/null +++ b/src/Drivers/Gd/Modifiers/ResizeModifier.php @@ -0,0 +1,73 @@ +adjustedSize($image); + foreach ($image as $frame) { + $this->resizeFrame($frame, $resizeTo); + } + + return $image; + } + + /** + * @throws InvalidArgumentException + * @throws ModifierException + * @throws DriverException + */ + private function resizeFrame(FrameInterface $frame, SizeInterface $resizeTo): void + { + // create empty canvas in target size + $modified = Cloner::cloneEmpty($frame->native(), $resizeTo); + + // copy content from resource + imagecopyresampled( + $modified, + $frame->native(), + $resizeTo->pivot()->x(), + $resizeTo->pivot()->y(), + 0, + 0, + $resizeTo->width(), + $resizeTo->height(), + $frame->size()->width(), + $frame->size()->height() + ); + + // set new content as resource + $frame->setNative($modified); + } + + /** + * Return the size the modifier will resize to + */ + protected function adjustedSize(ImageInterface $image): SizeInterface + { + return $image->size()->resize($this->width, $this->height); + } +} diff --git a/src/Drivers/Gd/Modifiers/ResolutionModifier.php b/src/Drivers/Gd/Modifiers/ResolutionModifier.php new file mode 100644 index 000000000..7c9fb275c --- /dev/null +++ b/src/Drivers/Gd/Modifiers/ResolutionModifier.php @@ -0,0 +1,42 @@ +x)); + $y = intval(round($this->y)); + + foreach ($image as $frame) { + imageresolution($frame->native(), $x, $y); + } + + // GD returns 96x96 as resolution by default even if the image has no resolution. + // This is problematic because it is impossible to tell whether the image + // really has this resolution or whether it just corresponds to the default value. + // + // If the resolution was change to 96x96 (default resolution of GD) we mark + // the resolution as changed to be able to distinguish it + if ($x === 96 && $y === 96) { + $image->core()->meta()->set('resolutionChanged', true); + } + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/RotateModifier.php b/src/Drivers/Gd/Modifiers/RotateModifier.php new file mode 100644 index 000000000..a4424057f --- /dev/null +++ b/src/Drivers/Gd/Modifiers/RotateModifier.php @@ -0,0 +1,123 @@ +backgroundColor(); + + foreach ($image as $frame) { + $this->modifyFrame($frame, $background); + } + + return $image; + } + + /** + * Apply rotation modification on given frame, given background + * color is used for newly create image areas + * + * @throws InvalidArgumentException + * @throws ModifierException + * @throws DriverException + */ + protected function modifyFrame(FrameInterface $frame, ColorInterface $background): void + { + // normalize color to rgb colorspace + $background = $background->toColorspace(Rgb::class); + + if (!$background instanceof RgbColor) { + throw new ModifierException('Failed to normalize background color to RGB color space'); + } + + // get transparent color from frame core + $transparent = match ($transparent = imagecolortransparent($frame->native())) { + -1 => imagecolorallocatealpha( + $frame->native(), + $background->red()->value(), + $background->green()->value(), + $background->blue()->value(), + 127 + ), + default => $transparent, + }; + + // rotate original image against transparent background + $rotated = imagerotate( + $frame->native(), + $this->rotationAngle() * -1, + $transparent + ); + + // create size from original after rotation + $container = (new Size( + imagesx($rotated), + imagesy($rotated), + ))->movePivot(Alignment::CENTER); + + // create size from original and rotate points + $cutout = (new Size( + imagesx($frame->native()), + imagesy($frame->native()), + $container->pivot() + ))->alignHorizontally(Alignment::CENTER) + ->alignVertically(Alignment::CENTER) + ->rotate($this->rotationAngle()); + + // create new gd image + $modified = Cloner::cloneEmpty($frame->native(), $container, $background); + + // draw the cutout on new gd image to have a transparent + // background where the rotated image will be placed + imagealphablending($modified, false); + imagefilledpolygon( + $modified, + $cutout->toArray(), + imagecolortransparent($modified) + ); + + // place rotated image on new gd image + imagealphablending($modified, true); + imagecopy( + $modified, + $rotated, + 0, + 0, + 0, + 0, + imagesx($rotated), + imagesy($rotated) + ); + + $frame->setNative($modified); + } +} diff --git a/src/Drivers/Gd/Modifiers/ScaleDownModifier.php b/src/Drivers/Gd/Modifiers/ScaleDownModifier.php new file mode 100644 index 000000000..8efe602fd --- /dev/null +++ b/src/Drivers/Gd/Modifiers/ScaleDownModifier.php @@ -0,0 +1,21 @@ +size()->scaleDown($this->width, $this->height); + } +} diff --git a/src/Drivers/Gd/Modifiers/ScaleModifier.php b/src/Drivers/Gd/Modifiers/ScaleModifier.php new file mode 100644 index 000000000..dece12e52 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/ScaleModifier.php @@ -0,0 +1,16 @@ +size()->scale($this->width, $this->height); + } +} diff --git a/src/Drivers/Gd/Modifiers/SharpenModifier.php b/src/Drivers/Gd/Modifiers/SharpenModifier.php new file mode 100644 index 000000000..eaeb2b4d3 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/SharpenModifier.php @@ -0,0 +1,53 @@ +matrix(); + foreach ($image as $frame) { + $result = imageconvolution($frame->native(), $matrix, 1, 0); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set convolution matrix', + ); + } + } + + return $image; + } + + /** + * Create matrix to be used by imageconvolution() + * + * @return array> + */ + private function matrix(): array + { + $min = $this->level >= 10 ? $this->level * -0.01 : 0; + $max = $this->level * -0.025; + $abs = ((4 * $min + 4 * $max) * -1) + 1; + + return [ + [$min, $max, $min], + [$max, $abs, $max], + [$min, $max, $min] + ]; + } +} diff --git a/src/Drivers/Gd/Modifiers/SliceAnimationModifier.php b/src/Drivers/Gd/Modifiers/SliceAnimationModifier.php new file mode 100644 index 000000000..d58f541ee --- /dev/null +++ b/src/Drivers/Gd/Modifiers/SliceAnimationModifier.php @@ -0,0 +1,31 @@ +offset >= $image->count()) { + throw new InvalidArgumentException('Offset is not in the range of frames'); + } + + $image->core()->slice($this->offset, $this->length); + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/TextModifier.php b/src/Drivers/Gd/Modifiers/TextModifier.php new file mode 100644 index 000000000..a87f716eb --- /dev/null +++ b/src/Drivers/Gd/Modifiers/TextModifier.php @@ -0,0 +1,135 @@ +driver()->fontProcessor(); + $lines = $fontProcessor->textBlock($this->text, $this->font, $this->position); + + // decode text colors + $textColor = $this->gdTextColor($image); + $strokeColor = $this->gdStrokeColor($image); + + foreach ($image as $frame) { + imagealphablending($frame->native(), true); + if ($this->font->hasFile()) { + foreach ($lines as $line) { + foreach ($this->strokeOffsets($this->font) as $offset) { + imagettftext( + image: $frame->native(), + size: $fontProcessor->nativeFontSize($this->font), + angle: $this->font->angle() * -1, + x: $line->position()->x() + $offset->x(), + y: $line->position()->y() + $offset->y(), + color: $strokeColor, + font_filename: $this->font->filepath(), + text: (string) $line + ); + } + + imagettftext( + image: $frame->native(), + size: $fontProcessor->nativeFontSize($this->font), + angle: $this->font->angle() * -1, + x: $line->position()->x(), + y: $line->position()->y(), + color: $textColor, + font_filename: $this->font->filepath(), + text: (string) $line + ); + } + } else { + foreach ($lines as $line) { + foreach ($this->strokeOffsets($this->font) as $offset) { + imagestring( + image: $frame->native(), + font: $this->gdFont(), + x: $line->position()->x() + $offset->x(), + y: $line->position()->y() + $offset->y(), + string: (string) $line, + color: $strokeColor + ); + } + + imagestring( + image: $frame->native(), + font: $this->gdFont(), + x: $line->position()->x(), + y: $line->position()->y(), + string: (string) $line, + color: $textColor + ); + } + } + } + + return $image; + } + + /** + * Decode text color in GD compatible format + * + * @throws StateException + */ + protected function gdTextColor(ImageInterface $image): int + { + return $this + ->driver() + ->colorProcessor($image) + ->export(parent::textColor()); + } + + /** + * Decode color for stroke (outline) effect in GD compatible format + * + * @throws StateException + */ + protected function gdStrokeColor(ImageInterface $image): int + { + if (!$this->font->hasStrokeEffect()) { + return 0; + } + + $color = parent::strokeColor(); + + if ($color->isTransparent()) { + throw new StateException('The stroke color must be fully opaque'); + } + + return $this + ->driver() + ->colorProcessor($image) + ->export($color); + } + + /** + * Return GD's internal font size + */ + private function gdFont(): int + { + if (!in_array($this->font->size(), range(1, 5))) { + return 1; + } + + return (int) $this->font->size(); + } +} diff --git a/src/Drivers/Gd/Modifiers/TrimModifier.php b/src/Drivers/Gd/Modifiers/TrimModifier.php new file mode 100644 index 000000000..31b2b5123 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/TrimModifier.php @@ -0,0 +1,111 @@ +isAnimated()) { + throw new NotSupportedException('Trim modifier cannot be applied to animated images'); + } + + // apply tolerance with a min. value of .5 because the default tolerance of '0' should + // already trim away similar colors which is not the case with imagecropauto. + $trimmed = imagecropauto( + $image->core()->native(), + IMG_CROP_THRESHOLD, + max([.5, $this->tolerance / 10]), + $this->trimColor($image) + ); + + // if the tolerance is very high, it is possible that no image is left. + // imagick returns a 1x1 pixel image in this case. this does the same. + if ($trimmed === false) { + $trimmed = $this->driver()->createImage(1, 1)->core()->native(); + } + + $image->core()->setNative($trimmed); + + return $image; + } + + /** + * Create an average color from the colors of the four corner points of the given image + * + * @throws ModifierException + */ + private function trimColor(ImageInterface $image): int + { + // trim color base + $red = 0; + $green = 0; + $blue = 0; + + // corner coordinates + $size = $image->size(); + $cornerPoints = [ + new Point(0, 0), + new Point($size->width() - 1, 0), + new Point(0, $size->height() - 1), + new Point($size->width() - 1, $size->height() - 1), + ]; + + // create an average color to be used in trim operation + foreach ($cornerPoints as $pos) { + $cornerColor = imagecolorat($image->core()->native(), $pos->x(), $pos->y()); + + if ($cornerColor === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to determine average color for process', + ); + } + + try { + $rgb = imagecolorsforindex($image->core()->native(), $cornerColor); + } catch (ValueError) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to read trim color from index', + ); + } + + $red += round(round($rgb['red'] / 51) * 51); + $green += round(round($rgb['green'] / 51) * 51); + $blue += round(round($rgb['blue'] / 51) * 51); + } + + $red = (int) round($red / 4); + $green = (int) round($green / 4); + $blue = (int) round($blue / 4); + + $color = imagecolorallocate($image->core()->native(), $red, $green, $blue); + + if ($color === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to allocate trim color', + ); + } + + return $color; + } +} diff --git a/src/Drivers/Imagick/Analyzers/ColorspaceAnalyzer.php b/src/Drivers/Imagick/Analyzers/ColorspaceAnalyzer.php new file mode 100644 index 000000000..d26289425 --- /dev/null +++ b/src/Drivers/Imagick/Analyzers/ColorspaceAnalyzer.php @@ -0,0 +1,41 @@ +core()->native()->getImageColorspace()) { + Imagick::COLORSPACE_CMYK => new Cmyk(), + Imagick::COLORSPACE_SRGB, Imagick::COLORSPACE_RGB => new Rgb(), + Imagick::COLORSPACE_HSL => new Hsl(), + Imagick::COLORSPACE_HSB => new Hsv(), + constant(Imagick::class . '::COLORSPACE_OKLAB') => new Oklab(), + constant(Imagick::class . '::COLORSPACE_OKLCH') => new Oklch(), + default => throw new AnalyzerException('Failed to analyze unknown colorspace'), + }; + } catch (Error $e) { + throw new AnalyzerException('Failed to analyze colorspace', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Analyzers/HeightAnalyzer.php b/src/Drivers/Imagick/Analyzers/HeightAnalyzer.php new file mode 100644 index 000000000..609ce4ef0 --- /dev/null +++ b/src/Drivers/Imagick/Analyzers/HeightAnalyzer.php @@ -0,0 +1,26 @@ +core()->native()->getImageHeight(); + } catch (ImagickException $e) { + throw new AnalyzerException('Failed to read image height', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Analyzers/PixelColorAnalyzer.php b/src/Drivers/Imagick/Analyzers/PixelColorAnalyzer.php new file mode 100644 index 000000000..3da70cfe8 --- /dev/null +++ b/src/Drivers/Imagick/Analyzers/PixelColorAnalyzer.php @@ -0,0 +1,54 @@ +x > $image->width() - 1 || $this->y > $image->height() - 1) { + throw new InvalidArgumentException( + 'The specified position (' . $this->x . ', ' . $this->y . ') is not within the image area', + ); + } + + $colorProcessor = $this->driver()->colorProcessor($image); + + return $this->colorAt($colorProcessor, $image->core()->frame($this->frame)); + } + + /** + * @throws AnalyzerException + */ + protected function colorAt(ColorProcessorInterface $processor, FrameInterface $frame): ColorInterface + { + try { + return $processor->import( + $frame->native()->getImagePixelColor($this->x, $this->y) + ); + } catch (ImagickException $e) { + throw new AnalyzerException( + 'Failed to read pixel color at position ' . $this->x . ', ' . $this->y, + previous: $e + ); + } + } +} diff --git a/src/Drivers/Imagick/Analyzers/PixelColorsAnalyzer.php b/src/Drivers/Imagick/Analyzers/PixelColorsAnalyzer.php new file mode 100644 index 000000000..e75287302 --- /dev/null +++ b/src/Drivers/Imagick/Analyzers/PixelColorsAnalyzer.php @@ -0,0 +1,31 @@ +driver()->colorProcessor($image); + + foreach ($image as $frame) { + $colors->push( + parent::colorAt($colorProcessor, $frame) + ); + } + + return $colors; + } +} diff --git a/src/Drivers/Imagick/Analyzers/ProfileAnalyzer.php b/src/Drivers/Imagick/Analyzers/ProfileAnalyzer.php new file mode 100644 index 000000000..deb6ab2f7 --- /dev/null +++ b/src/Drivers/Imagick/Analyzers/ProfileAnalyzer.php @@ -0,0 +1,32 @@ +core()->native()->getImageProfiles('icc'); + + if (!array_key_exists('icc', $profiles)) { + throw new AnalyzerException('No ICC profile found in image'); + } + + return new Profile($profiles['icc']); + } +} diff --git a/src/Drivers/Imagick/Analyzers/ResolutionAnalyzer.php b/src/Drivers/Imagick/Analyzers/ResolutionAnalyzer.php new file mode 100644 index 000000000..d329b213a --- /dev/null +++ b/src/Drivers/Imagick/Analyzers/ResolutionAnalyzer.php @@ -0,0 +1,38 @@ +core()->native(); + + try { + $imageResolution = $imagick->getImageResolution(); + } catch (ImagickException $e) { + throw new AnalyzerException('Failed to read image resolution', previous: $e); + } + + return new Resolution( + $imageResolution['x'], + $imageResolution['y'], + $imagick->getImageUnits() === 2 ? Length::CM : Length::INCH + ); + } +} diff --git a/src/Drivers/Imagick/Analyzers/WidthAnalyzer.php b/src/Drivers/Imagick/Analyzers/WidthAnalyzer.php new file mode 100644 index 000000000..5bf17b390 --- /dev/null +++ b/src/Drivers/Imagick/Analyzers/WidthAnalyzer.php @@ -0,0 +1,26 @@ +core()->native()->getImageWidth(); + } catch (ImagickException $e) { + throw new AnalyzerException('Failed to read image width', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/ColorProcessor.php b/src/Drivers/Imagick/ColorProcessor.php new file mode 100644 index 000000000..7de4632b6 --- /dev/null +++ b/src/Drivers/Imagick/ColorProcessor.php @@ -0,0 +1,152 @@ +colorspace; + } + + /** + * @throws DriverException + */ + public function export(ColorInterface $color): ImagickPixel + { + $color = $this->colorspace->importColor($color); + + if ($this->colorspace instanceof Cmyk) { + try { + $pixel = new ImagickPixel(); + $pixel->setColorValue(Imagick::COLOR_CYAN, $color->channel(Cyan::class)->normalized()); + $pixel->setColorValue(Imagick::COLOR_MAGENTA, $color->channel(Magenta::class)->normalized()); + $pixel->setColorValue(Imagick::COLOR_YELLOW, $color->channel(Yellow::class)->normalized()); + $pixel->setColorValue(Imagick::COLOR_BLACK, $color->channel(Key::class)->normalized()); + } catch (ImagickException | ImagickPixelException $e) { + throw new DriverException('Failed to create CMYK color', previous: $e); + } + + return $pixel; + } + + $color = $color->toColorspace(Rgb::class); + + try { + return new ImagickPixel( + sprintf( + "srgba(%s, %s, %s, %s)", + $color->channel(Red::class)->value(), + $color->channel(Green::class)->value(), + $color->channel(Blue::class)->value(), + $color->channel(Alpha::class)->toString(), + ) + ); + } catch (ImagickException | ImagickPixelException $e) { + throw new DriverException('Failed to create color', previous: $e); + } + } + + /** + * @throws InvalidArgumentException + * @throws DriverException + * @throws NotSupportedException + */ + public function import(mixed $color): ColorInterface + { + if (!$color instanceof ImagickPixel) { + throw new InvalidArgumentException( + 'Imagick driver can only process colors from instances of ' . ImagickPixel::class, + ); + } + + try { + return match ($this->colorspace::class) { + Cmyk::class => $this->colorspace->colorFromNormalized([ + $color->getColorValue(Imagick::COLOR_CYAN), + $color->getColorValue(Imagick::COLOR_MAGENTA), + $color->getColorValue(Imagick::COLOR_YELLOW), + $color->getColorValue(Imagick::COLOR_BLACK), + ]), + Rgb::class => $this->colorspace->colorFromNormalized([ + $color->getColorValue(Imagick::COLOR_RED), + $color->getColorValue(Imagick::COLOR_GREEN), + $color->getColorValue(Imagick::COLOR_BLUE), + $color->getColorValue(Imagick::COLOR_ALPHA), + ]), + Hsl::class => Rgb::class::colorFromNormalized([ + $color->getColorValue(Imagick::COLOR_RED), + $color->getColorValue(Imagick::COLOR_GREEN), + $color->getColorValue(Imagick::COLOR_BLUE), + $color->getColorValue(Imagick::COLOR_ALPHA), + ])->toColorspace(Hsl::class), + Hsv::class => Rgb::colorFromNormalized([ + $color->getColorValue(Imagick::COLOR_RED), + $color->getColorValue(Imagick::COLOR_GREEN), + $color->getColorValue(Imagick::COLOR_BLUE), + $color->getColorValue(Imagick::COLOR_ALPHA), + ])->toColorspace(Hsv::class), + Oklab::class => Rgb::colorFromNormalized([ + $color->getColorValue(Imagick::COLOR_RED), + $color->getColorValue(Imagick::COLOR_GREEN), + $color->getColorValue(Imagick::COLOR_BLUE), + $color->getColorValue(Imagick::COLOR_ALPHA), + ])->toColorspace(Oklab::class), + Oklch::class => Rgb::colorFromNormalized([ + $color->getColorValue(Imagick::COLOR_RED), + $color->getColorValue(Imagick::COLOR_GREEN), + $color->getColorValue(Imagick::COLOR_BLUE), + $color->getColorValue(Imagick::COLOR_ALPHA), + ])->toColorspace(Oklch::class), + default => throw new NotSupportedException( + 'Colorspace ' . $this->colorspace::class . ' is not supported by driver' + ) + }; + } catch (ImagickPixelException $e) { + throw new DriverException( + 'Failed to import color from ' . ImagickPixel::class, + previous: $e, + ); + } + } +} diff --git a/src/Drivers/Imagick/Core.php b/src/Drivers/Imagick/Core.php new file mode 100644 index 000000000..62eb59297 --- /dev/null +++ b/src/Drivers/Imagick/Core.php @@ -0,0 +1,460 @@ + + */ +class Core implements CoreInterface, Iterator +{ + protected int $iteratorIndex = 0; + protected CollectionInterface $meta; + + /** + * Create new core instance + */ + public function __construct(protected Imagick $imagick) + { + $this->meta = new Collection(); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::has() + */ + public function has(int|string $key): bool + { + try { + return $this->imagick->setIteratorIndex((int) $key); + } catch (ImagickException) { + return false; + } + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::push() + * + * @throws DriverException + */ + public function push(mixed $item): CollectionInterface + { + return $this->add($item); + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::map() + * + * @throws Exception + */ + public function map(callable $callback): CoreInterface + { + throw new \Exception('Not implemented'); + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::filter() + * + * @throws Exception + */ + public function filter(callable $callback): CoreInterface + { + throw new \Exception('Not implemented'); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::get() + * + * @throws DriverException + */ + public function get(int|string $key, mixed $default = null): mixed + { + try { + $this->imagick->setIteratorIndex((int) $key); + } catch (ImagickException) { + return $default; + } + + try { + return new Frame($this->imagick->current()); + } catch (ImagickException | RuntimeException $e) { + throw new DriverException('Failed to get current frame data', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::set() + * + * @throws DriverException + */ + public function set(int|string $key, mixed $item): CollectionInterface + { + return $this->add($item); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::at() + * + * @throws DriverException + */ + public function at(int $key = 0, mixed $default = null): mixed + { + return $this->get($key, $default); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::clear() + */ + public function clear(): CollectionInterface + { + $this->imagick->clear(); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::slice() + * + * @throws DriverException + */ + public function slice(int $offset, ?int $length = null): CollectionInterface + { + $allowedIndexes = []; + $length = is_null($length) ? $this->count() : $length; + for ($i = $offset; $i < $offset + $length; $i++) { + $allowedIndexes[] = $i; + } + + try { + $sliced = new Imagick(); + } catch (ImagickException $e) { + throw new DriverException('Failed to slice image', previous: $e); + } + + foreach ($this->imagick as $key => $native) { + if (in_array($key, $allowedIndexes)) { + try { + $sliced->addImage($native->getImage()); + } catch (ImagickException $e) { + throw new DriverException('Failed to slice image', previous: $e); + } + } + } + + try { + $coalesced = $sliced->coalesceImages(); + $sliced->clear(); + $coalesced->setImageIterations($this->imagick->getImageIterations()); + } catch (ImagickException $e) { + throw new DriverException('Failed to slice image', previous: $e); + } + + $this->imagick->clear(); + $this->imagick = $coalesced; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::add() + * + * @throws DriverException + */ + public function add(FrameInterface $frame): CoreInterface + { + $imagick = $frame->native(); + + try { + $imagick->setImageDelay( + (int) round($frame->delay() * 100) + ); + + $imagick->setImageDispose($frame->disposalMethod()); + + $size = $frame->size(); + $imagick->setImagePage( + $size->width(), + $size->height(), + $frame->offsetLeft(), + $frame->offsetTop() + ); + + $this->imagick->addImage($imagick); + } catch (ImagickException $e) { + throw new DriverException('Failed to add image frame', previous: $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::count() + * + * @throws DriverException + */ + public function count(): int + { + try { + return $this->imagick->getNumberImages(); + } catch (ImagickException $e) { + throw new DriverException('Failed to count image frames', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see Iterator::rewind() + * + * @throws DriverException + */ + public function current(): mixed + { + try { + $this->imagick->setIteratorIndex($this->iteratorIndex); + + return new Frame($this->imagick->current()); + } catch (ImagickException | RuntimeException $e) { + throw new DriverException('Failed to iterate image frames', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see Iterator::rewind() + */ + public function next(): void + { + $this->iteratorIndex += 1; + } + + /** + * {@inheritdoc} + * + * @see Iterator::rewind() + */ + public function key(): mixed + { + return $this->iteratorIndex; + } + + /** + * {@inheritdoc} + * + * @see Iterator::rewind() + */ + public function valid(): bool + { + try { + $result = $this->imagick->setIteratorIndex($this->iteratorIndex); + } catch (ImagickException) { + return false; + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see Iterator::rewind() + */ + public function rewind(): void + { + $this->iteratorIndex = 0; + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::native() + */ + public function native(): mixed + { + return $this->imagick; + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::setNative() + * + * @throws InvalidArgumentException + */ + public function setNative(mixed $native): CoreInterface + { + if (!$native instanceof Imagick) { + throw new InvalidArgumentException('Argument $native must be of type ' . Imagick::class); + } + + $this->imagick->clear(); + $this->imagick = $native; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::frame() + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public function frame(int $position): FrameInterface + { + foreach ($this->imagick as $core) { + try { + if ($core->getIteratorIndex() === $position) { + return new Frame($core); + } + } catch (ImagickException | RuntimeException $e) { + throw new DriverException('Failed to load image frame a position ' . $position, previous: $e); + } + } + + throw new InvalidArgumentException('Frame #' . $position . ' could not be found in the image'); + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::loops() + * + * @throws DriverException + */ + public function loops(): int + { + try { + return $this->imagick->getImageIterations(); + } catch (ImagickException $e) { + throw new DriverException('Failed to get image loop count', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::setLoops() + * + * @throws DriverException + */ + public function setLoops(int $loops): CoreInterface + { + try { + $coalesced = $this->imagick->coalesceImages(); + $this->imagick->clear(); + $this->imagick = $coalesced; + $this->imagick->setImageIterations($loops); + } catch (ImagickException $e) { + throw new DriverException('Failed to set image loop count', previous: $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::first() + * + * @throws DriverException + * @throws StateException + */ + public function first(): FrameInterface + { + try { + return $this->frame(0); + } catch (InvalidArgumentException $e) { + throw new StateException('First frame not found in image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see CollectableInterface::last() + * + * @throws DriverException + * @throws StateException + */ + public function last(): FrameInterface + { + try { + return $this->frame($this->count() - 1); + } catch (InvalidArgumentException $e) { + throw new StateException('Last frame not found in image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see CoreInterface::meta() + */ + public function meta(): CollectionInterface + { + return $this->meta; + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::toArray() + */ + public function toArray(): array + { + $frames = []; + + foreach ($this as $frame) { + $frames[] = $frame; + } + + return $frames; + } + + /** + * Clone instance + */ + public function __clone(): void + { + $this->meta = clone $this->meta; + $this->imagick = clone $this->imagick; + } +} diff --git a/src/Drivers/Imagick/Decoders/Base64ImageDecoder.php b/src/Drivers/Imagick/Decoders/Base64ImageDecoder.php new file mode 100644 index 000000000..3637748c5 --- /dev/null +++ b/src/Drivers/Imagick/Decoders/Base64ImageDecoder.php @@ -0,0 +1,45 @@ +couldBeBase64Data($input); + } + + /** + * {@inheritdoc} + * + * @see DecoderInterface::decode() + */ + public function decode(mixed $input): ImageInterface + { + try { + $data = $this->decodeBase64Data($input); + } catch (DecoderException) { + throw new ImageDecoderException('Unable to Base64-decode image from string'); + } + + try { + return parent::decode($data); + } catch (DecoderException) { + throw new ImageDecoderException('Base64-encoded data contains unsupported image type'); + } + } +} diff --git a/src/Drivers/Imagick/Decoders/BinaryImageDecoder.php b/src/Drivers/Imagick/Decoders/BinaryImageDecoder.php new file mode 100644 index 000000000..5f35ca771 --- /dev/null +++ b/src/Drivers/Imagick/Decoders/BinaryImageDecoder.php @@ -0,0 +1,76 @@ +couldBeBinaryData($input); + } + + /** + * {@inheritdoc} + * + * @see DecoderInterface::decode() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + * @throws StateException + */ + public function decode(mixed $input): ImageInterface + { + if (!is_string($input) && !$input instanceof Stringable) { + throw new InvalidArgumentException( + 'Image source must be binary data of type string or instance of ' . Stringable::class, + ); + } + + $input = (string) $input; + + if ($input === '') { + throw new InvalidArgumentException('Unable to decode binary data from empty string'); + } + + try { + $imagick = new Imagick(); + $imagick->readImageBlob($input); + } catch (ImagickException) { + throw new ImageDecoderException('Failed to decode unsupported image format from binary data'); + } + + // decode image + $image = parent::decode($imagick); + + // get media type enum from string media type + $format = Format::tryCreate($image->origin()->mediaType()); + + // extract exif data for appropriate formats + if (in_array($format, [Format::JPEG, Format::TIFF])) { + $image->setExif($this->extractExifData($input)); + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Decoders/DataUriImageDecoder.php b/src/Drivers/Imagick/Decoders/DataUriImageDecoder.php new file mode 100644 index 000000000..3c67e4b5b --- /dev/null +++ b/src/Drivers/Imagick/Decoders/DataUriImageDecoder.php @@ -0,0 +1,62 @@ +couldBeDataUrl($input); + } + + /** + * {@inheritdoc} + * + * @see DecoderInterface::decode() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + * @throws StateException + */ + public function decode(mixed $input): ImageInterface + { + if ($input instanceof DataUri) { + try { + return parent::decode($input->data()); + } catch (DecoderException) { + throw new ImageDecoderException('Data Uri contains unsupported image type'); + } + } + + if (!is_string($input)) { + throw new InvalidArgumentException( + 'Image source must be data uri scheme of type string or ' . DataUri::class, + ); + } + + try { + return parent::decode(DataUri::parse($input)->data()); + } catch (DecoderException) { + throw new ImageDecoderException('Data Uri contains unsupported image type'); + } + } +} diff --git a/src/Drivers/Imagick/Decoders/EncodedImageObjectDecoder.php b/src/Drivers/Imagick/Decoders/EncodedImageObjectDecoder.php new file mode 100644 index 000000000..46c52b38b --- /dev/null +++ b/src/Drivers/Imagick/Decoders/EncodedImageObjectDecoder.php @@ -0,0 +1,50 @@ +toString()); + } catch (DecoderException) { + throw new ImageDecoderException(EncodedImage::class . ' contains unsupported image type'); + } + } +} diff --git a/src/Drivers/Imagick/Decoders/FilePathImageDecoder.php b/src/Drivers/Imagick/Decoders/FilePathImageDecoder.php new file mode 100644 index 000000000..2102d46a4 --- /dev/null +++ b/src/Drivers/Imagick/Decoders/FilePathImageDecoder.php @@ -0,0 +1,77 @@ +couldBeFilePath($input); + } + + /** + * {@inheritdoc} + * + * @see DecoderInterface::decode() + * + * @throws InvalidArgumentException + * @throws DirectoryNotFoundException + * @throws FileNotFoundException + * @throws FileNotReadableException + * @throws DriverException + * @throws StateException + * @throws ImageDecoderException + */ + public function decode(mixed $input): ImageInterface + { + // make sure path is valid + $path = self::readableFilePathOrFail($input); + + try { + $imagick = new Imagick(); + $imagick->readImage($path); + } catch (ImagickException) { + throw new ImageDecoderException( + 'Failed to decode image data from file "' . $path . '"' + ); + } + + // decode image + $image = parent::decode($imagick); + + // set file path on origin + $image->origin()->setFilePath($path); + + try { + // extract exif data for the appropriate formats + if (in_array($imagick->getImageFormat(), ['JPEG', 'TIFF', 'TIF'])) { + $image->setExif($this->extractExifData($path)); + } + } catch (ImagickException $e) { + throw new ImageDecoderException('Failed to retrieve image format', previous: $e); + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Decoders/NativeObjectDecoder.php b/src/Drivers/Imagick/Decoders/NativeObjectDecoder.php new file mode 100644 index 000000000..41df7915f --- /dev/null +++ b/src/Drivers/Imagick/Decoders/NativeObjectDecoder.php @@ -0,0 +1,108 @@ +getImageFormat() !== 'JPEG') { + $input = $input->coalesceImages(); + } + } catch (ImagickException $e) { + throw new DriverException('Failed to coalesce image', previous: $e); + } + + // turn images with colorspace 'GRAY' into 'SRGB' to avoid working on + // grayscale colorspace images as this results images loosing color + // information when placed into this image. + try { + if ($input->getImageColorspace() === Imagick::COLORSPACE_GRAY) { + $input->setImageColorspace(Imagick::COLORSPACE_SRGB); + } + } catch (ImagickException $e) { + throw new DriverException('Failed to convert image to sRGB', previous: $e); + } + + // create image object + $image = new Image($this->driver(), new Core($input)); + + // If autoOrientation is disabled, automatic image alignment should be prevented. + // Therefore, it is set to "undefined" here. To still be able to correct the + // orientation manually later, we save the original value. + if ($this->driver()->config()->autoOrientation === false) { + try { + $image->core()->meta()->set('originalImageOrientation', $input->getImageOrientation()); + $input->setImageOrientation(Imagick::ORIENTATION_UNDEFINED); + } catch (ImagickException $e) { + throw new ImageDecoderException( + 'Failed to set adjust image orientation', + previous: $e + ); + } + } + + // discard animation depending on config + if (!$this->driver()->config()->decodeAnimation) { + $image->modify(new RemoveAnimationModifier()); + } + + // adjust image rotation + if ($this->driver()->config()->autoOrientation) { + $image->modify(new OrientModifier()); + } + + // set media type on origin + try { + $image->origin()->setMediaType($input->getImageMimeType()); + } catch (ImagickException $e) { + throw new ImageDecoderException('Failed to retrieve image media type', previous: $e); + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Decoders/SplFileInfoImageDecoder.php b/src/Drivers/Imagick/Decoders/SplFileInfoImageDecoder.php new file mode 100644 index 000000000..928c09918 --- /dev/null +++ b/src/Drivers/Imagick/Decoders/SplFileInfoImageDecoder.php @@ -0,0 +1,58 @@ +'); + } + + try { + $background = new ImagickPixel('rgba(255, 255, 255, 0)'); + + $imagick = new Imagick(); + $imagick->newImage($width, $height, $background, 'png'); + $this->applyDefaultSettings($imagick); + } catch (ImagickException | ImagickPixelException $e) { + throw new DriverException('Failed to create new image', previous: $e); + } + + return new Image($this, new Core($imagick)); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::createCore() + * + * @throws DriverException + */ + public function createCore(array $frames): CoreInterface + { + try { + $core = new Core(new Imagick()); + } catch (ImagickException $e) { + throw new DriverException('Failed to create new core', previous: $e); + } + + foreach ($frames as $frame) { + $core->add($frame); + } + + $this->applyDefaultSettings($core->native()); + + return $core; + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::colorProcessor() + */ + public function colorProcessor(ImageInterface $image): ColorProcessorInterface + { + return new ColorProcessor($image->colorspace()); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::fontProcessor() + */ + public function fontProcessor(): FontProcessorInterface + { + return new FontProcessor(); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::supports() + */ + public function supports(string|Format|FileExtension|MediaType $identifier): bool + { + try { + $format = Format::create($identifier); + } catch (InvalidArgumentException) { + return false; + } + + return count(Imagick::queryFormats($format->name)) >= 1; + } + + /** + * Return version of ImageMagick library + * + * @throws DriverException + */ + public function version(): string + { + $pattern = '/^ImageMagick (?P(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)' . + '(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?' . + '(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)/'; + + if (preg_match($pattern, Imagick::getVersion()['versionString'], $matches) !== 1) { + throw new DriverException('Unable to read ImageMagick version number'); + } + + return $matches['version']; + } + + /** + * Apply default settings for native image object. + * + * @throws DriverException + */ + private function applyDefaultSettings(Imagick $imagick): Imagick + { + try { + $background = new ImagickPixel('rgba(255, 255, 255, 0)'); + + $imagick->setType(Imagick::IMGTYPE_UNDEFINED); + $imagick->setImageType(Imagick::IMGTYPE_UNDEFINED); + $imagick->setColorspace(Imagick::COLORSPACE_SRGB); + $imagick->setImageResolution(72, 72); + $imagick->setImageUnits(Imagick::RESOLUTION_PIXELSPERINCH); + $imagick->setImageBackgroundColor($background); + $imagick->setImageIterations(0); + + return $imagick; + } catch (ImagickException | ImagickPixelException $e) { + throw new DriverException('Failed to apply default image settings', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Encoders/AvifEncoder.php b/src/Drivers/Imagick/Encoders/AvifEncoder.php new file mode 100644 index 000000000..5bdd54111 --- /dev/null +++ b/src/Drivers/Imagick/Encoders/AvifEncoder.php @@ -0,0 +1,55 @@ +strip || (is_null($this->strip) && $this->driver()->config()->strip)) { + $image->modify(new StripMetaModifier()); + } + + try { + $imagick = clone $image->core()->native(); + $imagick->setFormat($format); + $imagick->setImageFormat($format); + $imagick->setCompression($compression); + $imagick->setImageCompression($compression); + $imagick->setCompressionQuality($this->quality); + $imagick->setImageCompressionQuality($this->quality); + + $result = new EncodedImage($imagick->getImagesBlob(), 'image/avif'); + $imagick->clear(); + + return $result; + } catch (ImageException $e) { + throw new EncoderException('Failed to encode avif format', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Encoders/BmpEncoder.php b/src/Drivers/Imagick/Encoders/BmpEncoder.php new file mode 100644 index 000000000..a53d2f45c --- /dev/null +++ b/src/Drivers/Imagick/Encoders/BmpEncoder.php @@ -0,0 +1,47 @@ +core()->native(); + $imagick->setFormat($format); + $imagick->setImageFormat($format); + $imagick->setCompression($compression); + $imagick->setImageCompression($compression); + + $result = new EncodedImage($imagick->getImagesBlob(), 'image/bmp'); + $imagick->clear(); + + return $result; + } catch (ImageException $e) { + throw new EncoderException('Failed to encode bmp format', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Encoders/GifEncoder.php b/src/Drivers/Imagick/Encoders/GifEncoder.php new file mode 100644 index 000000000..ce56a39ff --- /dev/null +++ b/src/Drivers/Imagick/Encoders/GifEncoder.php @@ -0,0 +1,51 @@ +core()->native(); + $imagick->setFormat($format); + $imagick->setImageFormat($format); + $imagick->setCompression($compression); + $imagick->setImageCompression($compression); + + if ($this->interlaced) { + $imagick->setInterlaceScheme(Imagick::INTERLACE_LINE); + } + + $result = new EncodedImage($imagick->getImagesBlob(), 'image/gif'); + $imagick->clear(); + + return $result; + } catch (ImageException $e) { + throw new EncoderException('Failed to encode gif format', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Encoders/HeicEncoder.php b/src/Drivers/Imagick/Encoders/HeicEncoder.php new file mode 100644 index 000000000..9b1eb575a --- /dev/null +++ b/src/Drivers/Imagick/Encoders/HeicEncoder.php @@ -0,0 +1,51 @@ +strip || (is_null($this->strip) && $this->driver()->config()->strip)) { + $image->modify(new StripMetaModifier()); + } + + try { + $imagick = clone $image->core()->native(); + $imagick->setFormat($format); + $imagick->setImageFormat($format); + $imagick->setCompressionQuality($this->quality); + $imagick->setImageCompressionQuality($this->quality); + + $result = new EncodedImage($imagick->getImagesBlob(), 'image/heic'); + $imagick->clear(); + + return $result; + } catch (ImageException $e) { + throw new EncoderException('Failed to encode heic format', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Encoders/IcoEncoder.php b/src/Drivers/Imagick/Encoders/IcoEncoder.php new file mode 100644 index 000000000..f5c845bfb --- /dev/null +++ b/src/Drivers/Imagick/Encoders/IcoEncoder.php @@ -0,0 +1,47 @@ +core()->native(); + $imagick->setFormat($format); + $imagick->setImageFormat($format); + $imagick->setCompression($compression); + $imagick->setImageCompression($compression); + + $result = new EncodedImage($imagick->getImagesBlob(), 'image/x-icon'); + $imagick->clear(); + + return $result; + } catch (ImagickException $e) { + throw new EncoderException('Failed to encode ico format', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Encoders/Jpeg2000Encoder.php b/src/Drivers/Imagick/Encoders/Jpeg2000Encoder.php new file mode 100644 index 000000000..bc420e5fb --- /dev/null +++ b/src/Drivers/Imagick/Encoders/Jpeg2000Encoder.php @@ -0,0 +1,57 @@ +strip || (is_null($this->strip) && $this->driver()->config()->strip)) { + $image->modify(new StripMetaModifier()); + } + + try { + $imagick = clone $image->core()->native(); + $imagick->setImageBackgroundColor('white'); + $imagick->setBackgroundColor('white'); + $imagick->setFormat($format); + $imagick->setImageFormat($format); + $imagick->setCompression($compression); + $imagick->setImageCompression($compression); + $imagick->setCompressionQuality($this->quality); + $imagick->setImageCompressionQuality($this->quality); + + $result = new EncodedImage($imagick->getImagesBlob(), 'image/jp2'); + $imagick->clear(); + + return $result; + } catch (ImagickException $e) { + throw new EncoderException('Failed to encode jp2 format', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Encoders/JpegEncoder.php b/src/Drivers/Imagick/Encoders/JpegEncoder.php new file mode 100644 index 000000000..4c7c8f8ca --- /dev/null +++ b/src/Drivers/Imagick/Encoders/JpegEncoder.php @@ -0,0 +1,74 @@ +driver()->decodeColor( + $this->driver()->config()->backgroundColor + ); + + // resolve background color because jpeg has no transparency + $background = $this->driver() + ->colorProcessor($image) + ->export($backgroundColor); + + // set alpha value to 1 because Imagick renders + // possible full transparent colors as black + $background->setColorValue(Imagick::COLOR_ALPHA, 1); + + // strip meta data + if ($this->strip || (is_null($this->strip) && $this->driver()->config()->strip)) { + $image->modify(new StripMetaModifier()); + } + + try { + $imagick = clone $image->core()->native(); + $imagick->setImageBackgroundColor($background); + $imagick->setBackgroundColor($background); + $imagick->setFormat($format); + $imagick->setImageFormat($format); + $imagick->setCompression($compression); + $imagick->setImageCompression($compression); + $imagick->setCompressionQuality($this->quality); + $imagick->setImageCompressionQuality($this->quality); + $imagick->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE); + + if ($this->progressive) { + $imagick->setInterlaceScheme(Imagick::INTERLACE_PLANE); + } + + $result = new EncodedImage($imagick->getImagesBlob(), 'image/jpeg'); + $imagick->clear(); + + return $result; + } catch (ImagickException $e) { + throw new EncoderException('Failed to encode jpeg format', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Encoders/PngEncoder.php b/src/Drivers/Imagick/Encoders/PngEncoder.php new file mode 100644 index 000000000..b775a76a5 --- /dev/null +++ b/src/Drivers/Imagick/Encoders/PngEncoder.php @@ -0,0 +1,63 @@ +indexed) { + // reduce colors + $output = clone $image; + $output->reduceColors(256); + + $output = $output->core()->native(); + $output->setFormat('PNG'); + $output->setImageFormat('PNG'); + } else { + $output = clone $image->core()->native(); + $output->setFormat('PNG32'); + $output->setImageFormat('PNG32'); + } + + $output->setCompression(Imagick::COMPRESSION_ZIP); + $output->setImageCompression(Imagick::COMPRESSION_ZIP); + + if ($this->interlaced) { + $output->setInterlaceScheme(Imagick::INTERLACE_LINE); + } + + $result = new EncodedImage($output->getImagesBlob(), 'image/png'); + $output->clear(); + + return $result; + } catch (ImagickException $e) { + throw new EncoderException('Failed to encode png format', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Encoders/TiffEncoder.php b/src/Drivers/Imagick/Encoders/TiffEncoder.php new file mode 100644 index 000000000..dc4e3e223 --- /dev/null +++ b/src/Drivers/Imagick/Encoders/TiffEncoder.php @@ -0,0 +1,57 @@ +strip || (is_null($this->strip) && $this->driver()->config()->strip)) { + $image->modify(new StripMetaModifier()); + } + + try { + $imagick = clone $image->core()->native(); + $imagick->setFormat($format); + $imagick->setImageFormat($format); + $imagick->setCompression($imagick->getImageCompression()); + $imagick->setImageCompression($imagick->getImageCompression()); + $imagick->setCompressionQuality($this->quality); + $imagick->setImageCompressionQuality($this->quality); + + $result = new EncodedImage($imagick->getImagesBlob(), 'image/tiff'); + $imagick->clear(); + + return $result; + } catch (ImagickException $e) { + throw new EncoderException('Failed to encode tiff format', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/Encoders/WebpEncoder.php b/src/Drivers/Imagick/Encoders/WebpEncoder.php new file mode 100644 index 000000000..7803f5ef7 --- /dev/null +++ b/src/Drivers/Imagick/Encoders/WebpEncoder.php @@ -0,0 +1,75 @@ +strip || (is_null($this->strip) && $this->driver()->config()->strip)) { + $image->modify(new StripMetaModifier()); + } + + try { + $imagick = clone $image->core()->native(); + + try { + $imagick->setImageBackgroundColor(new ImagickPixel('transparent')); + } catch (ImagickPixelException $e) { + throw new EncoderException('Failed to encode webp format', previous: $e); + } + + if (!$image->isAnimated()) { + $imagick = $imagick->mergeImageLayers(Imagick::LAYERMETHOD_MERGE); + } + + $imagick->setFormat($format); + $imagick->setImageFormat($format); + $imagick->setCompression($compression); + $imagick->setImageCompression($compression); + $imagick->setImageCompressionQuality($this->quality); + + if ($this->quality === 100) { + $imagick->setOption('webp:lossless', 'true'); + } + + $result = new EncodedImage($imagick->getImagesBlob(), 'image/webp'); + $imagick->clear(); + + return $result; + } catch (ImagickException $e) { + throw new EncoderException('Failed to encode webp format', previous: $e); + } + } +} diff --git a/src/Drivers/Imagick/FontProcessor.php b/src/Drivers/Imagick/FontProcessor.php new file mode 100644 index 000000000..444696a13 --- /dev/null +++ b/src/Drivers/Imagick/FontProcessor.php @@ -0,0 +1,82 @@ +toImagickDraw($font); + try { + $dimensions = (new Imagick())->queryFontMetrics($draw, $text); + } catch (ImagickException $e) { + throw new DriverException('Failed query font metrics', previous: $e); + } + + return new Size( + intval(round($dimensions['textWidth'])), + intval(round($dimensions['ascender'] + $dimensions['descender'])), + ); + } + + /** + * Imagick::annotateImage() needs an ImagickDraw object - this method takes + * the font object as the base and adds an optional passed color to the new + * ImagickDraw object. + * + * @throws StateException + * @throws DriverException + */ + public function toImagickDraw(FontInterface $font, ?ImagickPixel $color = null): ImagickDraw + { + if (!$font->hasFile()) { + throw new StateException('No font file specified'); + } + + try { + $draw = new ImagickDraw(); + $draw->setStrokeAntialias(true); + $draw->setTextAntialias(true); + $draw->setFont($font->filepath()); + $draw->setFontSize($this->nativeFontSize($font)); + $draw->setTextAlignment(Imagick::ALIGN_LEFT); + + if ($color instanceof ImagickPixel) { + $draw->setFillColor($color); + } + } catch (ImagickException | ImagickDrawException $e) { + throw new DriverException('Failed to convert font to ImagickDraw instance', previous: $e); + } + + return $draw; + } +} diff --git a/src/Drivers/Imagick/Frame.php b/src/Drivers/Imagick/Frame.php new file mode 100644 index 000000000..6a288e3d6 --- /dev/null +++ b/src/Drivers/Imagick/Frame.php @@ -0,0 +1,250 @@ +native->setImageBackgroundColor($background); + $this->native->setBackgroundColor($background); + } catch (ImagickException | ImagickPixelException $e) { + throw new DriverException('Failed to create instance of ' . self::class, previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::toImage() + */ + public function toImage(DriverInterface $driver): ImageInterface + { + return new Image($driver, new Core($this->native())); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::setNative() + * + * @throws InvalidArgumentException + */ + public function setNative(mixed $native): FrameInterface + { + if (!$native instanceof Imagick) { + throw new InvalidArgumentException( + 'Value for argument setNative() "$native" must be instanceof of ' . Imagick::class, + ); + } + + $this->native = $native; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::native() + */ + public function native(): Imagick + { + return $this->native; + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::size() + * + * @throws DriverException + */ + public function size(): SizeInterface + { + try { + return new Size( + $this->native->getImageWidth(), + $this->native->getImageHeight() + ); + } catch (ImagickException | InvalidArgumentException $e) { + throw new DriverException('Failed to get frame size', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::delay() + * + * @throws DriverException + */ + public function delay(): float + { + try { + return $this->native->getImageDelay() / 100; + } catch (ImagickException $e) { + throw new DriverException('Failed to get frame delay', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::setDelay() + * + * @throws DriverException + */ + public function setDelay(float $delay): FrameInterface + { + try { + $this->native->setImageDelay(intval(round($delay * 100))); + } catch (ImagickException $e) { + throw new DriverException('Failed to set frame disposal method', previous: $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::disposalMethod() + * + * @throws DriverException + */ + public function disposalMethod(): int + { + try { + return $this->native->getImageDispose(); + } catch (ImagickException $e) { + throw new DriverException('Failed to get frame disposal method', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::setDisposalMethod() + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public function setDisposalMethod(int $method): FrameInterface + { + if (!in_array($method, [0, 1, 2, 3])) { + throw new InvalidArgumentException('Value for argument disposal method "$method" must be 0, 1, 2 or 3'); + } + + try { + $this->native->setImageDispose($method); + } catch (ImagickException $e) { + throw new DriverException('Failed to set frame disposal method', previous: $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::setOffset() + * + * @throws DriverException + */ + public function setOffset(int $left, int $top): FrameInterface + { + try { + $this->native->setImagePage( + $this->native->getImageWidth(), + $this->native->getImageHeight(), + $left, + $top + ); + } catch (ImagickException $e) { + throw new DriverException('Failed to set frame offset', previous: $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::offsetLeft() + * + * @throws DriverException + */ + public function offsetLeft(): int + { + try { + return $this->native->getImagePage()['x']; + } catch (ImagickException $e) { + throw new DriverException('Failed to get frame offset', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::setOffsetLeft() + * + * @throws RuntimeException + */ + public function setOffsetLeft(int $offset): FrameInterface + { + return $this->setOffset($offset, $this->offsetTop()); + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::offsetTop() + * + * @throws DriverException + */ + public function offsetTop(): int + { + try { + return $this->native->getImagePage()['y']; + } catch (ImagickException $e) { + throw new DriverException('Failed to get frame offset', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see DriverInterface::setOffsetTop() + * + * @throws RuntimeException + */ + public function setOffsetTop(int $offset): FrameInterface + { + return $this->setOffset($this->offsetLeft(), $offset); + } +} diff --git a/src/Drivers/Imagick/Modifiers/BlurModifier.php b/src/Drivers/Imagick/Modifiers/BlurModifier.php new file mode 100644 index 000000000..de431772b --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/BlurModifier.php @@ -0,0 +1,38 @@ +native()->blurImage($this->level, 0.5 * $this->level); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to blur image', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to blur image', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/BrightnessModifier.php b/src/Drivers/Imagick/Modifiers/BrightnessModifier.php new file mode 100644 index 000000000..866fa223b --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/BrightnessModifier.php @@ -0,0 +1,38 @@ +native()->modulateImage(100 + $this->level, 100, 100); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to adjust image brightness', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to adjust image brightness', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/ColorizeModifier.php b/src/Drivers/Imagick/Modifiers/ColorizeModifier.php new file mode 100644 index 000000000..76655fd3e --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ColorizeModifier.php @@ -0,0 +1,60 @@ +normalizeLevel($this->red); + $green = $this->normalizeLevel($this->green); + $blue = $this->normalizeLevel($this->blue); + + foreach ($image as $frame) { + try { + $qrange = $frame->native()->getQuantumRange(); + } catch (ImageException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to get quantum range', + previous: $e + ); + } + + try { + $result = $frame->native()->levelImage(0, $red, $qrange['quantumRangeLong'], Imagick::CHANNEL_RED) + && $frame->native()->levelImage(0, $green, $qrange['quantumRangeLong'], Imagick::CHANNEL_GREEN) + && $frame->native()->levelImage(0, $blue, $qrange['quantumRangeLong'], Imagick::CHANNEL_BLUE); + + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to adjust image colors', + ); + } + } catch (ImageException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to adjust image colors', + previous: $e + ); + } + } + + return $image; + } + + private function normalizeLevel(int $level): int + { + return $level > 0 ? intval(round($level / 5)) : intval(round(($level + 100) / 100)); + } +} diff --git a/src/Drivers/Imagick/Modifiers/ColorspaceModifier.php b/src/Drivers/Imagick/Modifiers/ColorspaceModifier.php new file mode 100644 index 000000000..a5d464155 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ColorspaceModifier.php @@ -0,0 +1,93 @@ +targetColorspace(); + $imagick = $image->core()->native(); + + try { + $result = $imagick->transformImageColorspace( + $this->imagickColorspaceOrFail($colorspace) + ); + + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to transform image colorspace', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to transform image colorspace', + previous: $e + ); + } + + return $image; + } + + /** + * @throws ModifierException + * @throws NotSupportedException + */ + private function imagickColorspaceOrFail(ColorspaceInterface $colorspace): int + { + if ($colorspace instanceof Rgb) { + return Imagick::COLORSPACE_SRGB; + } + + if ($colorspace instanceof Cmyk) { + return Imagick::COLORSPACE_CMYK; + } + + if ($colorspace instanceof Hsl) { + return Imagick::COLORSPACE_HSL; + } + + if ($colorspace instanceof Hsv) { + return Imagick::COLORSPACE_HSB; + } + + try { + if ($colorspace instanceof Oklab && defined(Imagick::class . '::COLORSPACE_OKLAB')) { + return constant(Imagick::class . '::COLORSPACE_OKLAB'); + } + + if ($colorspace instanceof Oklch && defined(Imagick::class . '::COLORSPACE_OKLCH')) { + return constant(Imagick::class . '::COLORSPACE_OKLCH'); + } + } catch (Error $e) { + throw new ModifierException( + 'Failed to convert colorspace to Imagick constant', + previous: $e, + ); + } + + throw new NotSupportedException('Colorspace ' . $colorspace::class . ' is not supported by driver'); + } +} diff --git a/src/Drivers/Imagick/Modifiers/ContainDownModifier.php b/src/Drivers/Imagick/Modifiers/ContainDownModifier.php new file mode 100644 index 000000000..a4173145e --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ContainDownModifier.php @@ -0,0 +1,27 @@ +size() + ->containDown( + $this->width, + $this->height + ) + ->alignPivotTo( + $this->resizeSize($image), + $this->alignment + ); + } +} diff --git a/src/Drivers/Imagick/Modifiers/ContainModifier.php b/src/Drivers/Imagick/Modifiers/ContainModifier.php new file mode 100644 index 000000000..7c066e7ad --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ContainModifier.php @@ -0,0 +1,177 @@ +cropSize($image); + $resize = $this->resizeSize($image); + + try { + $transparent = new ImagickPixel('transparent'); + } catch (ImagickPixelException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to create ImagickPixel', + previous: $e, + ); + } + + $background = $this->driver() + ->colorProcessor($image) + ->export( + $this->backgroundColor() + ); + + foreach ($image as $frame) { + try { + $result = $frame->native()->scaleImage( + $crop->width(), + $crop->height(), + ); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to resize image', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to resize image', + previous: $e + ); + } + + try { + $result = $frame->native()->setBackgroundColor($transparent) + && $frame->native()->setImageBackgroundColor($transparent); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set image background color', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set image background color', + previous: $e + ); + } + + try { + $result = $frame->native()->extentImage( + $resize->width(), + $resize->height(), + $crop->pivot()->x() * -1, + $crop->pivot()->y() * -1 + ); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to resize image', + ); + } + } catch (ImagickException | ImagickDrawException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to resize image', + previous: $e + ); + } + + if ($resize->width() > $crop->width()) { + // fill new emerged background + try { + $draw = new ImagickDraw(); + $draw->setFillColor($background); + + $delta = abs($crop->pivot()->x()); + + if ($delta > 0) { + $draw->rectangle( + 0, + 0, + $delta - 1, + $resize->height() + ); + } + + $draw->rectangle( + $crop->width() + $delta, + 0, + $resize->width(), + $resize->height() + ); + + $result = $frame->native()->drawImage($draw); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable fill new image areas with replacement color', + ); + } + } catch (ImagickException | ImagickDrawException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable fill new image areas with replacement color', + previous: $e + ); + } + } + + if ($resize->height() > $crop->height()) { + // fill new emerged background + try { + $draw = new ImagickDraw(); + $draw->setFillColor($background); + + $delta = abs($crop->pivot()->y()); + + if ($delta > 0) { + $draw->rectangle( + 0, + 0, + $resize->width(), + $delta - 1 + ); + } + + $draw->rectangle( + 0, + $crop->height() + $delta, + $resize->width(), + $resize->height() + ); + + $result = $frame->native()->drawImage($draw); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable fill new image areas with replacement color', + ); + } + } catch (ImagickException | ImagickDrawException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable fill new image areas with replacement color', + previous: $e + ); + } + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/ContrastModifier.php b/src/Drivers/Imagick/Modifiers/ContrastModifier.php new file mode 100644 index 000000000..fec20b7b7 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ContrastModifier.php @@ -0,0 +1,47 @@ +native()->sigmoidalContrastImage($this->level > 0, abs($this->level / 4), $midpoint); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to adjust image contrast', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to adjust image contrast', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/CoverDownModifier.php b/src/Drivers/Imagick/Modifiers/CoverDownModifier.php new file mode 100644 index 000000000..23a3894cb --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/CoverDownModifier.php @@ -0,0 +1,18 @@ +resizeDown($this->width, $this->height); + } +} diff --git a/src/Drivers/Imagick/Modifiers/CoverModifier.php b/src/Drivers/Imagick/Modifiers/CoverModifier.php new file mode 100644 index 000000000..443da14d0 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/CoverModifier.php @@ -0,0 +1,50 @@ +cropSize($image); + $resize = $this->resizeSize($crop); + + foreach ($image as $frame) { + try { + $frame->native()->cropImage( + $crop->width(), + $crop->height(), + $crop->pivot()->x(), + $crop->pivot()->y() + ); + + $frame->native()->scaleImage( + $resize->width(), + $resize->height() + ); + + $frame->native()->setImagePage(0, 0, 0, 0); + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to resize image', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/CropModifier.php b/src/Drivers/Imagick/Modifiers/CropModifier.php new file mode 100644 index 000000000..1e3f40cf4 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/CropModifier.php @@ -0,0 +1,124 @@ +driver()->colorProcessor($image)->export( + $this->backgroundColor() + ); + + try { + // create empty container imagick to rebuild core + $imagick = new Imagick(); + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to create new imagick instance', + previous: $e + ); + } + + // save resolution to add it later + $resolution = $image->resolution()->perInch(); + + // define position of the image on the new canvas + $crop = $this->crop($image); + $position = [ + ($crop->pivot()->x() + $this->x) * -1, + ($crop->pivot()->y() + $this->y) * -1, + ]; + + foreach ($image as $frame) { + // create new frame canvas with modifiers background + try { + $canvas = new Imagick(); + $canvas->newImage($crop->width(), $crop->height(), $background, 'png'); + $canvas->setImageResolution($resolution->x(), $resolution->y()); + $canvas->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET); // or ALPHACHANNEL_ACTIVATE? + $canvas->setImageColorspace(match ($image->colorspace()::class) { + Cmyk::class => Imagick::COLORSPACE_CMYK, + Hsv::class => Imagick::COLORSPACE_HSB, + Hsl::class => Imagick::COLORSPACE_HSL, + default => Imagick::COLORSPACE_SRGB, + }); + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to create new frame canvas', + previous: $e + ); + } + + // set animation details + if ($image->isAnimated()) { + try { + $canvas->setImageDelay($frame->native()->getImageDelay()); + $canvas->setImageIterations($frame->native()->getImageIterations()); + $canvas->setImageDispose($frame->native()->getImageDispose()); + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set animation details', + previous: $e + ); + } + } + + // make the rectangular position of the original image transparent + // so that we can later place the original on top. this preserves + // the transparency of the original and shows the background color + // of the modifier in the other areas. if the original image has no + // transparent area the rectangular transparency will be covered by + // the original. + try { + $clearer = new Imagick(); + $clearer->newImage( + $frame->native()->getImageWidth(), + $frame->native()->getImageHeight(), + new ImagickPixel('black'), + ); + $canvas->compositeImage($clearer, Imagick::COMPOSITE_DSTOUT, ...$position); + $clearer->clear(); + + // place original frame content onto prepared frame canvas + $canvas->compositeImage($frame->native(), Imagick::COMPOSITE_DEFAULT, ...$position); + + // add newly built frame to container imagick + $imagick->addImage($canvas); + $canvas->clear(); + } catch (ImagickException | ImagickPixelException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to clear transparent areas', + previous: $e + ); + } + } + + // replace imagick in the original image + $image->core()->setNative($imagick); + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/DrawBezierModifier.php b/src/Drivers/Imagick/Modifiers/DrawBezierModifier.php new file mode 100644 index 000000000..dc4ec753b --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/DrawBezierModifier.php @@ -0,0 +1,91 @@ +drawable->count() !== 3 && $this->drawable->count() !== 4) { + throw new InvalidArgumentException('You must specify either 3 or 4 points to create a bezier curve'); + } + + try { + $drawing = new ImagickDraw(); + + $drawing->setFillColor( + $this->driver()->colorProcessor($image)->export( + $this->backgroundColor() + ) + ); + + if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 0) { + $borderColor = $this->driver()->colorProcessor($image)->export( + $this->borderColor() + ); + + $drawing->setStrokeColor($borderColor); + $drawing->setStrokeWidth($this->drawable->borderSize()); + } + + $drawing->pathStart(); + $drawing->pathMoveToAbsolute( + $this->drawable->first()->x(), + $this->drawable->first()->y() + ); + if ($this->drawable->count() === 3) { + $drawing->pathCurveToQuadraticBezierAbsolute( + $this->drawable->second()->x(), + $this->drawable->second()->y(), + $this->drawable->last()->x(), + $this->drawable->last()->y() + ); + } elseif ($this->drawable->count() === 4) { + $drawing->pathCurveToAbsolute( + $this->drawable->second()->x(), + $this->drawable->second()->y(), + $this->drawable->third()->x(), + $this->drawable->third()->y(), + $this->drawable->last()->x(), + $this->drawable->last()->y() + ); + } + $drawing->pathFinish(); + + foreach ($image as $frame) { + $result = $frame->native()->drawImage($drawing); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw bezier curve', + ); + } + } + } catch (ImagickException | ImagickDrawException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw bezier curve', + previous: $e, + ); + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/DrawEllipseModifier.php b/src/Drivers/Imagick/Modifiers/DrawEllipseModifier.php new file mode 100644 index 000000000..07238151a --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/DrawEllipseModifier.php @@ -0,0 +1,88 @@ +buildDrawing($image); + + foreach ($image as $frame) { + try { + $result = $frame->native()->drawImage($drawing); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw ellipse on image', + ); + } + } catch (ImagickException | ImagickDrawException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw ellipse on image', + previous: $e + ); + } + } + + return $image; + } + + /** + * @throws ModifierException + * @throws StateException + * @throws ColorDecoderException + */ + private function buildDrawing(ImageInterface $image): ImagickDraw + { + try { + $drawing = new ImagickDraw(); + $drawing->setFillColor( + $this->driver()->colorProcessor($image)->export( + $this->backgroundColor() + ) + ); + + if ($this->drawable->hasBorder()) { + $borderColor = $this->driver()->colorProcessor($image)->export( + $this->borderColor() + ); + + $drawing->setStrokeWidth($this->drawable->borderSize()); + $drawing->setStrokeColor($borderColor); + } + + $drawing->ellipse( + $this->drawable->position()->x(), + $this->drawable->position()->y(), + $this->drawable->width() / 2, + $this->drawable->height() / 2, + 0, + 360 + ); + } catch (ImagickException | ImagickDrawException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to build ImagickDraw object', + previous: $e + ); + } + + return $drawing; + } +} diff --git a/src/Drivers/Imagick/Modifiers/DrawLineModifier.php b/src/Drivers/Imagick/Modifiers/DrawLineModifier.php new file mode 100644 index 000000000..81d95e4da --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/DrawLineModifier.php @@ -0,0 +1,63 @@ +setStrokeWidth($this->drawable->width()); + $drawing->setFillOpacity(0); + + if ($this->drawable->hasBackgroundColor()) { + $drawing->setStrokeColor( + $this->driver()->colorProcessor($image)->export( + $this->backgroundColor() + ) + ); + } + + $drawing->line( + $this->drawable->start()->x(), + $this->drawable->start()->y(), + $this->drawable->end()->x(), + $this->drawable->end()->y(), + ); + } catch (ImagickException | ImagickDrawException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to build ImagickDraw object', + previous: $e + ); + } + + foreach ($image as $frame) { + $result = $frame->native()->drawImage($drawing); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable draw line on image', + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/DrawPixelModifier.php b/src/Drivers/Imagick/Modifiers/DrawPixelModifier.php new file mode 100644 index 000000000..83416fb4e --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/DrawPixelModifier.php @@ -0,0 +1,57 @@ +driver()->colorProcessor($image)->export($this->color()); + + try { + $pixel = new ImagickDraw(); + $pixel->setFillColor($color); + $pixel->point($this->position->x(), $this->position->y()); + } catch (ImagickException | ImagickDrawException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to build ImagickDraw object', + previous: $e + ); + } + + foreach ($image as $frame) { + try { + $result = $frame->native()->drawImage($pixel); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw pixel on image', + ); + } + } catch (ImagickException | ImagickDrawException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw pixel on image', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/DrawPolygonModifier.php b/src/Drivers/Imagick/Modifiers/DrawPolygonModifier.php new file mode 100644 index 000000000..50253231d --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/DrawPolygonModifier.php @@ -0,0 +1,85 @@ +setFillColor( + $this->driver()->colorProcessor($image)->export( + $this->backgroundColor() + ) + ); + + if ($this->drawable->hasBorder()) { + $borderColor = $this->driver()->colorProcessor($image)->export( + $this->borderColor() + ); + + $drawing->setStrokeColor($borderColor); + $drawing->setStrokeWidth($this->drawable->borderSize()); + } + + $drawing->polygon($this->points()); + } catch (ImagickException | ImagickDrawException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to build ImagickDraw object', + previous: $e + ); + } + + foreach ($image as $frame) { + try { + $result = $frame->native()->drawImage($drawing); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw polygon on image', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw polygon on image', + previous: $e + ); + } + } + + return $image; + } + + /** + * Return points of drawable in processable form for ImagickDraw. + * + * @return array> + */ + private function points(): array + { + $points = []; + foreach ($this->drawable as $point) { + $points[] = ['x' => $point->x(), 'y' => $point->y()]; + } + + return $points; + } +} diff --git a/src/Drivers/Imagick/Modifiers/DrawRectangleModifier.php b/src/Drivers/Imagick/Modifiers/DrawRectangleModifier.php new file mode 100644 index 000000000..8aba0e467 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/DrawRectangleModifier.php @@ -0,0 +1,75 @@ +setFillColor( + $this->driver()->colorProcessor($image)->export( + $this->backgroundColor() + ) + ); + + if ($this->drawable->hasBorder()) { + $borderColor = $this->driver()->colorProcessor($image)->export( + $this->borderColor() + ); + + $drawing->setStrokeColor($borderColor); + $drawing->setStrokeWidth($this->drawable->borderSize()); + } + + // build rectangle + $drawing->rectangle( + $this->drawable->position()->x(), + $this->drawable->position()->y(), + $this->drawable->position()->x() + $this->drawable->width(), + $this->drawable->position()->y() + $this->drawable->height() + ); + } catch (ImagickException | ImagickDrawException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to build ImagickDraw object', + previous: $e + ); + } + + foreach ($image as $frame) { + try { + $result = $frame->native()->drawImage($drawing); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw rectangle on image', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw rectangle on image', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/FillModifier.php b/src/Drivers/Imagick/Modifiers/FillModifier.php new file mode 100644 index 000000000..8d3edc9a7 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/FillModifier.php @@ -0,0 +1,119 @@ +driver()->colorProcessor($image)->export( + $this->color() + ); + + foreach ($image->core()->native() as $frame) { + if ($this->hasPosition()) { + $this->floodFillWithColor($frame, $pixel); + } else { + $this->fillAllWithColor($frame, $pixel); + } + } + + return $image; + } + + /** + * @throws ModifierException + */ + private function floodFillWithColor(Imagick $frame, ImagickPixel $pixel): void + { + try { + $target = $frame->getImagePixelColor( + $this->position->x(), + $this->position->y() + ); + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to find target flood fill color', + previous: $e + ); + } + + try { + $result = $frame->floodFillPaintImage( + $pixel, + 100, + $target, + $this->position->x(), + $this->position->y(), + false, + Imagick::CHANNEL_ALL + ); + + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to flood fill image', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to flood fill image', + previous: $e + ); + } + } + + /** + * @throws ModifierException + */ + private function fillAllWithColor(Imagick $frame, ImagickPixel $pixel): void + { + try { + $draw = new ImagickDraw(); + $draw->setFillColor($pixel); + $draw->rectangle(0, 0, $frame->getImageWidth(), $frame->getImageHeight()); + $frame->drawImage($draw); + } catch (ImagickException | ImagickDrawException | ImagickPixelException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to build ImagickDraw object', + previous: $e + ); + } + + try { + // deactive alpha channel when image was filled with opaque color + if ($pixel->getColorValue(Imagick::COLOR_ALPHA) === 1.0) { + $result = $frame->setImageAlphaChannel(Imagick::ALPHACHANNEL_DEACTIVATE); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to adjust alpha channel', + ); + } + } + } catch (ImagickException | ImagickPixelException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to adjust alpha channel', + previous: $e + ); + } + } +} diff --git a/src/Drivers/Imagick/Modifiers/FillTransparentAreasModifier.php b/src/Drivers/Imagick/Modifiers/FillTransparentAreasModifier.php new file mode 100644 index 000000000..e1e245bab --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/FillTransparentAreasModifier.php @@ -0,0 +1,46 @@ +backgroundColor($this->driver()); + + // get imagickpixel from background color + $pixel = $this->driver() + ->colorProcessor($image) + ->export($backgroundColor); + + // merge transparent areas with the background color + foreach ($image as $frame) { + try { + $frame->native()->setImageBackgroundColor($pixel); + $frame->native()->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE); + $frame->native()->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN); + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set image background color', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/FlipModifier.php b/src/Drivers/Imagick/Modifiers/FlipModifier.php new file mode 100644 index 000000000..34b3ea3a6 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/FlipModifier.php @@ -0,0 +1,41 @@ +direction === Direction::HORIZONTAL + ? $frame->native()->flopImage() + : $frame->native()->flipImage(); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to mirror image', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to mirror image', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/GammaModifier.php b/src/Drivers/Imagick/Modifiers/GammaModifier.php new file mode 100644 index 000000000..69aaa2bd0 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/GammaModifier.php @@ -0,0 +1,38 @@ +native()->gammaImage($this->gamma); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to adjust image gamma', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to adjust image gamma', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/GrayscaleModifier.php b/src/Drivers/Imagick/Modifiers/GrayscaleModifier.php new file mode 100644 index 000000000..0af8703ae --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/GrayscaleModifier.php @@ -0,0 +1,38 @@ +native()->modulateImage(100, 0, 100); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to modulate image', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to modulate image', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/InsertModifier.php b/src/Drivers/Imagick/Modifiers/InsertModifier.php new file mode 100644 index 000000000..c5bb3dc82 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/InsertModifier.php @@ -0,0 +1,84 @@ +driver()->decodeImage($this->image); + $position = $this->position($image, $watermark); + + // set opacity of watermark + if ($this->transparency < 1) { + try { + $opacity = (int) round(self::convertRange($this->transparency, 0, 1, 0, 100)); + $alphaEval = $opacity > 0 ? 100 / $opacity : 1000; + + $result = $watermark->core()->native()->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET) + && $watermark->core()->native()->evaluateImage( + Imagick::EVALUATE_DIVIDE, + $alphaEval, + Imagick::CHANNEL_ALPHA, + ); + + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set transparency of watermark', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set transparency of watermark', + previous: $e + ); + } catch (RuntimeException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set transparency of watermark', + previous: $e + ); + } + } + + foreach ($image as $frame) { + try { + $result = $frame->native()->compositeImage( + $watermark->core()->native(), + Imagick::COMPOSITE_DEFAULT, + $position->x(), + $position->y() + ); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to insert watermark image', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to insert watermark image', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/InvertModifier.php b/src/Drivers/Imagick/Modifiers/InvertModifier.php new file mode 100644 index 000000000..b96e0b7f7 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/InvertModifier.php @@ -0,0 +1,46 @@ +native()->negateImage(false, $channel); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to invert image colors', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to invert image colors', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/OrientModifier.php b/src/Drivers/Imagick/Modifiers/OrientModifier.php new file mode 100644 index 000000000..ac776d41c --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/OrientModifier.php @@ -0,0 +1,69 @@ +core()->native()->getImageOrientation(); + $orientation = $orientation === Imagick::ORIENTATION_UNDEFINED + ? $image->core()->meta()->get('originalImageOrientation', 0) + : $orientation; + + try { + $result = match ($orientation) { + Imagick::ORIENTATION_TOPRIGHT + => $image->core()->native()->flopImage(), // 2 + + Imagick::ORIENTATION_BOTTOMRIGHT + => $image->core()->native()->rotateImage('#000', 180), // 3 + + Imagick::ORIENTATION_BOTTOMLEFT + => $image->core()->native()->rotateImage('#000', 180) && $image->core()->native()->flopImage(), // 4 + + Imagick::ORIENTATION_LEFTTOP + => $image->core()->native()->rotateImage('#000', 90) && $image->core()->native()->flopImage(), // 5 + + Imagick::ORIENTATION_RIGHTTOP + => $image->core()->native()->rotateImage('#000', 90), // 6 + + Imagick::ORIENTATION_RIGHTBOTTOM + => $image->core()->native()->rotateImage('#000', 270) && $image->core()->native()->flopImage(), // 7 + + Imagick::ORIENTATION_LEFTBOTTOM + => $image->core()->native()->rotateImage('#000', 270), // 8 + + default => 'value', + }; + + // set new orientation in image + $result = $result && $image->core()->native()->setImageOrientation(Imagick::ORIENTATION_TOPLEFT); + + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to process rotation of image', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to process rotation', + previous: $e + ); + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/PixelateModifier.php b/src/Drivers/Imagick/Modifiers/PixelateModifier.php new file mode 100644 index 000000000..909d6853d --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/PixelateModifier.php @@ -0,0 +1,62 @@ +pixelateFrame($frame); + } + + return $image; + } + + /** + * @throws ModifierException + */ + protected function pixelateFrame(FrameInterface $frame): void + { + $size = $frame->size(); + + try { + $result = $frame->native()->scaleImage( + (int) round(max(1, $size->width() / $this->size)), + (int) round(max(1, $size->height() / $this->size)) + ) && $frame->native()->scaleImage( + $size->width(), + $size->height() + ); + + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to pixelate image', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to pixelate image', + previous: $e + ); + } catch (DivisionByZeroError $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to pixelate image', + previous: $e + ); + } + } +} diff --git a/src/Drivers/Imagick/Modifiers/ProfileModifier.php b/src/Drivers/Imagick/Modifiers/ProfileModifier.php new file mode 100644 index 000000000..dee270ece --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ProfileModifier.php @@ -0,0 +1,38 @@ +core()->native(); + + try { + $result = $imagick->profileImage('icc', (string) $this->profile); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set ICC color profile', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set ICC color profile', + previous: $e + ); + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/ReduceColorsModifier.php b/src/Drivers/Imagick/Modifiers/ReduceColorsModifier.php new file mode 100644 index 000000000..3ade58c09 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ReduceColorsModifier.php @@ -0,0 +1,55 @@ +limit <= 0) { + throw new InvalidArgumentException('Quantization limit must be greater than 0'); + } + + // no color reduction if the limit is higher than the colors in the img + if ($this->limit > $image->core()->native()->getImageColors()) { + return $image; + } + + foreach ($image as $frame) { + try { + $result = $frame->native()->quantizeImage( + $this->limit, + $frame->native()->getImageColorspace(), + 0, + false, + false + ); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to process quantization', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to process quantization', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/RemoveAnimationModifier.php b/src/Drivers/Imagick/Modifiers/RemoveAnimationModifier.php new file mode 100644 index 000000000..1b26da9cc --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/RemoveAnimationModifier.php @@ -0,0 +1,46 @@ +selectedFrame($image); + + $result = $imagick->addImage($frame->native()->getImage()); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to re-apply image frame', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to re-apply image frame', + previous: $e + ); + } + + // set new imagick to image + $image->core()->setNative($imagick); + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/RemoveProfileModifier.php b/src/Drivers/Imagick/Modifiers/RemoveProfileModifier.php new file mode 100644 index 000000000..f008414b7 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/RemoveProfileModifier.php @@ -0,0 +1,38 @@ +core()->native(); + + try { + $result = $imagick->profileImage('icc', null); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to remove ICC color profile', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to remove ICC color profile', + previous: $e + ); + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/ResizeCanvasModifier.php b/src/Drivers/Imagick/Modifiers/ResizeCanvasModifier.php new file mode 100644 index 000000000..3aa332c5b --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ResizeCanvasModifier.php @@ -0,0 +1,35 @@ +cropSize($image); + + $image->modify(new CropModifier( + $cropSize->width(), + $cropSize->height(), + $cropSize->pivot()->x(), + $cropSize->pivot()->y(), + $this->backgroundColor(), + )); + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/ResizeCanvasRelativeModifier.php b/src/Drivers/Imagick/Modifiers/ResizeCanvasRelativeModifier.php new file mode 100644 index 000000000..e1fcb7403 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ResizeCanvasRelativeModifier.php @@ -0,0 +1,16 @@ +size()->resizeDown($this->width, $this->height); + } +} diff --git a/src/Drivers/Imagick/Modifiers/ResizeModifier.php b/src/Drivers/Imagick/Modifiers/ResizeModifier.php new file mode 100644 index 000000000..7f9121ffe --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ResizeModifier.php @@ -0,0 +1,44 @@ +adjustedSize($image); + + foreach ($image as $frame) { + try { + $frame->native()->scaleImage( + $resizeTo->width(), + $resizeTo->height() + ); + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to process resizing', + previous: $e + ); + } + } + + return $image; + } + + protected function adjustedSize(ImageInterface $image): SizeInterface + { + return $image->size()->resize($this->width, $this->height); + } +} diff --git a/src/Drivers/Imagick/Modifiers/ResolutionModifier.php b/src/Drivers/Imagick/Modifiers/ResolutionModifier.php new file mode 100644 index 000000000..d55010a51 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ResolutionModifier.php @@ -0,0 +1,38 @@ +core()->native(); + + try { + $result = $imagick->setImageResolution($this->x, $this->y); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set image resolution', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to set image resolution', + previous: $e + ); + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/RotateModifier.php b/src/Drivers/Imagick/Modifiers/RotateModifier.php new file mode 100644 index 000000000..6a8a21194 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/RotateModifier.php @@ -0,0 +1,45 @@ +driver() + ->colorProcessor($image) + ->export($this->backgroundColor()); + + foreach ($image as $frame) { + try { + $result = $frame->native()->rotateImage($background, $this->rotationAngle()); + + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to rotate image', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to rotate image', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/ScaleDownModifier.php b/src/Drivers/Imagick/Modifiers/ScaleDownModifier.php new file mode 100644 index 000000000..7fd233ef4 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ScaleDownModifier.php @@ -0,0 +1,16 @@ +size()->scaleDown($this->width, $this->height); + } +} diff --git a/src/Drivers/Imagick/Modifiers/ScaleModifier.php b/src/Drivers/Imagick/Modifiers/ScaleModifier.php new file mode 100644 index 000000000..f64e9e2f4 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/ScaleModifier.php @@ -0,0 +1,16 @@ +size()->scale($this->width, $this->height); + } +} diff --git a/src/Drivers/Imagick/Modifiers/SharpenModifier.php b/src/Drivers/Imagick/Modifiers/SharpenModifier.php new file mode 100644 index 000000000..83fdfbd95 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/SharpenModifier.php @@ -0,0 +1,38 @@ +native()->unsharpMaskImage(1, 1, $this->level / 6.25, 0); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to process unsharp mask', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to process unsharp mask', + previous: $e + ); + } + } + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/SliceAnimationModifier.php b/src/Drivers/Imagick/Modifiers/SliceAnimationModifier.php new file mode 100644 index 000000000..c798faa44 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/SliceAnimationModifier.php @@ -0,0 +1,27 @@ +offset >= $image->count()) { + throw new InvalidArgumentException('Offset is not in the range of frames'); + } + + $image->core()->slice($this->offset, $this->length); + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/StripMetaModifier.php b/src/Drivers/Imagick/Modifiers/StripMetaModifier.php new file mode 100644 index 000000000..63d311f88 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/StripMetaModifier.php @@ -0,0 +1,70 @@ +core()->native()->getImageProfiles('icc'); + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to preserve icc profiles', + previous: $e + ); + } + + // remove meta data + try { + $result = $image->core()->native()->stripImage(); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to strip meta data', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to strip meta data', + previous: $e + ); + } + + $image->setExif(new Collection()); + + if ($profiles !== []) { + // re-apply icc profiles + try { + $result = $image->core()->native()->profileImage("icc", $profiles['icc']); + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to re-apply icc profile', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to re-apply icc profile', + previous: $e + ); + } + } + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/TextModifier.php b/src/Drivers/Imagick/Modifiers/TextModifier.php new file mode 100644 index 000000000..a6e502997 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/TextModifier.php @@ -0,0 +1,158 @@ +processor()->textBlock($this->text, $this->font, $this->position); + $drawText = $this->imagickDrawText($image, $this->font); + $drawStroke = $this->imagickDrawStroke($image, $this->font); + + foreach ($image as $frame) { + foreach ($lines as $line) { + foreach ($this->strokeOffsets($this->font) as $offset) { + // Draw the stroke outline under the actual text + $this->maybeDrawTextline($frame, $line, $drawStroke, $offset); + } + + // Draw the actual text + $this->maybeDrawTextline($frame, $line, $drawText); + } + } + + return $image; + } + + /** + * Create an ImagickDraw object to draw text on the image + * + * @throws StateException + * @throws DriverException + * @throws RuntimeException + */ + private function imagickDrawText(ImageInterface $image, FontInterface $font): ImagickDraw + { + $color = $this->driver()->decodeColor($font->color()); + + if ($font->hasStrokeEffect() && $color->isTransparent()) { + throw new StateException( + 'The text color must be fully opaque when using the stroke effect' + ); + } + + $color = $this->driver()->colorProcessor($image)->export($color); + + return $this->processor()->toImagickDraw($font, $color); + } + + /** + * Create a ImagickDraw object to draw the outline stroke effect on the Image + * + * @throws StateException + * @throws DriverException + * @throws RuntimeException + */ + private function imagickDrawStroke(ImageInterface $image, FontInterface $font): ?ImagickDraw + { + if (!$font->hasStrokeEffect()) { + return null; + } + + $color = $this->driver()->decodeColor($font->strokeColor()); + + if ($color->isTransparent()) { + throw new StateException( + 'The stroke color must be fully opaque' + ); + } + + $color = $this->driver()->colorProcessor($image)->export($color); + + return $this->processor()->toImagickDraw($font, $color); + } + + /** + * Maybe draw given line of text on frame instance depending on given + * ImageDraw instance. Optionally move line position by given offset. + * + * @throws ModifierException + */ + private function maybeDrawTextline( + FrameInterface $frame, + Line $textline, + ?ImagickDraw $draw = null, + PointInterface $offset = new Point(), + ): void { + if ($draw instanceof ImagickDraw) { + try { + $result = $frame->native()->annotateImage( + $draw, + $textline->position()->x() + $offset->x(), + $textline->position()->y() + $offset->y(), + $this->font->angle(), + (string) $textline + ); + } catch (ImageException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw text line', + previous: $e + ); + } + + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to draw text line', + ); + } + } + } + + /** + * Return imagick font processor + * + * @throws DriverException + * @throws StateException + */ + private function processor(): FontProcessor + { + $processor = $this->driver()->fontProcessor(); + + if (!$processor instanceof FontProcessor) { + throw new DriverException('Font processor does not match the driver'); + } + + return $processor; + } +} diff --git a/src/Drivers/Imagick/Modifiers/TrimModifier.php b/src/Drivers/Imagick/Modifiers/TrimModifier.php new file mode 100644 index 000000000..100e60352 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/TrimModifier.php @@ -0,0 +1,46 @@ +isAnimated()) { + throw new NotSupportedException('Trim modifier cannot be applied to animated images'); + } + + $imagick = $image->core()->native(); + + try { + $result = $imagick->trimImage(($this->tolerance / 100 * $imagick->getQuantum()) / 1.5) + && $imagick->setImagePage(0, 0, 0, 0); + + if ($result === false) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to processs image trimming', + ); + } + } catch (ImagickException $e) { + throw new ModifierException( + 'Failed to apply ' . self::class . ', unable to processs image trimming', + previous: $e + ); + } + + return $image; + } +} diff --git a/src/Drivers/Specializable.php b/src/Drivers/Specializable.php new file mode 100644 index 000000000..30a1c3f2e --- /dev/null +++ b/src/Drivers/Specializable.php @@ -0,0 +1,13 @@ +analyze($this); + } +} diff --git a/src/Drivers/SpecializableDecoder.php b/src/Drivers/SpecializableDecoder.php new file mode 100644 index 000000000..5acbe384a --- /dev/null +++ b/src/Drivers/SpecializableDecoder.php @@ -0,0 +1,41 @@ +modify($this); + } +} diff --git a/src/EncodedImage.php b/src/EncodedImage.php new file mode 100644 index 000000000..b1a7704ca --- /dev/null +++ b/src/EncodedImage.php @@ -0,0 +1,94 @@ +mediaType; + } + + /** + * {@inheritdoc} + * + * @see EncodedImageInterface::mimetype() + */ + public function mimetype(): string + { + return $this->mediaType(); + } + + /** + * {@inheritdoc} + * + * @see EncodedImageInterface::toDataUri() + * + * @throws StreamException + */ + public function toDataUri(): DataUriInterface + { + return DataUri::create( + data: (string) $this, + mediaType: $this->mediaType(), + ); + } + + /** + * {@inheritdoc} + * + * @see EncodedImageInterface::toBase64() + * + * @throws StreamException + */ + public function toBase64(): string + { + return base64_encode((string) $this); + } + + /** + * Show debug info for the current image. + * + * @return array + */ + public function __debugInfo(): array + { + try { + $size = $this->size(); + } catch (Throwable) { + $size = 0; + } + + return [ + 'mediaType' => $this->mediaType(), + 'size' => $size, + ]; + } +} diff --git a/src/Encoders/AutoEncoder.php b/src/Encoders/AutoEncoder.php new file mode 100644 index 000000000..0c909d1c5 --- /dev/null +++ b/src/Encoders/AutoEncoder.php @@ -0,0 +1,25 @@ +encode( + $this->encoderByMediaType( + $image->origin()->mediaType() + ) + ); + } +} diff --git a/src/Encoders/AvifEncoder.php b/src/Encoders/AvifEncoder.php new file mode 100644 index 000000000..67d3deed7 --- /dev/null +++ b/src/Encoders/AvifEncoder.php @@ -0,0 +1,26 @@ + 100) { + throw new InvalidArgumentException('Quality must be in range 0 to 100'); + } + } +} diff --git a/src/Encoders/BmpEncoder.php b/src/Encoders/BmpEncoder.php new file mode 100644 index 000000000..46dbc9f56 --- /dev/null +++ b/src/Encoders/BmpEncoder.php @@ -0,0 +1,15 @@ + + */ + protected array $options = []; + + /** + * Create new encoder instance to encode to format of given file extension. + * + * @param null|string|FileExtension $extension Target file extension for example "png" + * @throws InvalidArgumentException + * @throws NotSupportedException + */ + public function __construct(public null|string|FileExtension $extension = null, mixed ...$options) + { + if ($extension === '') { + throw new InvalidArgumentException('Unable to find file extension from empty string'); + } + + $mediaType = null; + + if (is_string($extension)) { + try { + $mediaType = FileExtension::from(strtolower($extension))->mediaType(); + } catch (Error) { + throw new NotSupportedException( + 'Unable to find encoder for unknown file extension "' . $extension . '"', + ); + } + } + + if ($extension instanceof FileExtension) { + $mediaType = $extension->mediaType(); + } + + parent::__construct($mediaType, ...$options); + } + + /** + * {@inheritdoc} + * + * @see EncoderInterface::encode() + * + * @throws NotSupportedException + * @throws InvalidArgumentException + */ + public function encode(ImageInterface $image): EncodedImageInterface + { + $extension = is_null($this->extension) ? $image->origin()->fileExtension() : $this->extension; + + if ($extension === null) { + throw new NotSupportedException('Unable to find encoder by unknown origin file extension'); + } + + return $image->encode( + $this->encoderByFileExtension( + $extension + ) + ); + } + + /** + * Create matching encoder for given file extension + * + * @throws InvalidArgumentException + * @throws NotSupportedException + */ + protected function encoderByFileExtension(string|FileExtension $extension): EncoderInterface + { + if ($extension === '') { + throw new InvalidArgumentException('Argument $extension must not be an empty string'); + } + + try { + $extension = is_string($extension) ? FileExtension::from(strtolower($extension)) : $extension; + } catch (Error) { + throw new NotSupportedException( + 'Unable to find encoder for unknown image file extension "' . $extension . '"', + ); + } + + return $extension->format()->encoder(...$this->options); + } +} diff --git a/src/Encoders/FilePathEncoder.php b/src/Encoders/FilePathEncoder.php new file mode 100644 index 000000000..84103c394 --- /dev/null +++ b/src/Encoders/FilePathEncoder.php @@ -0,0 +1,51 @@ +path) ? + $image->origin()->fileExtension() : + pathinfo($this->path, PATHINFO_EXTENSION); + + if ($extension === null || $extension === '') { + throw new InvalidArgumentException( + 'Unable to extract file extension from path "' . $this->path . '"', + ); + } + + return $image->encode( + $this->encoderByFileExtension( + $extension + ) + ); + } +} diff --git a/src/Encoders/FormatEncoder.php b/src/Encoders/FormatEncoder.php new file mode 100644 index 000000000..5f3413173 --- /dev/null +++ b/src/Encoders/FormatEncoder.php @@ -0,0 +1,47 @@ + + */ + protected array $options = []; + + /** + * Create new encoder instance to encode to given format. + */ + public function __construct(protected ?Format $format = null, mixed ...$options) + { + $this->options = $options; + } + + /** + * {@inheritdoc} + * + * @see EncoderInterface::encode() + * + * @throws NotSupportedException + */ + public function encode(ImageInterface $image): EncodedImageInterface + { + try { + $format = is_null($this->format) ? $image->origin()->format() : $this->format; + } catch (NotSupportedException $e) { + throw new NotSupportedException('Unable to find encoder by unknown origin image format', previous: $e); + } + + return $format->encoder(...$this->options)->encode($image); + } +} diff --git a/src/Encoders/GifEncoder.php b/src/Encoders/GifEncoder.php new file mode 100644 index 000000000..ae3fe47ee --- /dev/null +++ b/src/Encoders/GifEncoder.php @@ -0,0 +1,18 @@ + 100) { + throw new InvalidArgumentException('Quality must be in range 0 to 100'); + } + } +} diff --git a/src/Encoders/IcoEncoder.php b/src/Encoders/IcoEncoder.php new file mode 100644 index 000000000..f3626347a --- /dev/null +++ b/src/Encoders/IcoEncoder.php @@ -0,0 +1,18 @@ + 100) { + throw new InvalidArgumentException('Quality must be in range 0 to 100'); + } + } +} diff --git a/src/Encoders/JpegEncoder.php b/src/Encoders/JpegEncoder.php new file mode 100644 index 000000000..62ed680ee --- /dev/null +++ b/src/Encoders/JpegEncoder.php @@ -0,0 +1,27 @@ + 100) { + throw new InvalidArgumentException('Quality must be in range 0 to 100'); + } + } +} diff --git a/src/Encoders/MediaTypeEncoder.php b/src/Encoders/MediaTypeEncoder.php new file mode 100644 index 000000000..d932a97f9 --- /dev/null +++ b/src/Encoders/MediaTypeEncoder.php @@ -0,0 +1,67 @@ + + */ + protected array $options = []; + + /** + * Create new encoder instance. + * + * @param null|string|MediaType $mediaType Target media type for example "image/jpeg" + */ + public function __construct(public null|string|MediaType $mediaType = null, mixed ...$options) + { + $this->options = $options; + } + + /** + * {@inheritdoc} + * + * @see EncoderInterface::encode() + * + * @throws NotSupportedException + */ + public function encode(ImageInterface $image): EncodedImageInterface + { + $mediaType = is_null($this->mediaType) ? $image->origin()->mediaType() : $this->mediaType; + + return $image->encode( + $this->encoderByMediaType($mediaType) + ); + } + + /** + * Return new encoder by given media (MIME) type. + * + * @throws NotSupportedException + */ + protected function encoderByMediaType(string|MediaType $mediaType): EncoderInterface + { + try { + $mediaType = is_string($mediaType) ? MediaType::from($mediaType) : $mediaType; + } catch (Error) { + throw new NotSupportedException( + 'Unable to find encoder for unknown image media type "' . $mediaType . '"', + ); + } + + return $mediaType->format()->encoder(...$this->options); + } +} diff --git a/src/Encoders/PngEncoder.php b/src/Encoders/PngEncoder.php new file mode 100644 index 000000000..376bc0a6c --- /dev/null +++ b/src/Encoders/PngEncoder.php @@ -0,0 +1,18 @@ + 100) { + throw new InvalidArgumentException('Quality must be in range 0 to 100'); + } + } +} diff --git a/src/Encoders/WebpEncoder.php b/src/Encoders/WebpEncoder.php new file mode 100644 index 000000000..7bc290c7b --- /dev/null +++ b/src/Encoders/WebpEncoder.php @@ -0,0 +1,26 @@ + 100) { + throw new InvalidArgumentException('Quality must be in range 0 to 100'); + } + } +} diff --git a/src/Exceptions/AnalyzerException.php b/src/Exceptions/AnalyzerException.php new file mode 100644 index 000000000..48342f439 --- /dev/null +++ b/src/Exceptions/AnalyzerException.php @@ -0,0 +1,10 @@ +stream = self::buildStreamOrFail($data); + } + + /** + * {@inheritdoc} + * + * @see FileInterface::fromPath() + * + * @throws InvalidArgumentException + * @throws DirectoryNotFoundException + * @throws FileNotFoundException + * @throws FileNotReadableException + * @throws StreamException + */ + public static function fromPath(string $path): self + { + $stream = fopen(self::readableFilePathOrFail($path), 'r'); + + if ($stream === false) { + throw new FileNotReadableException('Failed to open file from path "' . $path . '"'); + } + + return new self($stream); + } + + /** + * {@inheritdoc} + * + * @see FileInterface::save() + * + * @throws InvalidArgumentException + * @throws DirectoryNotFoundException + * @throws FileNotWritableException + * @throws StreamException + */ + public function save(string $path): void + { + if ($path === '') { + throw new InvalidArgumentException('Path must not be an empty string'); + } + + if (strlen($path) > PHP_MAXPATHLEN) { + throw new InvalidArgumentException( + "Path is longer than the configured max. value of " . PHP_MAXPATHLEN + ); + } + + $dir = pathinfo($path, PATHINFO_DIRNAME); + + if (!is_dir($dir)) { + throw new DirectoryNotFoundException( + 'Can\'t write to path. Directory "' . $dir . '" does not exist' + ); + } + + if (!is_writable($dir)) { + throw new FileNotWritableException( + 'Can\'t write to path. Directory "' . $dir . '" is not writable' + ); + } + + if (is_file($path) && !is_writable($path)) { + throw new FileNotWritableException( + "Can't write to path. Existing file " . $path . " is not writable" + ); + } + + // write data + $saved = file_put_contents($path, $this->toStream()); + + if ($saved === false) { + throw new FileNotWritableException( + "Failed to write file to path " . $path + ); + } + } + + /** + * {@inheritdoc} + * + * @see FileInterface::toString() + * + * @throws StreamException + */ + public function toString(): string + { + $data = stream_get_contents($this->toStream(), offset: 0); + + if ($data === false) { + throw new StreamException('Unable to read data from stream'); + } + + return $data; + } + + /** + * {@inheritdoc} + * + * @see FileInterface::toStream() + * + * @throws StreamException + */ + public function toStream() + { + $rewind = rewind($this->stream); + + if ($rewind === false) { + throw new StreamException('Failed to rewind stream'); + } + + return $this->stream; + } + + /** + * {@inheritdoc} + * + * @see FileInterface::size() + * + * @throws StreamException + */ + public function size(): int + { + $info = fstat($this->toStream()); + + if (!is_array($info)) { + throw new StreamException('Unable to read size of stream'); + } + + return intval($info['size']); + } + + /** + * {@inheritdoc} + * + * @see FileInterface::__toString() + * + * @throws StreamException + */ + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/src/FileExtension.php b/src/FileExtension.php new file mode 100644 index 000000000..569a66245 --- /dev/null +++ b/src/FileExtension.php @@ -0,0 +1,146 @@ +fileExtension(); + } catch (NotSupportedException $e) { + throw new InvalidArgumentException( + 'Unable to create file extension from "' . $identifier::class . '"', + previous: $e + ); + } + } + + if ($identifier instanceof MediaType) { + try { + return $identifier->fileExtension(); + } catch (NotSupportedException $e) { + throw new InvalidArgumentException( + 'Unable to create file extension from "' . $identifier->value . '"', + previous: $e + ); + } + } + + try { + $extension = self::from(strtolower($identifier)); + } catch (Error) { + try { + $extension = MediaType::from(strtolower($identifier))->fileExtension(); + } catch (Error | NotSupportedException) { + throw new InvalidArgumentException('Unable to create file extension from "' . $identifier . '"'); + } + } + + return $extension; + } + + /** + * Try to create media type from given identifier and return null on failure. + */ + public static function tryCreate(string|self|Format|MediaType $identifier): ?self + { + try { + return self::create($identifier); + } catch (InvalidArgumentException) { + return null; + } + } + + /** + * Return the matching format for the current file extension. + */ + public function format(): Format + { + return match ($this) { + self::JPEG, + self::JPG, + self::PJPEG, + self::PJPG => Format::JPEG, + self::WEBP => Format::WEBP, + self::GIF => Format::GIF, + self::PNG => Format::PNG, + self::AVIF => Format::AVIF, + self::BMP => Format::BMP, + self::TIF, + self::TIFF => Format::TIFF, + self::JP2, + self::JP2K, + self::J2K, + self::JPF, + self::JPM, + self::JPG2, + self::J2C, + self::JPC, + self::JPX => Format::JP2, + self::HEIC, + self::HEIF => Format::HEIC, + self::ICO => Format::ICO, + }; + } + + /** + * Return media types for the current file extension. + * + * @return array + */ + public function mediaTypes(): array + { + return $this->format()->mediaTypes(); + } + + /** + * Return the first found media type for the current file extension. + * + * @throws NotSupportedException + */ + public function mediaType(): MediaType + { + return $this->format()->mediaType(); + } +} diff --git a/src/Format.php b/src/Format.php new file mode 100644 index 000000000..24325ed16 --- /dev/null +++ b/src/Format.php @@ -0,0 +1,185 @@ +format(); + } + + if ($identifier instanceof FileExtension) { + return $identifier->format(); + } + + try { + $format = MediaType::from(strtolower($identifier))->format(); + } catch (Error) { + try { + $format = FileExtension::from(strtolower($identifier))->format(); + } catch (Error) { + throw new InvalidArgumentException('Unable to create format from "' . $identifier . '"'); + } + } + + return $format; + } + + /** + * Try to create format from given identifier and return null on failure. + * + * @param string|Format|MediaType|FileExtension $identifier + * @return Format|null + */ + public static function tryCreate(string|self|MediaType|FileExtension $identifier): ?self + { + try { + return self::create($identifier); + } catch (InvalidArgumentException) { + return null; + } + } + + /** + * Return the possible media (MIME) types for the current format. + * + * @return array + */ + public function mediaTypes(): array + { + return array_filter( + MediaType::cases(), + fn(MediaType $mediaType): bool => $mediaType->format() === $this + ); + } + + /** + * Return the first found media type for the current format. + * + * @throws NotSupportedException + */ + public function mediaType(): MediaType + { + $types = $this->mediaTypes(); + + $result = reset($types); + + if (!$result instanceof MediaType) { + throw new NotSupportedException('Unable to retrieve unsupported media type from format'); + } + + return $result; + } + + /** + * Return the possible file extension for the current format. + * + * @return array + */ + public function fileExtensions(): array + { + return array_filter( + FileExtension::cases(), + fn(FileExtension $fileExtension): bool => $fileExtension->format() === $this + ); + } + + /** + * Return the first found file extension for the current format. + * + * @throws NotSupportedException + */ + public function fileExtension(): FileExtension + { + $extensions = $this->fileExtensions(); + + $result = reset($extensions); + + if (!$result instanceof FileExtension) { + throw new NotSupportedException('Unable to retrieve unsupported file extension for format'); + } + + return $result; + } + + /** + * Create an encoder instance with given options that matches the format. + */ + public function encoder(mixed ...$options): EncoderInterface + { + // get classname of target encoder from current format + $classname = match ($this) { + self::AVIF => AvifEncoder::class, + self::BMP => BmpEncoder::class, + self::GIF => GifEncoder::class, + self::HEIC => HeicEncoder::class, + self::ICO => IcoEncoder::class, + self::JP2 => Jpeg2000Encoder::class, + self::JPEG => JpegEncoder::class, + self::PNG => PngEncoder::class, + self::TIFF => TiffEncoder::class, + self::WEBP => WebpEncoder::class, + }; + + // get parameters of target encoder + $parameters = []; + $reflectionClass = new ReflectionClass($classname); + $constructor = $reflectionClass->getConstructor(); + if ($constructor !== null) { + $parameters = array_map( + fn(ReflectionParameter $parameter): string => $parameter->getName(), + $constructor->getParameters(), + ); + } + + // filter out unavailable options of target encoder + $options = array_filter( + $options, + fn(mixed $key): bool => in_array($key, $parameters), + ARRAY_FILTER_USE_KEY, + ); + + return new $classname(...$options); + } +} diff --git a/src/Fraction.php b/src/Fraction.php new file mode 100644 index 000000000..62dddab3c --- /dev/null +++ b/src/Fraction.php @@ -0,0 +1,44 @@ + 1, + self::HALF => .5, + self::THIRD => .3333333333, + self::TWO_THIRDS => .6666666667, + self::QUARTER => .25, + self::THREE_QUARTER => .75, + self::ONE_AND_A_HALF => 1.5, + self::DOUBLE => 2, + self::TRIPLE => 3, + }; + } + + /** + * Calculate fraction of given value. + */ + public function of(int|float $value): float + { + return round($value * $this->multiplier(), 9); + } +} diff --git a/src/Geometry/Bezier.php b/src/Geometry/Bezier.php new file mode 100644 index 000000000..8b9602224 --- /dev/null +++ b/src/Geometry/Bezier.php @@ -0,0 +1,244 @@ + + * @implements ArrayAccess + */ +class Bezier implements IteratorAggregate, Countable, ArrayAccess, DrawableInterface +{ + use HasBorder; + use HasBackgroundColor; + + /** + * Create new bezier instance. + * + * @param array $points + */ + public function __construct( + protected array $points = [], + protected PointInterface $pivot = new Point() + ) { + // + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::position() + */ + public function position(): PointInterface + { + return $this->pivot; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::setPosition() + */ + public function setPosition(PointInterface $position): self + { + $this->pivot = $position; + + return $this; + } + + /** + * Implement iteration through all points of bezier. + * + * @return Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->points); + } + + /** + * Return current pivot point. + */ + public function pivot(): PointInterface + { + return $this->pivot; + } + + /** + * Change pivot point to given point. + */ + public function setPivot(PointInterface $pivot): self + { + $this->pivot = $pivot; + + return $this; + } + + /** + * Return first control point of bezier. + */ + public function first(): ?PointInterface + { + if ($point = reset($this->points)) { + return $point; + } + + return null; + } + + /** + * Return second control point of bezier. + */ + public function second(): ?PointInterface + { + if (array_key_exists(1, $this->points)) { + return $this->points[1]; + } + + return null; + } + + /** + * Return third control point of bezier. + */ + public function third(): ?PointInterface + { + if (array_key_exists(2, $this->points)) { + return $this->points[2]; + } + + return null; + } + + /** + * Return last control point of bezier. + */ + public function last(): ?PointInterface + { + if ($point = end($this->points)) { + return $point; + } + + return null; + } + + /** + * Return bezier's point count. + */ + public function count(): int + { + return count($this->points); + } + + /** + * Determine if point exists at given offset. + */ + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->points); + } + + /** + * Return point at given offset. + */ + public function offsetGet(mixed $offset): mixed + { + return $this->points[$offset]; + } + + /** + * Set point at given offset. + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->points[$offset] = $value; + } + + /** + * Unset offset at given offset. + */ + public function offsetUnset(mixed $offset): void + { + unset($this->points[$offset]); + } + + /** + * Add given point to bezier. + */ + public function addPoint(PointInterface $point): self + { + $this->points[] = $point; + + return $this; + } + + /** + * Return array of all x/y values of all points of bezier. + * + * @return array + */ + public function toArray(): array + { + $coordinates = []; + foreach ($this->points as $point) { + $coordinates[] = $point->x(); + $coordinates[] = $point->y(); + } + + return $coordinates; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::factory() + */ + public function factory(): DrawableFactoryInterface + { + return new BezierFactory($this); + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::adjust() + */ + public function adjust(callable $adjustments): DrawableInterface + { + $factory = $this->factory(); + $adjustments($factory); + + return $factory->drawable(); + } + + /** + * Clone bezier. + */ + public function __clone(): void + { + $this->points = array_map(fn($point) => clone $point, $this->points); + $this->pivot = clone $this->pivot; + + if ($this->backgroundColor instanceof AbstractColor) { + $this->backgroundColor = clone $this->backgroundColor; + } + + if ($this->borderColor instanceof AbstractColor) { + $this->borderColor = clone $this->borderColor; + } + } +} diff --git a/src/Geometry/Circle.php b/src/Geometry/Circle.php new file mode 100644 index 000000000..790fb0c6f --- /dev/null +++ b/src/Geometry/Circle.php @@ -0,0 +1,67 @@ +setWidth($diameter); + $this->setHeight($diameter); + + return $this; + } + + /** + * Get diameter of circle. + */ + public function diameter(): int + { + return $this->width(); + } + + /** + * Set radius of circle. + */ + public function setRadius(int $radius): self + { + return $this->setDiameter(intval($radius * 2)); + } + + /** + * Get radius of circle. + */ + public function radius(): int + { + return intval(round($this->diameter() / 2)); + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::factory() + */ + public function factory(): DrawableFactoryInterface + { + return new CircleFactory($this); + } +} diff --git a/src/Geometry/Ellipse.php b/src/Geometry/Ellipse.php new file mode 100644 index 000000000..878711064 --- /dev/null +++ b/src/Geometry/Ellipse.php @@ -0,0 +1,143 @@ +pivot; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::setPosition() + */ + public function setPosition(PointInterface $position): self + { + $this->pivot = $position; + + return $this; + } + + /** + * Return pivot point of Ellipse. + */ + public function pivot(): PointInterface + { + return $this->pivot; + } + + /** + * Set size of Ellipse. + */ + public function setSize(int $width, int $height): self + { + return $this->setWidth($width)->setHeight($height); + } + + /** + * Set width of Ellipse. + */ + public function setWidth(int $width): self + { + $this->width = $width; + + return $this; + } + + /** + * Set height of Ellipse. + */ + public function setHeight(int $height): self + { + $this->height = $height; + + return $this; + } + + /** + * Get width of Ellipse. + */ + public function width(): int + { + return $this->width; + } + + /** + * Get height of Ellipse. + */ + public function height(): int + { + return $this->height; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::factory() + */ + public function factory(): DrawableFactoryInterface + { + return new EllipseFactory($this); + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::adjust() + */ + public function adjust(callable $adjustments): DrawableInterface + { + $factory = $this->factory(); + $adjustments($factory); + + return $factory->drawable(); + } + + /** + * Clone ellipse. + */ + public function __clone(): void + { + $this->pivot = clone $this->pivot; + + if ($this->backgroundColor instanceof AbstractColor) { + $this->backgroundColor = clone $this->backgroundColor; + } + + if ($this->borderColor instanceof AbstractColor) { + $this->borderColor = clone $this->borderColor; + } + } +} diff --git a/src/Geometry/Factories/BezierFactory.php b/src/Geometry/Factories/BezierFactory.php new file mode 100644 index 000000000..dae2dd52d --- /dev/null +++ b/src/Geometry/Factories/BezierFactory.php @@ -0,0 +1,81 @@ +bezier = $bezier instanceof Bezier ? clone $bezier : new Bezier([]); + + if (is_callable($bezier)) { + $bezier($this); + } + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::build() + */ + public static function build(null|callable|DrawableInterface $drawable = null): Bezier + { + return (new self($drawable))->drawable(); + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::drawable() + */ + public function drawable(): Bezier + { + return $this->bezier; + } + + /** + * Add a point to the bezier to be produced. + */ + public function point(int $x, int $y): self + { + $this->bezier->addPoint(new Point($x, $y)); + + return $this; + } + + /** + * Set the background color of the bezier to be produced. + */ + public function background(string|ColorInterface $color): self + { + $this->bezier->setBackgroundColor($color); + + return $this; + } + + /** + * Set the border color & border size of the bezier to be produced. + * + * @throws InvalidArgumentException + */ + public function border(string|ColorInterface $color, int $size = 1): self + { + $this->bezier->setBorder($color, $size); + + return $this; + } +} diff --git a/src/Geometry/Factories/CircleFactory.php b/src/Geometry/Factories/CircleFactory.php new file mode 100644 index 000000000..7409dd3dd --- /dev/null +++ b/src/Geometry/Factories/CircleFactory.php @@ -0,0 +1,100 @@ +circle = $circle instanceof Circle ? clone $circle : new Circle(0); + + if (is_callable($circle)) { + $circle($this); + } + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::build() + */ + public static function build(null|callable|DrawableInterface $drawable = null): Circle + { + return (new self($drawable))->drawable(); + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::drawable() + */ + public function drawable(): Circle + { + return $this->circle; + } + + /** + * Set the radius of the circle to be produced. + */ + public function radius(int $radius): self + { + $this->circle->setSize($radius * 2, $radius * 2); + + return $this; + } + + /** + * Set the diameter of the circle to be produced. + */ + public function diameter(int $diameter): self + { + $this->circle->setSize($diameter, $diameter); + + return $this; + } + + /** + * Set the background color of the circle to be produced. + */ + public function background(string|ColorInterface $color): self + { + $this->circle->setBackgroundColor($color); + + return $this; + } + + /** + * Set the border color & border size of the circle to be produced. + * + * @throws InvalidArgumentException + */ + public function border(string|ColorInterface $color, int $size = 1): self + { + $this->circle->setBorder($color, $size); + + return $this; + } + + /** + * Set the position where the circle should be drawn. + */ + public function at(int $x, int $y): self + { + $this->circle->position()->setPosition($x, $y); + + return $this; + } +} diff --git a/src/Geometry/Factories/Drawable.php b/src/Geometry/Factories/Drawable.php new file mode 100644 index 000000000..39efc0f9d --- /dev/null +++ b/src/Geometry/Factories/Drawable.php @@ -0,0 +1,76 @@ +ellipse = $ellipse instanceof Ellipse ? clone $ellipse : new Ellipse(0, 0); + + if (is_callable($ellipse)) { + $ellipse($this); + } + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::build() + */ + public static function build(null|callable|DrawableInterface $drawable = null): Ellipse + { + return (new self($drawable))->drawable(); + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::drawable() + */ + public function drawable(): Ellipse + { + return $this->ellipse; + } + + /** + * Set the size of the ellipse to be produced. + */ + public function size(int $width, int $height): self + { + $this->ellipse->setSize($width, $height); + + return $this; + } + + /** + * Set the width of the ellipse to be produced. + */ + public function width(int $width): self + { + $this->ellipse->setWidth($width); + + return $this; + } + + /** + * Set the height of the ellipse to be produced. + */ + public function height(int $height): self + { + $this->ellipse->setHeight($height); + + return $this; + } + + /** + * Set the background color of the ellipse to be produced. + */ + public function background(string|ColorInterface $color): self + { + $this->ellipse->setBackgroundColor($color); + + return $this; + } + + /** + * Set the border color & border size of the ellipse to be produced. + * + * @throws InvalidArgumentException + */ + public function border(string|ColorInterface $color, int $size = 1): self + { + $this->ellipse->setBorder($color, $size); + + return $this; + } + + /** + * Set the position where the ellipse should be drawn. + */ + public function at(int $x, int $y): self + { + $this->ellipse->position()->setPosition($x, $y); + + return $this; + } +} diff --git a/src/Geometry/Factories/LineFactory.php b/src/Geometry/Factories/LineFactory.php new file mode 100644 index 000000000..4d18fc1de --- /dev/null +++ b/src/Geometry/Factories/LineFactory.php @@ -0,0 +1,112 @@ +line = $line instanceof Line ? clone $line : new Line(new Point(), new Point()); + + if (is_callable($line)) { + $line($this); + } + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::build() + */ + public static function build(null|callable|DrawableInterface $drawable = null): Line + { + return (new self($drawable))->drawable(); + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::drawable() + */ + public function drawable(): Line + { + return $this->line; + } + + /** + * Set the color of the line to be produced. + */ + public function color(string|ColorInterface $color): self + { + $this->line->setBackgroundColor($color); + $this->line->setBorderColor($color); + + return $this; + } + + /** + * Set the (background) color of the line to be produced. + */ + public function background(string|ColorInterface $color): self + { + $this->line->setBackgroundColor($color); + $this->line->setBorderColor($color); + + return $this; + } + + /** + * Set the border size & border color of the line to be produced. + */ + public function border(string|ColorInterface $color, int $size = 1): self + { + $this->line->setBackgroundColor($color); + $this->line->setBorderColor($color); + $this->line->setWidth($size); + + return $this; + } + + /** + * Set the width of the line to be produced. + */ + public function width(int $size): self + { + $this->line->setWidth($size); + + return $this; + } + + /** + * Set the coordinates of the starting point of the line to be produced. + */ + public function from(int $x, int $y): self + { + $this->line->setStart(new Point($x, $y)); + + return $this; + } + + /** + * Set the coordinates of the end point of the line to be produced. + */ + public function to(int $x, int $y): self + { + $this->line->setEnd(new Point($x, $y)); + + return $this; + } +} diff --git a/src/Geometry/Factories/PolygonFactory.php b/src/Geometry/Factories/PolygonFactory.php new file mode 100644 index 000000000..efc063277 --- /dev/null +++ b/src/Geometry/Factories/PolygonFactory.php @@ -0,0 +1,81 @@ +polygon = $polygon instanceof Polygon ? clone $polygon : new Polygon([]); + + if (is_callable($polygon)) { + $polygon($this); + } + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::build() + */ + public static function build(null|callable|DrawableInterface $drawable = null): Polygon + { + return (new self($drawable))->drawable(); + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::drawable() + */ + public function drawable(): Polygon + { + return $this->polygon; + } + + /** + * Add a point to the polygon to be produced. + */ + public function point(int $x, int $y): self + { + $this->polygon->addPoint(new Point($x, $y)); + + return $this; + } + + /** + * Set the background color of the polygon to be produced. + */ + public function background(string|ColorInterface $color): self + { + $this->polygon->setBackgroundColor($color); + + return $this; + } + + /** + * Set the border color & border size of the polygon to be produced. + * + * @throws InvalidArgumentException + */ + public function border(string|ColorInterface $color, int $size = 1): self + { + $this->polygon->setBorder($color, $size); + + return $this; + } +} diff --git a/src/Geometry/Factories/RectangleFactory.php b/src/Geometry/Factories/RectangleFactory.php new file mode 100644 index 000000000..cbe6ce76a --- /dev/null +++ b/src/Geometry/Factories/RectangleFactory.php @@ -0,0 +1,114 @@ +rectangle = $rectangle instanceof Rectangle ? clone $rectangle : new Rectangle(0, 0); + + if (is_callable($rectangle)) { + $rectangle($this); + } + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::build() + * + * @throws InvalidArgumentException + */ + public static function build(null|callable|DrawableInterface $drawable = null): Rectangle + { + return (new self($drawable))->drawable(); + } + + /** + * {@inheritdoc} + * + * @see DrawableFactoryInterface::drawable() + */ + public function drawable(): Rectangle + { + return $this->rectangle; + } + + /** + * Set the size of the rectangle to be produced. + */ + public function size(int $width, int $height): self + { + $this->rectangle->setSize($width, $height); + + return $this; + } + + /** + * Set the width of the rectangle to be produced. + */ + public function width(int $width): self + { + $this->rectangle->setWidth($width); + + return $this; + } + + /** + * Set the height of the rectangle to be produced. + */ + public function height(int $height): self + { + $this->rectangle->setHeight($height); + + return $this; + } + + /** + * Set the background color of the rectangle to be produced. + */ + public function background(string|ColorInterface $color): self + { + $this->rectangle->setBackgroundColor($color); + + return $this; + } + + /** + * Set the border color & border size of the rectangle to be produced. + * + * @throws InvalidArgumentException + */ + public function border(string|ColorInterface $color, int $size = 1): self + { + $this->rectangle->setBorder($color, $size); + + return $this; + } + + /** + * Set the position where the rectangle should be drawn. + */ + public function at(int $x, int $y): self + { + $this->rectangle->position()->setPosition($x, $y); + + return $this; + } +} diff --git a/src/Geometry/Line.php b/src/Geometry/Line.php new file mode 100644 index 000000000..184817020 --- /dev/null +++ b/src/Geometry/Line.php @@ -0,0 +1,168 @@ +start; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::setPosition() + */ + public function setPosition(PointInterface $position): self + { + $this->start = $position; + + return $this; + } + + /** + * Return line width + */ + public function width(): int + { + return $this->width; + } + + /** + * Set line width. + */ + public function setWidth(int $width): self + { + $this->width = $width; + + return $this; + } + + /** + * Get starting point of line. + */ + public function start(): PointInterface + { + return $this->start; + } + + /** + * get end point of line. + */ + public function end(): PointInterface + { + return $this->end; + } + + /** + * Set starting point of line. + */ + public function setStart(PointInterface $start): self + { + $this->start = $start; + + return $this; + } + + /** + * Set starting point of line by coordinates. + */ + public function from(int $x, int $y): self + { + $this->start()->setX($x); + $this->start()->setY($y); + + return $this; + } + + /** + * Set end point of line by coordinates. + */ + public function to(int $x, int $y): self + { + $this->end()->setX($x); + $this->end()->setY($y); + + return $this; + } + + /** + * Set end point of line. + */ + public function setEnd(PointInterface $end): self + { + $this->end = $end; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::factory() + */ + public function factory(): DrawableFactoryInterface + { + return new LineFactory($this); + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::adjust() + */ + public function adjust(callable $adjustments): DrawableInterface + { + $factory = $this->factory(); + $adjustments($factory); + + return $factory->drawable(); + } + + /** + * Clone line. + */ + public function __clone(): void + { + $this->start = clone $this->start; + $this->end = clone $this->end; + + if ($this->backgroundColor instanceof AbstractColor) { + $this->backgroundColor = clone $this->backgroundColor; + } + + if ($this->borderColor instanceof AbstractColor) { + $this->borderColor = clone $this->borderColor; + } + } +} diff --git a/src/Geometry/Pixel.php b/src/Geometry/Pixel.php new file mode 100644 index 000000000..e2735db78 --- /dev/null +++ b/src/Geometry/Pixel.php @@ -0,0 +1,43 @@ +background = $background; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::backgroundColor() + */ + public function backgroundColor(): ColorInterface + { + return $this->background; + } +} diff --git a/src/Geometry/Point.php b/src/Geometry/Point.php new file mode 100644 index 000000000..6143f7aea --- /dev/null +++ b/src/Geometry/Point.php @@ -0,0 +1,143 @@ + + */ +class Point implements PointInterface, IteratorAggregate +{ + /** + * Create new point instance. + */ + public function __construct( + protected int $x = 0, + protected int $y = 0 + ) { + // + } + + /** + * {@inheritdoc} + * + * @see IteratorAggregate::getIterator() + */ + public function getIterator(): Traversable + { + return new ArrayIterator([$this->x, $this->y]); + } + + /** + * {@inheritdoc} + * + * @see PointInterface::setX() + */ + public function setX(int $x): self + { + $this->x = $x; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see PointInterface::x() + */ + public function x(): int + { + return $this->x; + } + + /** + * {@inheritdoc} + * + * @see PointInterface::setY() + */ + public function setY(int $y): self + { + $this->y = $y; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see PointInterface::y() + */ + public function y(): int + { + return $this->y; + } + + /** + * {@inheritdoc} + * + * @see PointInterface::moveX() + */ + public function moveX(int $value): self + { + $this->x += $value; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see PointInterface::moveY() + */ + public function moveY(int $value): self + { + $this->y += $value; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see PointInterface::move() + */ + public function move(int $x, int $y): self + { + return $this->moveX($x)->moveY($y); + } + + /** + * {@inheritdoc} + * + * @see PointInterface::setPosition() + */ + public function setPosition(int $x, int $y): self + { + $this->setX($x); + $this->setY($y); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see PointInterface::rotate() + */ + public function rotate(float $angle, PointInterface $pivot): self + { + $sin = round(sin(deg2rad($angle)), 6); + $cos = round(cos(deg2rad($angle)), 6); + + return $this->setPosition( + intval($cos * ($this->x() - $pivot->x()) - $sin * ($this->y() - $pivot->y()) + $pivot->x()), + intval($sin * ($this->x() - $pivot->x()) + $cos * ($this->y() - $pivot->y()) + $pivot->y()) + ); + } +} diff --git a/src/Geometry/Polygon.php b/src/Geometry/Polygon.php new file mode 100644 index 000000000..c46a4254c --- /dev/null +++ b/src/Geometry/Polygon.php @@ -0,0 +1,424 @@ + + * @implements ArrayAccess + */ +class Polygon implements IteratorAggregate, Countable, ArrayAccess, DrawableInterface +{ + use HasBorder; + use HasBackgroundColor; + + /** + * Create new polygon instance. + * + * @param array $points + */ + public function __construct( + protected array $points = [], + protected PointInterface $pivot = new Point() + ) { + // + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::position() + */ + public function position(): PointInterface + { + return $this->pivot; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::setPosition() + */ + public function setPosition(PointInterface $position): self + { + $this->pivot = $position; + + return $this; + } + + /** + * Implement iteration through all points of polygon. + * + * @return Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->points); + } + + /** + * Return current pivot point. + */ + public function pivot(): PointInterface + { + return $this->pivot; + } + + /** + * Change pivot point to given point. + */ + public function setPivot(PointInterface $pivot): self + { + $this->pivot = $pivot; + + return $this; + } + + /** + * Return first point of polygon. + */ + public function first(): ?PointInterface + { + if ($point = reset($this->points)) { + return $point; + } + + return null; + } + + /** + * Return last point of polygon. + */ + public function last(): ?PointInterface + { + if ($point = end($this->points)) { + return $point; + } + + return null; + } + + /** + * Return polygon's point count. + */ + public function count(): int + { + return count($this->points); + } + + /** + * Determine if point exists at given offset. + */ + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->points); + } + + /** + * Return point at given offset. + */ + public function offsetGet(mixed $offset): mixed + { + return $this->points[$offset]; + } + + /** + * Set point at given offset + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->points[$offset] = $value; + } + + /** + * Unset offset at given offset. + */ + public function offsetUnset(mixed $offset): void + { + unset($this->points[$offset]); + } + + /** + * Add given point to polygon. + */ + public function addPoint(PointInterface $point): self + { + $this->points[] = $point; + + return $this; + } + + /** + * Calculate total horizontal span of polygon. + */ + public function width(): int + { + return abs($this->mostLeftPoint()->x() - $this->mostRightPoint()->x()); + } + + /** + * Calculate total vertical span of polygon. + */ + public function height(): int + { + return abs($this->mostBottomPoint()->y() - $this->mostTopPoint()->y()); + } + + /** + * Return most left point of all points in polygon. + */ + public function mostLeftPoint(): PointInterface + { + $points = $this->points; + + usort($points, function (PointInterface $a, PointInterface $b): int { + if ($a->x() === $b->x()) { + return 0; + } + return $a->x() < $b->x() ? -1 : 1; + }); + + return $points[0]; + } + + /** + * Return most right point in polygon. + */ + public function mostRightPoint(): PointInterface + { + $points = $this->points; + + usort($points, function (PointInterface $a, PointInterface $b): int { + if ($a->x() === $b->x()) { + return 0; + } + return $a->x() > $b->x() ? -1 : 1; + }); + + return $points[0]; + } + + /** + * Return most top point in polygon. + */ + public function mostTopPoint(): PointInterface + { + $points = $this->points; + + usort($points, function (PointInterface $a, PointInterface $b): int { + if ($a->y() === $b->y()) { + return 0; + } + return $a->y() > $b->y() ? -1 : 1; + }); + + return $points[0]; + } + + /** + * Return most bottom point in polygon. + */ + public function mostBottomPoint(): PointInterface + { + $points = $this->points; + + usort($points, function (PointInterface $a, PointInterface $b): int { + if ($a->y() === $b->y()) { + return 0; + } + return $a->y() < $b->y() ? -1 : 1; + }); + + return $points[0]; + } + + /** + * Return point in absolute center of the polygon. + */ + public function centerPoint(): PointInterface + { + return new Point( + $this->mostRightPoint()->x() - (intval(round($this->width() / 2))), + $this->mostTopPoint()->y() - (intval(round($this->height() / 2))) + ); + } + + /** + * Align all points of the polygon horizontally to given position around pivot point. + * + * @throws InvalidArgumentException + */ + public function alignHorizontally(string|Alignment $position): self + { + $diff = match (Alignment::create($position)) { + Alignment::CENTER => $this->centerPoint()->x() - $this->pivot()->x(), + Alignment::RIGHT, + Alignment::TOP_RIGHT, + Alignment::BOTTOM_RIGHT => $this->mostRightPoint()->x() - $this->pivot()->x(), + Alignment::LEFT, + Alignment::TOP_LEFT, + Alignment::BOTTOM_LEFT => $this->mostLeftPoint()->x() - $this->pivot()->x(), + default => 0, + }; + + foreach ($this->points as $point) { + $point->setX(intval($point->x() - $diff)); + } + + return $this; + } + + /** + * Align all points of the polygon vertically to given position around pivot point. + * + * @throws InvalidArgumentException + */ + public function alignVertically(string|Alignment $position): self + { + $diff = match (Alignment::create($position)) { + Alignment::CENTER => $this->centerPoint()->y() - $this->pivot()->y(), + Alignment::TOP, + Alignment::TOP_RIGHT, + Alignment::TOP_LEFT => $this->mostTopPoint()->y() - $this->pivot()->y() - $this->height(), + Alignment::BOTTOM, + Alignment::BOTTOM_LEFT, + Alignment::BOTTOM_RIGHT => $this->mostBottomPoint()->y() - $this->pivot()->y() + $this->height(), + default => 0, + }; + + foreach ($this->points as $point) { + $point->setY(intval($point->y() - $diff)); + } + + return $this; + } + + /** + * Rotate points of polygon clockwise around pivot point with given angle. + */ + public function rotate(float $angle): self + { + $sin = sin(deg2rad($angle)); + $cos = cos(deg2rad($angle)); + + foreach ($this->points as $point) { + // translate point to pivot + $point->setX( + intval($point->x() - $this->pivot()->x()), + ); + $point->setY( + intval($point->y() - $this->pivot()->y()), + ); + + // rotate point + $x = $point->x() * $cos - $point->y() * $sin; + $y = $point->x() * $sin + $point->y() * $cos; + + // translate point back + $point->setX( + intval($x + $this->pivot()->x()), + ); + $point->setY( + intval($y + $this->pivot()->y()), + ); + } + + return $this; + } + + /** + * Move all points by given distance on the x-axis. + */ + public function movePointsX(int $distance): self + { + foreach ($this->points as $point) { + $point->moveX($distance); + } + + return $this; + } + + /** + * Move all points by given distance on the y-axis. + */ + public function movePointsY(int $distance): self + { + foreach ($this->points as $point) { + $point->moveY($distance); + } + + return $this; + } + + /** + * Return array of all x/y values of all points of polygon + * + * @return array + */ + public function toArray(): array + { + $coordinates = []; + foreach ($this->points as $point) { + $coordinates[] = $point->x(); + $coordinates[] = $point->y(); + } + + return $coordinates; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::factory() + */ + public function factory(): DrawableFactoryInterface + { + return new PolygonFactory($this); + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::adjust() + */ + public function adjust(callable $adjustments): DrawableInterface + { + $factory = $this->factory(); + $adjustments($factory); + + return $factory->drawable(); + } + + /** + * Clone polygon. + */ + public function __clone(): void + { + $this->points = array_map(fn($point) => clone $point, $this->points); + $this->pivot = clone $this->pivot; + + if ($this->backgroundColor instanceof AbstractColor) { + $this->backgroundColor = clone $this->backgroundColor; + } + + if ($this->borderColor instanceof AbstractColor) { + $this->borderColor = clone $this->borderColor; + } + } +} diff --git a/src/Geometry/Rectangle.php b/src/Geometry/Rectangle.php new file mode 100644 index 000000000..18975dfb0 --- /dev/null +++ b/src/Geometry/Rectangle.php @@ -0,0 +1,68 @@ +points); + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::factory() + * + * @throws RuntimeException + */ + public function factory(): DrawableFactoryInterface + { + // @phpstan-ignore missingType.checkedException + return new RectangleFactory($this); + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::adjust() + * + * @throws RuntimeException + */ + public function adjust(callable $adjustments): DrawableInterface + { + $factory = $this->factory(); + $adjustments($factory); + + return $factory->drawable(); + } +} diff --git a/src/Geometry/Tools/Resizer.php b/src/Geometry/Tools/Resizer.php new file mode 100644 index 000000000..725f608ea --- /dev/null +++ b/src/Geometry/Tools/Resizer.php @@ -0,0 +1,363 @@ +width); + } + + /** + * Return target width of resizer if available. + */ + protected function targetWidth(): ?int + { + return $this->hasTargetWidth() ? $this->width : null; + } + + /** + * Determine if resize has target height. + */ + protected function hasTargetHeight(): bool + { + return is_int($this->height); + } + + /** + * Return target width of resizer if available. + */ + protected function targetHeight(): ?int + { + return $this->hasTargetHeight() ? $this->height : null; + } + + /** + * Return target size object. + * + * @throws StateException + */ + protected function targetSize(): SizeInterface + { + if (!$this->hasTargetWidth() || !$this->hasTargetHeight()) { + throw new StateException('Target size needs width and height'); + } + + try { + return new Size($this->width, $this->height); + } catch (InvalidArgumentException $e) { + throw new StateException('Invalid target size', previous: $e); + } + } + + /** + * Set target width of resizer. + */ + public function toWidth(int $width): self + { + $this->width = $width; + + return $this; + } + + /** + * Set target height of resizer. + */ + public function toHeight(int $height): self + { + $this->height = $height; + + return $this; + } + + /** + * Set target size to given size object. + */ + public function toSize(SizeInterface $size): self + { + $this->width = $size->width(); + $this->height = $size->height(); + + return $this; + } + + /** + * Get proportinal width. + */ + protected function proportionalWidth(SizeInterface $size): int + { + if (!$this->hasTargetHeight()) { + return $size->width(); + } + + return max([1, (int) round($this->height * $size->aspectRatio())]); + } + + /** + * Get proportinal height. + */ + protected function proportionalHeight(SizeInterface $size): int + { + if (!$this->hasTargetWidth()) { + return $size->height(); + } + + return max([1, (int) round($this->width / $size->aspectRatio())]); + } + + /** + * Resize given size to target size of the resizer. + * + * @throws InvalidArgumentException + */ + public function resize(SizeInterface $size): SizeInterface + { + $resized = new Size($size->width(), $size->height()); + $width = $this->targetWidth(); + $height = $this->targetHeight(); + + if ($width !== null) { + $resized->setWidth($width); + } + + if ($height !== null) { + $resized->setHeight($height); + } + + return $resized; + } + + /** + * Resize given size to target size of the resizer but do not exceed original size. + * + * @throws InvalidArgumentException + */ + public function resizeDown(SizeInterface $size): SizeInterface + { + $resized = new Size($size->width(), $size->height()); + $width = $this->targetWidth(); + $height = $this->targetHeight(); + + if ($width !== null) { + $resized->setWidth( + min($width, $size->width()) + ); + } + + if ($height !== null) { + $resized->setHeight( + min($height, $size->height()) + ); + } + + return $resized; + } + + /** + * Resize given size to target size proportinally. + * + * @throws InvalidArgumentException + */ + public function scale(SizeInterface $size): SizeInterface + { + $resized = new Size($size->width(), $size->height()); + + if ($this->hasTargetWidth() && $this->hasTargetHeight()) { + $resized->setWidth(min( + $this->proportionalWidth($size), + $this->targetWidth() + )); + $resized->setHeight(min( + $this->proportionalHeight($size), + $this->targetHeight() + )); + } elseif ($this->hasTargetWidth()) { + $resized->setWidth($this->targetWidth()); + $resized->setHeight($this->proportionalHeight($size)); + } elseif ($this->hasTargetHeight()) { + $resized->setWidth($this->proportionalWidth($size)); + $resized->setHeight($this->targetHeight()); + } + + return $resized; + } + + /** + * Resize given size to target size proportinally but do not exceed original size. + * + * @throws InvalidArgumentException + */ + public function scaleDown(SizeInterface $size): SizeInterface + { + $resized = new Size($size->width(), $size->height()); + + if ($this->hasTargetWidth() && $this->hasTargetHeight()) { + $resized->setWidth(min( + $this->proportionalWidth($size), + $this->targetWidth(), + $size->width() + )); + $resized->setHeight(min( + $this->proportionalHeight($size), + $this->targetHeight(), + $size->height() + )); + } elseif ($this->hasTargetWidth()) { + $resized->setWidth(min( + $this->targetWidth(), + $size->width() + )); + $resized->setHeight(min( + $this->proportionalHeight($size), + $size->height() + )); + } elseif ($this->hasTargetHeight()) { + $resized->setWidth(min( + $this->proportionalWidth($size), + $size->width() + )); + $resized->setHeight(min( + $this->targetHeight(), + $size->height() + )); + } + + return $resized; + } + + /** + * Scale given size to cover target size. + * + * @param SizeInterface $size Size to be resized + * @throws InvalidArgumentException + * @throws StateException + */ + public function cover(SizeInterface $size): SizeInterface + { + $resized = new Size($size->width(), $size->height()); + + // auto height + $resized->setWidth($this->targetWidth()); + $resized->setHeight($this->proportionalHeight($size)); + + if ($resized->fitsWithin($this->targetSize())) { + // auto width + $resized->setWidth($this->proportionalWidth($size)); + $resized->setHeight($this->targetHeight()); + } + + return $resized; + } + + /** + * Scale the given size up or down so that the result can fit into the target size. + * + * @param SizeInterface $size Size to be resized + * @throws InvalidArgumentException + * @throws StateException + */ + public function contain(SizeInterface $size): SizeInterface + { + $resized = new Size($size->width(), $size->height()); + + // auto height + $resized->setWidth($this->targetWidth()); + $resized->setHeight($this->proportionalHeight($size)); + + if (!$resized->fitsWithin($this->targetSize())) { + // auto width + $resized->setWidth($this->proportionalWidth($size)); + $resized->setHeight($this->targetHeight()); + } + + return $resized; + } + + /** + * Scale the given size down so that the result can fit into the target size. + * + * @param SizeInterface $size Size to be resized + * @throws InvalidArgumentException + * @throws StateException + */ + public function containDown(SizeInterface $size): SizeInterface + { + $resized = new Size($size->width(), $size->height()); + + // auto height + $resized->setWidth( + min($size->width(), $this->targetWidth()) + ); + + $resized->setHeight( + min($size->height(), $this->proportionalHeight($size)) + ); + + if (!$resized->fitsWithin($this->targetSize())) { + // auto width + $resized->setWidth( + min($size->width(), $this->proportionalWidth($size)) + ); + $resized->setHeight( + min($size->height(), $this->targetHeight()) + ); + } + + return $resized; + } + + /** + * Crop target size out of given size at given alignment position (i.e. move the pivot point). + * + * @throws InvalidArgumentException + */ + public function crop(SizeInterface $size, string|Alignment $alignment = Alignment::TOP_LEFT): SizeInterface + { + return $this->resize($size)->alignPivotTo( + $size->movePivot($alignment), + $alignment + ); + } +} diff --git a/src/Geometry/Traits/HasBackgroundColor.php b/src/Geometry/Traits/HasBackgroundColor.php new file mode 100644 index 000000000..89dc3f0b4 --- /dev/null +++ b/src/Geometry/Traits/HasBackgroundColor.php @@ -0,0 +1,44 @@ +backgroundColor = $color; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::backgroundColor() + */ + public function backgroundColor(): null|string|ColorInterface + { + return $this->backgroundColor; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::hasBackgroundColor() + */ + public function hasBackgroundColor(): bool + { + return $this->backgroundColor !== null; + } +} diff --git a/src/Geometry/Traits/HasBorder.php b/src/Geometry/Traits/HasBorder.php new file mode 100644 index 000000000..7642cd4fe --- /dev/null +++ b/src/Geometry/Traits/HasBorder.php @@ -0,0 +1,88 @@ +setBorderSize($size)->setBorderColor($color); + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::setBorderSize() + * + * @throws InvalidArgumentException + */ + public function setBorderSize(int $size): self + { + if ($size < 0) { + throw new InvalidArgumentException( + 'Border size must be greater than or equal to 0' + ); + } + + $this->borderSize = $size; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::borderSize() + */ + public function borderSize(): int + { + return $this->borderSize; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::setBorderColor() + */ + public function setBorderColor(string|ColorInterface $color): self + { + $this->borderColor = $color; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::borderColor() + */ + public function borderColor(): null|string|ColorInterface + { + return $this->borderColor; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::hasBorder() + */ + public function hasBorder(): bool + { + return $this->borderSize > 0 && !is_null($this->borderColor); + } +} diff --git a/src/Image.php b/src/Image.php new file mode 100644 index 000000000..9320e01b6 --- /dev/null +++ b/src/Image.php @@ -0,0 +1,1263 @@ +origin = new Origin(); + $this->exif = new Collection(); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::driver() + */ + public function driver(): DriverInterface + { + return $this->driver; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::core() + */ + public function core(): CoreInterface + { + return $this->core; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::origin() + */ + public function origin(): OriginInterface + { + return $this->origin; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::setOrigin() + */ + public function setOrigin(OriginInterface $origin): ImageInterface + { + $this->origin = $origin; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::count() + */ + public function count(): int + { + return $this->core->count(); + } + + /** + * Implementation of IteratorAggregate + * + * @return Traversable + */ + public function getIterator(): Traversable + { + return $this->core; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::isAnimated() + */ + public function isAnimated(): bool + { + return $this->count() > 1; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::removeAnimation() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function removeAnimation(int|string $position = 0): ImageInterface + { + return $this->modify(new RemoveAnimationModifier($position)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::sliceAnimation() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function sliceAnimation(int $offset = 0, ?int $length = null): ImageInterface + { + return $this->modify(new SliceAnimationModifier($offset, $length)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::loops() + */ + public function loops(): int + { + return $this->core->loops(); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::setLoops() + */ + public function setLoops(int $loops): ImageInterface + { + $this->core->setLoops($loops); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::exif() + */ + public function exif(?string $query = null): mixed + { + return is_null($query) ? $this->exif : $this->exif->get($query); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::setExif() + */ + public function setExif(CollectionInterface $exif): ImageInterface + { + $this->exif = $exif; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::modify() + * + * @throws ModifierException + */ + public function modify(ModifierInterface $modifier): ImageInterface + { + return $this->driver()->specializeModifier($modifier)->apply($this); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::analyze() + * + * @throws AnalyzerException + */ + public function analyze(AnalyzerInterface $analyzer): mixed + { + return $this->driver()->specializeAnalyzer($analyzer)->analyze($this); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::save() + * + * @throws InvalidArgumentException + * @throws EncoderException + * @throws DirectoryNotFoundException + * @throws FileNotWritableException + * @throws StreamException + * @throws NotSupportedException + */ + public function save(?string $path = null, mixed ...$options): ImageInterface + { + if ($path === '') { + throw new InvalidArgumentException('Argument $path must not be an empty string'); + } + + if (is_null($path) && is_null($this->origin()->filePath())) { + throw new EncoderException('Unable to determine path for saving'); + } + + $path = is_null($path) ? $this->origin()->filePath() : $path; + + try { + // try to determine encoding format by file extension of the path + $encoded = $this->encode(new FilePathEncoder($path, ...$options)); + } catch (EncoderException) { + // fallback to encoding format by media type + $encoded = $this->encode(new MediaTypeEncoder(null, ...$options)); + } + + $encoded->save($path); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::width() + * + * @throws AnalyzerException + */ + public function width(): int + { + return $this->analyze(new WidthAnalyzer()); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::height() + * + * @throws AnalyzerException + */ + public function height(): int + { + return $this->analyze(new HeightAnalyzer()); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::size() + * + * @throws AnalyzerException + */ + public function size(): SizeInterface + { + try { + return new Size($this->width(), $this->height()); + } catch (InvalidArgumentException $e) { + throw new AnalyzerException('Failed to read image size', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::colorspace() + * + * @throws AnalyzerException + */ + public function colorspace(): ColorspaceInterface + { + return $this->analyze(new ColorspaceAnalyzer()); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::setColorspace() + * + * @throws ModifierException + * @throws NotSupportedException + */ + public function setColorspace(string|ColorspaceInterface $colorspace): ImageInterface + { + return $this->modify(new ColorspaceModifier($colorspace)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::resolution() + * + * @throws AnalyzerException + */ + public function resolution(): ResolutionInterface + { + return $this->analyze(new ResolutionAnalyzer()); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::setResolution() + * + * @throws ModifierException + */ + public function setResolution(float $x, float $y): ImageInterface + { + return $this->modify(new ResolutionModifier($x, $y)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::colorAt() + * + * @throws AnalyzerException + */ + public function colorAt(int $x, int $y, int $frame = 0): ColorInterface + { + return $this->analyze(new PixelColorAnalyzer($x, $y, $frame)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::colorsAt() + * + * @throws AnalyzerException + */ + public function colorsAt(int $x, int $y): CollectionInterface + { + return $this->analyze(new PixelColorsAnalyzer($x, $y)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::backgroundColor() + * + * @throws ColorDecoderException + */ + public function backgroundColor(): ColorInterface + { + return $this->driver()->decodeColor( + $this->driver()->config()->backgroundColor + ); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::setBackgroundColor() + * + * @throws InvalidArgumentException + * @throws ColorDecoderException + */ + public function setBackgroundColor(string|ColorInterface $color): ImageInterface + { + $this->driver()->config()->setOptions( + backgroundColor: $this->driver()->decodeColor($color) + ); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::profile() + * + * @throws AnalyzerException + */ + public function profile(): ProfileInterface + { + return $this->analyze(new ProfileAnalyzer()); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::setProfile() + * + * @throws ModifierException + */ + public function setProfile(ProfileInterface $profile): ImageInterface + { + return $this->modify(new ProfileModifier($profile)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::removeProfile() + * + * @throws ModifierException + */ + public function removeProfile(): ImageInterface + { + return $this->modify(new RemoveProfileModifier()); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::reduceColors() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function reduceColors(int $limit, null|string|ColorInterface $background = null): ImageInterface + { + return $this->modify(new ReduceColorsModifier($limit, $background)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::sharpen() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function sharpen(int $level = 10): ImageInterface + { + return $this->modify(new SharpenModifier($level)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::invert() + * + * @throws ModifierException + */ + public function invert(): ImageInterface + { + return $this->modify(new InvertModifier()); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::pixelate() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function pixelate(int $size): ImageInterface + { + return $this->modify(new PixelateModifier($size)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::grayscale() + * + * @throws ModifierException + */ + public function grayscale(): ImageInterface + { + return $this->modify(new GrayscaleModifier()); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::brightness() + * + * @throws ModifierException + */ + public function brightness(int $level): ImageInterface + { + return $this->modify(new BrightnessModifier($level)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::contrast() + * + * @throws ModifierException + */ + public function contrast(int $level): ImageInterface + { + return $this->modify(new ContrastModifier($level)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::gamma() + * + * @throws ModifierException + */ + public function gamma(float $gamma): ImageInterface + { + return $this->modify(new GammaModifier($gamma)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::colorize() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function colorize(int $red = 0, int $green = 0, int $blue = 0): ImageInterface + { + return $this->modify(new ColorizeModifier($red, $green, $blue)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::flip() + * + * @throws ModifierException + */ + public function flip(Direction $direction = Direction::HORIZONTAL): ImageInterface + { + return $this->modify(new FlipModifier($direction)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::blur() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function blur(int $level = 5): ImageInterface + { + return $this->modify(new BlurModifier($level)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::rotate() + * + * @throws InvalidArgumentException + * @throws ColorDecoderException + * @throws ModifierException + */ + public function rotate(float $angle, null|string|ColorInterface $background = null): ImageInterface + { + return $this->modify(new RotateModifier($angle, $background)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::orient() + * + * @throws ModifierException + */ + public function orient(): ImageInterface + { + return $this->modify(new OrientModifier()); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::text() + * + * @throws InvalidArgumentException + * @throws ModifierException + * @throws StateException + */ + public function text(string $text, int $x, int $y, callable|FontInterface $font): ImageInterface + { + return $this->modify( + new TextModifier( + $text, + new Point($x, $y), + FontFactory::build($font), + ), + ); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::resize() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function resize(null|int|Fraction $width = null, null|int|Fraction $height = null): ImageInterface + { + try { + return $this->modify(new ResizeModifier(...$this->resolveDimension($width, $height))); + } catch (AnalyzerException $e) { + throw new ModifierException('Failed to resize image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::resizeDown() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function resizeDown(null|int|Fraction $width = null, null|int|Fraction $height = null): ImageInterface + { + try { + return $this->modify(new ResizeDownModifier(...$this->resolveDimension($width, $height))); + } catch (AnalyzerException $e) { + throw new ModifierException('Failed to resize image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::scale() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function scale(null|int|Fraction $width = null, null|int|Fraction $height = null): ImageInterface + { + try { + return $this->modify(new ScaleModifier(...$this->resolveDimension($width, $height))); + } catch (AnalyzerException $e) { + throw new ModifierException('Failed to resize image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::scaleDown() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function scaleDown(null|int|Fraction $width = null, null|int|Fraction $height = null): ImageInterface + { + try { + return $this->modify(new ScaleDownModifier(...$this->resolveDimension($width, $height))); + } catch (AnalyzerException $e) { + throw new ModifierException('Failed to resize image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::cover() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function cover( + int|Fraction $width, + int|Fraction $height, + string|Alignment $alignment = Alignment::CENTER, + ): ImageInterface { + try { + return $this->modify(new CoverModifier(...[ + ...$this->resolveDimension($width, $height), + ...['alignment' => $alignment] + ])); + } catch (AnalyzerException $e) { + throw new ModifierException('Failed to resize image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::coverDown() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function coverDown( + int|Fraction $width, + int|Fraction $height, + string|Alignment $alignment = Alignment::CENTER, + ): ImageInterface { + try { + return $this->modify(new CoverDownModifier(...[ + ...$this->resolveDimension($width, $height), + ...['alignment' => $alignment] + ])); + } catch (AnalyzerException $e) { + throw new ModifierException('Failed to resize image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::resizeCanvas() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function resizeCanvas( + null|int|Fraction $width = null, + null|int|Fraction $height = null, + null|string|ColorInterface $background = null, + string|Alignment $alignment = Alignment::CENTER + ): ImageInterface { + try { + return $this->modify(new ResizeCanvasModifier(...[ + ...$this->resolveDimension($width, $height), + ...[ + 'background' => $background, + 'alignment' => $alignment, + ] + ])); + } catch (AnalyzerException $e) { + throw new ModifierException('Failed to resize image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::resizeCanvasRelative() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function resizeCanvasRelative( + null|int|Fraction $width = null, + null|int|Fraction $height = null, + null|string|ColorInterface $background = null, + string|Alignment $alignment = Alignment::CENTER + ): ImageInterface { + try { + return $this->modify(new ResizeCanvasRelativeModifier(...[ + ...$this->resolveDimension($width, $height), + ...[ + 'background' => $background, + 'alignment' => $alignment, + ] + ])); + } catch (AnalyzerException $e) { + throw new ModifierException('Failed to resize image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::containDown() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function containDown( + int|Fraction $width, + int|Fraction $height, + null|string|ColorInterface $background = null, + string|Alignment $alignment = Alignment::CENTER + ): ImageInterface { + try { + return $this->modify(new ContainDownModifier(...[ + ...$this->resolveDimension($width, $height), + ...[ + 'background' => $background, + 'alignment' => $alignment, + ] + ])); + } catch (AnalyzerException $e) { + throw new ModifierException('Failed to resize image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::contain() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function contain( + int|Fraction $width, + int|Fraction $height, + null|string|ColorInterface $background = null, + string|Alignment $alignment = Alignment::CENTER + ): ImageInterface { + try { + return $this->modify(new ContainModifier(...[ + ...$this->resolveDimension($width, $height), + ...[ + 'background' => $background, + 'alignment' => $alignment, + ] + ])); + } catch (AnalyzerException $e) { + throw new ModifierException('Failed to resize image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::crop() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function crop( + int|Fraction $width, + int|Fraction $height, + int $x = 0, + int $y = 0, + null|string|ColorInterface $background = null, + string|Alignment $alignment = Alignment::TOP_LEFT + ): ImageInterface { + try { + return $this->modify(new CropModifier(...[ + ...$this->resolveDimension($width, $height), + ...[ + 'x' => $x, + 'y' => $y, + 'background' => $background, + 'alignment' => $alignment, + ] + ])); + } catch (AnalyzerException $e) { + throw new ModifierException('Failed to resize image', previous: $e); + } + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::trim() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function trim(int $tolerance = 0): ImageInterface + { + return $this->modify(new TrimModifier($tolerance)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::insert() + * + * @throws InvalidArgumentException + * @throws ModifierException + */ + public function insert( + mixed $image, + int $x = 0, + int $y = 0, + string|Alignment $alignment = Alignment::TOP_LEFT, + float $transparency = 1 + ): ImageInterface { + return $this->modify(new InsertModifier($image, $x, $y, $alignment, $transparency)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::fill() + * + * @throws ModifierException + */ + public function fill(string|ColorInterface $color, ?int $x = null, ?int $y = null): ImageInterface + { + return $this->modify( + new FillModifier( + $color, + is_null($x) || is_null($y) ? null : new Point($x, $y), + ), + ); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::fillTransparentAreas() + * + * @throws ModifierException + */ + public function fillTransparentAreas(null|string|ColorInterface $color = null): ImageInterface + { + return $this->modify(new FillTransparentAreasModifier($color)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::drawPixel() + * + * @throws ModifierException + * @throws ColorDecoderException + */ + public function drawPixel(int $x, int $y, string|ColorInterface $color): ImageInterface + { + return $this->modify(new DrawPixelModifier(new Point($x, $y), $color)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::drawRectangle() + * + * @throws InvalidArgumentException + * @throws ModifierException + * @throws ColorDecoderException + */ + public function drawRectangle(callable|Rectangle $rectangle): ImageInterface + { + return $this->modify( + new DrawRectangleModifier( + RectangleFactory::build($rectangle) + ), + ); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::drawEllipse() + * + * @throws ModifierException + * @throws ColorDecoderException + */ + public function drawEllipse(callable|Ellipse $ellipse): ImageInterface + { + return $this->modify( + new DrawEllipseModifier( + EllipseFactory::build($ellipse) + ), + ); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::drawCircle() + * + * @throws ModifierException + * @throws ColorDecoderException + */ + public function drawCircle(callable|Circle $circle): ImageInterface + { + return $this->modify( + new DrawEllipseModifier( + CircleFactory::build($circle) + ) + ); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::drawPolygon() + * + * @throws InvalidArgumentException + * @throws ModifierException + * @throws ColorDecoderException + */ + public function drawPolygon(callable|Polygon $polygon): ImageInterface + { + return $this->modify( + new DrawPolygonModifier( + PolygonFactory::build($polygon) + ), + ); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::drawLine() + * + * @throws ModifierException + * @throws ColorDecoderException + */ + public function drawLine(callable|Line $line): ImageInterface + { + return $this->modify( + new DrawLineModifier( + LineFactory::build($line) + ), + ); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::drawBezier() + * + * @throws ModifierException + * @throws ColorDecoderException + */ + public function drawBezier(callable|Bezier $bezier): ImageInterface + { + return $this->modify( + new DrawBezierModifier( + BezierFactory::build($bezier) + ), + ); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::draw() + * + * @throws InvalidArgumentException + * @throws NotSupportedException + * @throws ModifierException + * @throws ColorDecoderException + */ + public function draw(DrawableInterface $drawable): ImageInterface + { + return $this->modify(match ($drawable::class) { + Rectangle::class => new DrawRectangleModifier($drawable), + Circle::class, Ellipse::class => new DrawEllipseModifier($drawable), + Bezier::class => new DrawBezierModifier($drawable), + Line::class => new DrawLineModifier($drawable), + Polygon::class => new DrawPolygonModifier($drawable), + default => throw new NotSupportedException('No modifier for ' . $drawable::class . ' found'), + }); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::encode() + * + * @throws EncoderException + */ + public function encode(?EncoderInterface $encoder = null): EncodedImageInterface + { + return $this->driver()->specializeEncoder( + $encoder ?: new AutoEncoder(), + )->encode($this); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::encodeUsingFormat() + * + * @throws EncoderException + */ + public function encodeUsingFormat(Format $format, mixed ...$options): EncodedImageInterface + { + return $this->encode(new FormatEncoder($format, ...$options)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::encodeUsingMediaType() + * + * @throws EncoderException + */ + public function encodeUsingMediaType(string|MediaType $mediaType, mixed ...$options): EncodedImageInterface + { + return $this->encode(new MediaTypeEncoder($mediaType, ...$options)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::encodeUsingFileExtension() + * + * @throws InvalidArgumentException + * @throws NotSupportedException + * @throws EncoderException + */ + public function encodeUsingFileExtension( + string|FileExtension $fileExtension, + mixed ...$options, + ): EncodedImageInterface { + return $this->encode(new FileExtensionEncoder($fileExtension, ...$options)); + } + + /** + * {@inheritdoc} + * + * @see ImageInterface::encodeUsingPath() + * + * @throws InvalidArgumentException + * @throws NotSupportedException + * @throws EncoderException + */ + public function encodeUsingPath(string $path, mixed ...$options,): EncodedImageInterface + { + return $this->encode(new FilePathEncoder($path, ...$options)); + } + + /** + * Build resizing dimension from various inputs including fractions based lengths. + * + * @throws InvalidArgumentException + * @throws AnalyzerException + * @return array{'width': ?int, 'height': ?int} + */ + private function resolveDimension(null|int|Fraction $width, null|int|Fraction $height): array + { + if ($width instanceof Fraction || $height instanceof Fraction) { + $size = $this->size(); + $width = ($width instanceof Fraction) ? (int) round($width->of($size->width())) : $width; + $height = ($height instanceof Fraction) ? (int) round($height->of($size->height())) : $height; + } + + return [ + 'width' => $width, + 'height' => $height, + ]; + } + + /** + * Show debug info for the current image. + * + * @return array + */ + public function __debugInfo(): array + { + try { + return [ + 'width' => $this->width(), + 'height' => $this->height(), + ]; + } catch (Throwable) { + return [ + 'width' => null, + 'height' => null, + ]; + } + } + + /** + * Clone image. + */ + public function __clone(): void + { + $this->driver = clone $this->driver; + $this->core = clone $this->core; + $this->exif = clone $this->exif; + } +} diff --git a/src/ImageManager.php b/src/ImageManager.php new file mode 100644 index 000000000..44c330c71 --- /dev/null +++ b/src/ImageManager.php @@ -0,0 +1,180 @@ +driver = $this->resolveDriver($driver, ...$options); + } + + /** + * {@inheritdoc} + * + * @see ImageManagerInterface::usingDriver() + * + * @throws InvalidArgumentException + */ + public static function usingDriver(string|DriverInterface $driver, mixed ...$options): ImageManagerInterface + { + return new self(self::resolveDriver($driver, ...$options)); + } + + /** + * {@inheritdoc} + * + * @see ImageManagerInterface::createImage() + * + * @throws InvalidArgumentException + * @throws DriverException + */ + public function createImage( + int $width, + int $height, + null|callable|AnimationFactoryInterface $animation = null, + ): ImageInterface { + if ($animation instanceof AnimationFactoryInterface) { + return $animation->image($this->driver); + } + + if (is_callable($animation)) { + return AnimationFactory::build($width, $height, $animation, $this->driver); + } + + return $this->driver->createImage($width, $height); + } + + /** + * {@inheritdoc} + * + * @see ImageManagerInterface::decode() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + */ + public function decode(mixed $source, null|string|array|DecoderInterface $decoders = null): ImageInterface + { + return $this->driver->decodeImage( + $source, + in_array(gettype($decoders), ['string', 'object']) ? [$decoders] : $decoders, + ); + } + + /** + * {@inheritdoc} + * + * @see ImageManagerInterface::decodePath() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + */ + public function decodePath(string|Stringable $path): ImageInterface + { + return $this->decode($path, FilePathImageDecoder::class); + } + + /** + * {@inheritdoc} + * + * @see ImageManagerInterface::decodeBinary() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + */ + public function decodeBinary(string|Stringable $binary): ImageInterface + { + return $this->decode($binary, BinaryImageDecoder::class); + } + + /** + * {@inheritdoc} + * + * @see ImageManagerInterface::decodeSplFileInfo() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + */ + public function decodeSplFileInfo(SplFileInfo $splFileInfo): ImageInterface + { + return $this->decode($splFileInfo, SplFileInfoImageDecoder::class); + } + + /** + * {@inheritdoc} + * + * @see ImageManagerInterface::decodeBase64() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + */ + public function decodeBase64(string|Stringable $base64): ImageInterface + { + return $this->decode($base64, Base64ImageDecoder::class); + } + + /** + * {@inheritdoc} + * + * @see ImageManagerInterface::decodeDataUri() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + */ + public function decodeDataUri(string|Stringable|DataUriInterface $dataUri): ImageInterface + { + return $this->decode($dataUri, DataUriImageDecoder::class); + } + + /** + * {@inheritdoc} + * + * @see ImageManagerInterface::decodeStream() + * + * @throws InvalidArgumentException + * @throws ImageDecoderException + * @throws DriverException + */ + public function decodeStream(mixed $stream): ImageInterface + { + return $this->decode($stream, StreamImageDecoder::class); + } +} diff --git a/src/InputHandler.php b/src/InputHandler.php new file mode 100644 index 000000000..d2df7b5fd --- /dev/null +++ b/src/InputHandler.php @@ -0,0 +1,165 @@ + $decoders + */ + public function __construct( + protected array $decoders = [], + protected ?DriverInterface $driver = null, + ) { + // + } + + /** + * Static factory method to create input handler for both image and color handling. + * + * @param array $decoders + */ + public static function usingDecoders(array $decoders, ?DriverInterface $driver = null): self + { + return new self($decoders, $driver); + } + + /** + * {@inheritdoc} + * + * @see InputHandlerInterface::handle() + * + * @throws InvalidArgumentException + * @throws NotSupportedException + * @throws DriverException + */ + public function handle(mixed $input): ImageInterface|ColorInterface + { + if ($input === null) { + throw new InvalidArgumentException('Unable to decode from null'); + } + + if ($input === '') { + throw new InvalidArgumentException('Unable to decode from empty string'); + } + + // if handler has only one single decoder run it can run directly + if (count($this->decoders) === 1) { + return $this->decoders()->current()->decode($input); + } + + // multiple decoders: try to find the matching decoder for the input + foreach ($this->decoders() as $decoder) { + if ($decoder->supports($input)) { + return $decoder->decode($input); + } + } + + throw new NotSupportedException('Unprocessable input'); + } + + /** + * Yield all decoders. + * + * @throws InvalidArgumentException + * @throws DriverException + */ + private function decoders(): Generator + { + foreach ($this->decoders as $decoder) { + yield $this->decoder($decoder); + } + } + + /** + * Resolve the given classname or object to a decoder object. + * + * @throws InvalidArgumentException + * @throws DriverException + */ + private function decoder(string|DecoderInterface $decoder): DecoderInterface + { + if (is_string($decoder)) { + if (!is_subclass_of($decoder, DecoderInterface::class)) { + throw new InvalidArgumentException('Decoder must implement ' . DecoderInterface::class); + } + + $decoder = new $decoder(); + } + + if ($this->driver === null) { + return $decoder; + } + + try { + return $this->driver->specializeDecoder($decoder); + } catch (NotSupportedException $e) { + throw new DriverException( + 'Failed to resolve decoder ' . $decoder::class . ' with driver ' . $this->driver::class, + previous: $e, + ); + } + } +} diff --git a/src/Interfaces/AnalyzerInterface.php b/src/Interfaces/AnalyzerInterface.php new file mode 100644 index 000000000..a9cd56ead --- /dev/null +++ b/src/Interfaces/AnalyzerInterface.php @@ -0,0 +1,13 @@ + + */ +interface CollectionInterface extends Traversable +{ + /** + * Determine if the collection has item at given key. + */ + public function has(int|string $key): bool; + + /** + * Add item to collection. + * + * @return CollectionInterface + */ + public function push(mixed $item): self; + + /** + * Return item for given key or return default if key does not exist. + */ + public function get(int|string $key, mixed $default = null): mixed; + + /** + * Set item in collection. + */ + public function set(int|string $key, mixed $item): self; + + /** + * Return item at given numeric position starting at 0. + */ + public function at(int $key = 0, mixed $default = null): mixed; + + /** + * Return first item in collection. + */ + public function first(): mixed; + + /** + * Return last item in collection. + */ + public function last(): mixed; + + /** + * Return item count of collection. + */ + public function count(): int; + + /** + * Map each item of collection by given callback. + */ + public function map(callable $callback): self; + + /** + * Run callback on each item of the collection and remove it if it does not return true. + */ + public function filter(callable $callback): self; + + /** + * Empty collection. + * + * @return CollectionInterface + */ + public function clear(): self; + + /** + * Transform collection as array. + * + * @return array + */ + public function toArray(): array; + + /** + * Extract items based on given values and discard the rest. + * + * @return CollectionInterface + */ + public function slice(int $offset, ?int $length = 0): self; +} diff --git a/src/Interfaces/ColorChannelInterface.php b/src/Interfaces/ColorChannelInterface.php new file mode 100644 index 000000000..adf7ae030 --- /dev/null +++ b/src/Interfaces/ColorChannelInterface.php @@ -0,0 +1,45 @@ + + */ + public function channels(): array; + + /** + * Retrieve the color channel by its classname. + */ + public function channel(string $classname): ColorChannelInterface; + + /** + * Get the alpha channel of the color. + */ + public function alpha(): ColorChannelInterface; + + /** + * Convert color to given colorspace. + */ + public function toColorspace(string|ColorspaceInterface $colorspace): self; + + /** + * Determine if the current color is gray. + */ + public function isGrayscale(): bool; + + /** + * Determine if the current color is (semi) transparent. + */ + public function isTransparent(): bool; + + /** + * Determine whether the current color is completely transparent. + */ + public function isClear(): bool; + + /** + * Return a copy of the current color with the specified transparency value. + */ + public function withTransparency(float $transparency): self; + + /** + * Return a copy of the current color with adjusted brightness. + * Positive values lighten, negative values darken. Range: -100 to 100. + */ + public function withBrightness(int $level): self; + + /** + * Return a copy of the current color with adjusted saturation. + * Positive values saturate, negative values desaturate. Range: -100 to 100. + */ + public function withSaturation(int $level): self; + + /** + * Return an inverted copy of the current color. + */ + public function withInversion(): self; +} diff --git a/src/Interfaces/ColorProcessorInterface.php b/src/Interfaces/ColorProcessorInterface.php new file mode 100644 index 000000000..892d6875b --- /dev/null +++ b/src/Interfaces/ColorProcessorInterface.php @@ -0,0 +1,23 @@ + + */ + public static function channels(): array; + + /** + * Create new color in colorspace from given normalized (0-1) channel values. + * + * @param array $normalized + */ + public static function colorFromNormalized(array $normalized): ColorInterface; + + /** + * Convert given color to the format of the current colorspace. + */ + public function importColor(ColorInterface $color): ColorInterface; +} diff --git a/src/Interfaces/CoreInterface.php b/src/Interfaces/CoreInterface.php new file mode 100644 index 000000000..fe99ce7fe --- /dev/null +++ b/src/Interfaces/CoreInterface.php @@ -0,0 +1,64 @@ + + */ + public function setNative(mixed $native): self; + + /** + * Count number of frames of animated image core. + */ + public function count(): int; + + /** + * Return frame of given position in an animated image. + */ + public function frame(int $position): FrameInterface; + + /** + * Add new frame to core. + * + * @return CoreInterface + */ + public function add(FrameInterface $frame): self; + + /** + * Return number of repetitions of an animated image. + */ + public function loops(): int; + + /** + * Set the number of repetitions for an animation. Where a value of 0 means infinite repetition. + * + * @return CoreInterface + */ + public function setLoops(int $loops): self; + + /** + * Get first frame in core. + */ + public function first(): FrameInterface; + + /** + * Get last frame in core. + */ + public function last(): FrameInterface; + + /** + * Access meta information of core instance. + */ + public function meta(): CollectionInterface; +} diff --git a/src/Interfaces/DataUriInterface.php b/src/Interfaces/DataUriInterface.php new file mode 100644 index 000000000..3a01594d3 --- /dev/null +++ b/src/Interfaces/DataUriInterface.php @@ -0,0 +1,93 @@ + $parameters + */ + public static function create( + string $data, + null|string|MediaType $mediaType = null, + array $parameters = [], + ): self; + + /** + * Return current data uri data. + */ + public function data(): string; + + /** + * Set data of current data uri scheme. + */ + public function setData(string $data): self; + + /** + * Get media type of current data uri output. + */ + public function mediaType(): ?string; + + /** + * Set media type of current data uri output. + */ + public function setMediaType(null|string|MediaType $mediaType): self; + + /** + * Get all parameters of current data uri output. + * + * @return array + */ + public function parameters(): array; + + /** + * Set (overwrite) all parameters of current data uri output. + * + * @param array $parameters + */ + public function setParameters(array $parameters): self; + + /** + * Append given parameters to current data uri output. + * + * @param array $parameters + */ + public function appendParameters(array $parameters): self; + + /** + * Get value of given parameter, return null if parameter is not set. + */ + public function parameter(string $key): ?string; + + /** + * Set (overwrite) parameter of given key to given value. + */ + public function setParameter(string $key, string $value): self; + + /** + * Get charset of current data uri scheme, null if no charset is defined. + */ + public function charset(): ?string; + + /** + * Define charset of current data uri scheme. + */ + public function setCharset(string $charset): self; + + /** + * Transform current data uri scheme to string. + */ + public function toString(): string; +} diff --git a/src/Interfaces/DecoderInterface.php b/src/Interfaces/DecoderInterface.php new file mode 100644 index 000000000..1442baeee --- /dev/null +++ b/src/Interfaces/DecoderInterface.php @@ -0,0 +1,21 @@ + $frames + */ + public function createCore(array $frames): CoreInterface; + + /** + * Decode image source with given decoders. Try all image decoders by default. + * + * Image sources can be as follows: + * + * - Path in filesystem + * - Raw binary image data + * - Base64 encoded image data + * - Data Uri + * - Stream resource + * - SplFileInfo object + * - Intervention Image Instance (Intervention\Image\Image) + * - Encoded Intervention Image (Intervention\Image\EncodedImage) + * - Driver-specific image (instance of GDImage or Imagick) + * + * @param array $decoders + */ + public function decodeImage(mixed $input, ?array $decoders = null): ImageInterface; + + /** + * Decode color source with given decoders. Try all color decoders by default. + * + * @param array $decoders + */ + public function decodeColor(mixed $input, ?array $decoders = null): ColorInterface; + + /** + * Return color processor for the given image and its colorspace. + */ + public function colorProcessor(ImageInterface $image): ColorProcessorInterface; + + /** + * Return font processor of the current driver. + */ + public function fontProcessor(): FontProcessorInterface; + + /** + * Check whether all requirements for operating the driver are met and + * throw exception if the check fails. + * + * @throws MissingDependencyException + */ + public function checkHealth(): void; + + /** + * Check if the current driver supports the given format and if the + * underlying PHP extension was built with support for the format. + */ + public function supports(string|Format|FileExtension|MediaType $identifier): bool; + + /** + * Return the version number of the image driver currently in use. + */ + public function version(): string; +} diff --git a/src/Interfaces/EncodedImageInterface.php b/src/Interfaces/EncodedImageInterface.php new file mode 100644 index 000000000..8c5fe07db --- /dev/null +++ b/src/Interfaces/EncodedImageInterface.php @@ -0,0 +1,28 @@ + + */ +interface ImageInterface extends IteratorAggregate, Countable +{ + /** + * Return the image driver. + */ + public function driver(): DriverInterface; + + /** + * Return the image core. + */ + public function core(): CoreInterface; + + /** + * Return the image origin. + */ + public function origin(): OriginInterface; + + /** + * Set the image origin. + */ + public function setOrigin(OriginInterface $origin): self; + + /** + * Return the image width in pixels. + * + * @link https://image.intervention.io/v4/basics/meta-information#read-the-pixel-width + */ + public function width(): int; + + /** + * Return the image height in pixels. + * + * @link https://image.intervention.io/v4/basics/meta-information#read-the-pixel-height + */ + public function height(): int; + + /** + * Return the image size as an object. + * + * @link https://image.intervention.io/v4/basics/meta-information#read-the-image-size-as-an-object + */ + public function size(): SizeInterface; + + /** + * Save the image to the given path. If no path is given, the image will + * be saved at its original location. + * + * @link https://image.intervention.io/v4/basics/image-output#encode--save-combined + */ + public function save(?string $path = null, mixed ...$options): self; + + /** + * Apply the given modifier to the image. + * + * @link https://image.intervention.io/v4/modifying-images/custom-modifiers + */ + public function modify(ModifierInterface $modifier): self; + + /** + * Analyze the image with the given analyzer. + */ + public function analyze(AnalyzerInterface $analyzer): mixed; + + /** + * Determine if the image is animated. + * + * @link https://image.intervention.io/v4/modifying-images/animations#check-the-current-image-instance-for-animation + */ + public function isAnimated(): bool; + + /** + * Remove all frames but keep the one at the specified position. + * + * Integer values select the exact frame position, while string values + * represent a percentage between '0%' and '100%' to determine the + * approximate frame position. + * + * @link https://image.intervention.io/v4/modifying-images/animations#remove-animation + */ + public function removeAnimation(int|string $position = 0): self; + + /** + * Keep only the frames defined by offset and length, discarding the rest. + * + * @link https://image.intervention.io/v4/modifying-images/animations#change-the-animation-frames + */ + public function sliceAnimation(int $offset = 0, ?int $length = null): self; + + /** + * Return the animation loop count. + * + * @link https://image.intervention.io/v4/modifying-images/animations#read-the-animation-iteration-count + */ + public function loops(): int; + + /** + * Set the animation loop count. + * + * @link https://image.intervention.io/v4/modifying-images/animations#change-the-animation-iteration-count + */ + public function setLoops(int $loops): self; + + /** + * Return the EXIF data of the image. + * + * @link https://image.intervention.io/v4/basics/meta-information#exif-information + */ + public function exif(?string $query = null): mixed; + + /** + * Set the EXIF data of the image. + */ + public function setExif(CollectionInterface $exif): self; + + /** + * Return the image resolution in DPI. + * + * @link https://image.intervention.io/v4/basics/meta-information#image-resolution + */ + public function resolution(): ResolutionInterface; + + /** + * Set the image resolution in DPI. + * + * @link https://image.intervention.io/v4/basics/meta-information#image-resolution + */ + public function setResolution(float $x, float $y): self; + + /** + * Return the image colorspace. + * + * @link https://image.intervention.io/v4/basics/colors#read-the-image-colorspace + */ + public function colorspace(): ColorspaceInterface; + + /** + * Transform the image to the given colorspace. + * + * @link https://image.intervention.io/v4/basics/colors#change-the-image-colorspace + */ + public function setColorspace(string|ColorspaceInterface $colorspace): self; + + /** + * Return the color of the pixel at the given position and frame. + * + * @link https://image.intervention.io/v4/basics/colors#color-information + */ + public function colorAt(int $x, int $y, int $frame = 0): ColorInterface; + + /** + * Return the colors of the pixel at the given position across all frames. + * + * @link https://image.intervention.io/v4/basics/colors#read-all-colors-of-certain-pixels-in-animated-images + */ + public function colorsAt(int $x, int $y): CollectionInterface; + + /** + * Return the background color used to replace transparent areas during + * encoding to formats that do not support transparency. + * + * @link https://image.intervention.io/v4/basics/configuration-drivers#configuration-options + */ + public function backgroundColor(): ColorInterface; + + /** + * Set the background color used to replace transparent areas during + * encoding to formats that do not support transparency. + * + * @link https://image.intervention.io/v4/basics/configuration-drivers#configuration-options + */ + public function setBackgroundColor(string|ColorInterface $color): self; + + /** + * Replace transparent areas with the given color or the configured background color. + * + * @link https://image.intervention.io/v4/basics/colors#merge-transparent-areas-with-color + */ + public function fillTransparentAreas(null|string|ColorInterface $color = null): self; + + /** + * Return the ICC color profile. + * + * @link https://image.intervention.io/v4/basics/colors#color-profiles + */ + public function profile(): ProfileInterface; + + /** + * Set the ICC color profile. + * + * @link https://image.intervention.io/v4/basics/colors#color-profiles + */ + public function setProfile(ProfileInterface $profile): self; + + /** + * Remove the ICC color profile. + * + * @link https://image.intervention.io/v4/basics/colors#color-profiles + */ + public function removeProfile(): self; + + /** + * Reduce the number of colors in the image to the given limit. + * + * @link https://image.intervention.io/v4/modifying-images/effects#reduce-colors + */ + public function reduceColors(int $limit, null|string|ColorInterface $background = null): self; + + /** + * Sharpen the image by the given level. + * + * @link https://image.intervention.io/v4/modifying-images/effects#sharpening-effect + */ + public function sharpen(int $level = 10): self; + + /** + * Turn the image into a grayscale version. + * + * @link https://image.intervention.io/v4/modifying-images/effects#convert-image-to-a-grayscale-version + */ + public function grayscale(): self; + + /** + * Adjust the image brightness by the given level. + * + * @link https://image.intervention.io/v4/modifying-images/effects#change-the-image-brightness + */ + public function brightness(int $level): self; + + /** + * Adjust the image contrast by the given level. + * + * @link https://image.intervention.io/v4/modifying-images/effects#change-the-image-contrast + */ + public function contrast(int $level): self; + + /** + * Apply gamma correction to the image. + * + * @link https://image.intervention.io/v4/modifying-images/effects#gamma-correction + */ + public function gamma(float $gamma): self; + + /** + * Adjust the intensity of the RGB color channels. + * + * @link https://image.intervention.io/v4/modifying-images/effects#color-correction + */ + public function colorize(int $red = 0, int $green = 0, int $blue = 0): self; + + /** + * Mirror the image in the given direction. + * + * @link https://image.intervention.io/v4/modifying-images/effects#mirror-images + */ + public function flip(Direction $direction = Direction::HORIZONTAL): self; + + /** + * Apply a blur effect with the given level. + * + * @link https://image.intervention.io/v4/modifying-images/effects#blur-effect + */ + public function blur(int $level = 5): self; + + /** + * Invert the image colors. + * + * @link https://image.intervention.io/v4/modifying-images/effects#invert-colors + */ + public function invert(): self; + + /** + * Apply a pixelation effect with the given tile size. + * + * @link https://image.intervention.io/v4/modifying-images/effects#pixelation-effect + */ + public function pixelate(int $size): self; + + /** + * Rotate the image clockwise by the given angle. + * + * @link https://image.intervention.io/v4/modifying-images/effects#image-rotation + */ + public function rotate(float $angle, null|string|ColorInterface $background = null): self; + + /** + * Orient the image upright based on EXIF data. + * + * @link https://image.intervention.io/v4/modifying-images/effects#image-orientation-according-to-exif-data + */ + public function orient(): self; + + /** + * Draw text on the image. + * + * @link https://image.intervention.io/v4/modifying-images/text-fonts + */ + public function text(string $text, int $x, int $y, callable|FontInterface $font): self; + + /** + * Resize the image to the given width and/or height. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#simple-image-resizing + */ + public function resize(null|int|Fraction $width = null, null|int|Fraction $height = null): self; + + /** + * Resize the image to the given width and/or height without exceeding + * the original dimensions. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#resize-without-exceeding-the-original-size + */ + public function resizeDown(null|int|Fraction $width = null, null|int|Fraction $height = null): self; + + /** + * Resize the image to the given width and/or height while maintaining + * the aspect ratio. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#resize-images-proportionally + */ + public function scale(null|int|Fraction $width = null, null|int|Fraction $height = null): self; + + /** + * Resize the image to the given width and/or height while maintaining + * the aspect ratio and without exceeding the original dimensions. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#scale-images-but-do-not-exceed-the-original-size + */ + public function scaleDown(null|int|Fraction $width = null, null|int|Fraction $height = null): self; + + /** + * Crop and resize the image to cover the given dimensions exactly. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#cropping--resizing-combined + */ + public function cover( + int|Fraction $width, + int|Fraction $height, + string|Alignment $alignment = Alignment::CENTER, + ): self; + + /** + * Crop and resize the image to cover the given dimensions without + * exceeding the original dimensions. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#fitted-resizing-without-exceeding-the-original-size + */ + public function coverDown( + int|Fraction $width, + int|Fraction $height, + string|Alignment $alignment = Alignment::CENTER, + ): self; + + /** + * Resize the image canvas to the given width and height without resampling + * + * The alignment position defines where the original image is fixed, + * and new areas are filled with the given background color. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#resize-image-boundaries-without-resampling-the-original-image + */ + public function resizeCanvas( + null|int|Fraction $width = null, + null|int|Fraction $height = null, + null|string|ColorInterface $background = null, + string|Alignment $alignment = Alignment::CENTER + ): self; + + /** + * Resize the image canvas by adding or subtracting the given width and + * height relative to the original dimensions. + * + * The alignment position defines where the original image is fixed, + * and new areas are filled with the given background color. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#resize-image-boundaries-relative-to-the-original + */ + public function resizeCanvasRelative( + null|int|Fraction $width = null, + null|int|Fraction $height = null, + null|string|ColorInterface $background = null, + string|Alignment $alignment = Alignment::CENTER + ): self; + + /** + * Resize the image to fit within the given dimensions while maintaining + * the aspect ratio. New areas are filled with the given background color. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#contain-resizing-1 + */ + public function contain( + int|Fraction $width, + int|Fraction $height, + null|string|ColorInterface $background = null, + string|Alignment $alignment = Alignment::CENTER + ): self; + + /** + * Resize the image to fit within the given dimensions while maintaining + * the aspect ratio and without exceeding the original dimensions. New + * areas are filled with the given background color. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#contain-resizing-without-upscaling + */ + public function containDown( + int|Fraction $width, + int|Fraction $height, + null|string|ColorInterface $background = null, + string|Alignment $alignment = Alignment::CENTER + ): self; + + /** + * Cut out a rectangular part of the image with the given width and height + * at the given alignment position offset by x and y. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#cut-out-a-rectangular-part + */ + public function crop( + int|Fraction $width, + int|Fraction $height, + int $x = 0, + int $y = 0, + null|string|ColorInterface $background = null, + string|Alignment $alignment = Alignment::TOP_LEFT + ): self; + + /** + * Trim border areas of similar color within the given tolerance. + * + * @link https://image.intervention.io/v4/modifying-images/resizing#remove-border-areas-in-similar-color + */ + public function trim(int $tolerance = 0): self; + + /** + * Insert another image at the given position relative to the alignment position. + * + * @link https://image.intervention.io/v4/modifying-images/inserting#insert-images + */ + public function insert( + mixed $image, + int $x = 0, + int $y = 0, + string|Alignment $alignment = Alignment::TOP_LEFT, + float $transparency = 1 + ): self; + + /** + * Fill the image with the given color. If coordinates are specified, the + * fill is applied as a flood fill starting at that position. Otherwise + * the entire image area is filled. + * + * @link https://image.intervention.io/v4/modifying-images/drawing#fill-images-with-color + */ + public function fill(string|ColorInterface $color, ?int $x = null, ?int $y = null): self; + + /** + * Draw a single pixel at the given position in the given color. + * + * @link https://image.intervention.io/v4/modifying-images/drawing#draw-pixels + */ + public function drawPixel(int $x, int $y, string|ColorInterface $color): self; + + /** + * Draw a rectangle on the image. + * + * @link https://image.intervention.io/v4/modifying-images/drawing#draw-a-rectangle + */ + public function drawRectangle(callable|Rectangle $rectangle): self; + + /** + * Draw an ellipse on the image. + * + * @link https://image.intervention.io/v4/modifying-images/drawing#draw-ellipses + */ + public function drawEllipse(callable|Ellipse $ellipse): self; + + /** + * Draw a circle on the image. + * + * @link https://image.intervention.io/v4/modifying-images/drawing#draw-a-circle + */ + public function drawCircle(callable|Circle $circle): self; + + /** + * Draw a polygon on the image. + * + * @link https://image.intervention.io/v4/modifying-images/drawing#draw-a-polygon + */ + public function drawPolygon(callable|Polygon $polygon): self; + + /** + * Draw a line on the image. + * + * @link https://image.intervention.io/v4/modifying-images/drawing#draw-a-line + */ + public function drawLine(callable|Line $line): self; + + /** + * Draw a bezier curve on the image. + * + * @link https://image.intervention.io/v4/modifying-images/drawing#draw-bezier-curves + */ + public function drawBezier(callable|Bezier $bezier): self; + + /** + * Draw a geometric object on the image. + * + * @link https://image.intervention.io/v4/modifying-images/drawing + */ + public function draw(DrawableInterface $drawable): self; + + /** + * Encode the image with the given encoder. If no encoder is provided, + * the format is detected from the original image automatically. + * + * @link https://image.intervention.io/v4/basics/image-output#encode-images-with-encoder-objects + */ + public function encode(?EncoderInterface $encoder = null): EncodedImageInterface; + + /** + * Encode the image in the given format. + * + * @link https://image.intervention.io/v4/basics/image-output#encode-images-using-format + */ + public function encodeUsingFormat(Format $format, mixed ...$options): EncodedImageInterface; + + /** + * Encode the image based on the given media (MIME) type. + * + * @link https://image.intervention.io/v4/basics/image-output#encode-images-using-media-mime-types + */ + public function encodeUsingMediaType(string|MediaType $mediaType, mixed ...$options): EncodedImageInterface; + + /** + * Encode the image based on the given file extension. + * + * @link https://image.intervention.io/v4/basics/image-output#encode-images-using-file-extensions + */ + public function encodeUsingFileExtension( + string|FileExtension $fileExtension, + mixed ...$options, + ): EncodedImageInterface; + + /** + * Encode the image based on the given file path's extension. + * + * @link https://image.intervention.io/v4/basics/image-output#encode-images-using-file-paths + */ + public function encodeUsingPath(string $path, mixed ...$options): EncodedImageInterface; +} diff --git a/src/Interfaces/ImageManagerInterface.php b/src/Interfaces/ImageManagerInterface.php new file mode 100644 index 000000000..577488ca6 --- /dev/null +++ b/src/Interfaces/ImageManagerInterface.php @@ -0,0 +1,88 @@ +|DecoderInterface $decoders + */ + public function decode(mixed $source, null|string|array|DecoderInterface $decoders = null): ImageInterface; + + /** + * Decode an image from the given file path. + * + * @link https://image.intervention.io/v4/basics/instantiation#read-images-from-file-paths + */ + public function decodePath(string|Stringable $path): ImageInterface; + + /** + * Decode an image from the given raw binary data. + * + * @link https://image.intervention.io/v4/basics/instantiation#read-images-from-binary-data + */ + public function decodeBinary(string|Stringable $binary): ImageInterface; + + /** + * Decode an image from the given SplFileInfo object. + * + * @link https://image.intervention.io/v4/basics/instantiation#read-images-from-splfileinfo-objects + */ + public function decodeSplFileInfo(SplFileInfo $splFileInfo): ImageInterface; + + /** + * Decode an image from the given base64 encoded data. + * + * @link https://image.intervention.io/v4/basics/instantiation#read-images-from-base64-encoded-data + */ + public function decodeBase64(string|Stringable $base64): ImageInterface; + + /** + * Decode an image from the given data URI. + * + * @link https://image.intervention.io/v4/basics/instantiation#read-images-from-data-uri-scheme + */ + public function decodeDataUri(string|Stringable|DataUriInterface $dataUri): ImageInterface; + + /** + * Decode an image from the given stream resource. + * + * @link https://image.intervention.io/v4/basics/instantiation#read-images-from-stream + */ + public function decodeStream(mixed $stream): ImageInterface; +} diff --git a/src/Interfaces/InputHandlerInterface.php b/src/Interfaces/InputHandlerInterface.php new file mode 100644 index 000000000..6aa046c96 --- /dev/null +++ b/src/Interfaces/InputHandlerInterface.php @@ -0,0 +1,13 @@ + + */ +interface PointInterface extends IteratorAggregate +{ + /** + * Return x position. + */ + public function x(): int; + + /** + * Return y position. + */ + public function y(): int; + + /** + * Set x position. + */ + public function setX(int $x): self; + + /** + * Set y position. + */ + public function setY(int $y): self; + + /** + * Move X coordinate. + */ + public function moveX(int $value): self; + + /** + * Move Y coordinate. + */ + public function moveY(int $value): self; + + /** + * Move position of current point by given coordinates. + */ + public function move(int $x, int $y): self; + + /** + * Set position of point. + */ + public function setPosition(int $x, int $y): self; + + /** + * Rotate the current point clockwise around given pivot point. + */ + public function rotate(float $angle, self $pivot): self; +} diff --git a/src/Interfaces/ProfileInterface.php b/src/Interfaces/ProfileInterface.php new file mode 100644 index 000000000..7618b9f0f --- /dev/null +++ b/src/Interfaces/ProfileInterface.php @@ -0,0 +1,20 @@ + + */ +interface SizeInterface extends Traversable +{ + /** + * Get width. + */ + public function width(): int; + + /** + * Get height. + */ + public function height(): int; + + /** + * Get pivot point. + */ + public function pivot(): PointInterface; + + /** + * Set width. + */ + public function setWidth(int $width): self; + + /** + * Set height. + */ + public function setHeight(int $height): self; + + /** + * Set pivot point. + */ + public function setPivot(PointInterface $pivot): self; + + /** + * Calculate aspect ratio of the current size. + */ + public function aspectRatio(): float; + + /** + * Determine if current size fits within given size. + */ + public function fitsWithin(self $size): bool; + + /** + * Determine if size is in landscape format. + */ + public function isLandscape(): bool; + + /** + * Determine if size is in portrait format. + */ + public function isPortrait(): bool; + + /** + * Move pivot to the given alignment position in the size and adjust the new position by given offset values. + */ + public function movePivot(string|Alignment $alignment, int $x = 0, int $y = 0): self; + + /** + * Align pivot relative to given size at given alignment position. + */ + public function alignPivotTo(self $size, string|Alignment $alignment): self; + + /** + * Calculate the relative position to another size based on the pivot point settings of both sizes. + */ + public function offsetTo(self $size): PointInterface; + + /** + * @see Resizer::resize() + */ + public function resize(?int $width = null, ?int $height = null): self; + + /** + * @see Resizer::resizeDown() + */ + public function resizeDown(?int $width = null, ?int $height = null): self; + + /** + * @see Resizer::scale() + */ + public function scale(?int $width = null, ?int $height = null): self; + + /** + * @see Resizer::scaleDown() + */ + public function scaleDown(?int $width = null, ?int $height = null): self; + + /** + * @see Resizer::cover() + */ + public function cover(int $width, int $height): self; + + /** + * @see Resizer::contain() + */ + public function contain(int $width, int $height): self; + + /** + * @see Resizer::containDown() + */ + public function containDown(int $width, int $height): self; +} diff --git a/src/Interfaces/SpecializableInterface.php b/src/Interfaces/SpecializableInterface.php new file mode 100644 index 000000000..c9aa3a99a --- /dev/null +++ b/src/Interfaces/SpecializableInterface.php @@ -0,0 +1,26 @@ + + */ + public function specializationArguments(): array; + + /** + * Set the driver for which the object will be specialized. + */ + public function setDriver(DriverInterface $driver): self; + + /** + * Return the driver for which the object is specialized. + */ + public function driver(): DriverInterface; +} diff --git a/src/Interfaces/SpecializedInterface.php b/src/Interfaces/SpecializedInterface.php new file mode 100644 index 000000000..5d23bab06 --- /dev/null +++ b/src/Interfaces/SpecializedInterface.php @@ -0,0 +1,10 @@ +parse($value); - } - - /** - * Parses given value as color - * - * @param mixed $value - * @return \Intervention\Image\AbstractColor - */ - public function parse($value) - { - switch (true) { - - case is_string($value): - $this->initFromString($value); - break; - - case is_int($value): - $this->initFromInteger($value); - break; - - case is_array($value): - $this->initFromArray($value); - break; - - case is_object($value): - $this->initFromObject($value); - break; - - case is_null($value): - $this->initFromArray(array(255, 255, 255, 0)); - break; - - default: - throw new \Intervention\Image\Exception\NotReadableException( - "Color format ({$value}) cannot be read." - ); - } - - return $this; - } - - /** - * Formats current color instance into given format - * - * @param string $type - * @return mixed - */ - public function format($type) - { - switch (strtolower($type)) { - - case 'rgba': - return $this->getRgba(); - - case 'hex': - return $this->getHex('#'); - - case 'int': - case 'integer': - return $this->getInt(); - - case 'array': - return $this->getArray(); - - case 'obj': - case 'object': - return $this; - - default: - throw new \Intervention\Image\Exception\NotSupportedException( - "Color format ({$type}) is not supported." - ); - } - } - - /** - * Reads RGBA values from string into array - * - * @param string $value - * @return array - */ - protected function rgbaFromString($value) - { - $result = false; - - // parse color string in hexidecimal format like #cccccc or cccccc or ccc - $hexPattern = '/^#?([a-f0-9]{1,2})([a-f0-9]{1,2})([a-f0-9]{1,2})$/i'; - - // parse color string in format rgb(140, 140, 140) - $rgbPattern = '/^rgb ?\(([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9]{1,3})\)$/i'; - - // parse color string in format rgba(255, 0, 0, 0.5) - $rgbaPattern = '/^rgba ?\(([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9]{1,3}), ?([0-9.]{1,4})\)$/i'; - - if (preg_match($hexPattern, $value, $matches)) { - $result = array(); - $result[0] = strlen($matches[1]) == '1' ? hexdec($matches[1].$matches[1]) : hexdec($matches[1]); - $result[1] = strlen($matches[2]) == '1' ? hexdec($matches[2].$matches[2]) : hexdec($matches[2]); - $result[2] = strlen($matches[3]) == '1' ? hexdec($matches[3].$matches[3]) : hexdec($matches[3]); - $result[3] = 1; - } elseif (preg_match($rgbPattern, $value, $matches)) { - $result = array(); - $result[0] = ($matches[1] >= 0 && $matches[1] <= 255) ? intval($matches[1]) : 0; - $result[1] = ($matches[2] >= 0 && $matches[2] <= 255) ? intval($matches[2]) : 0; - $result[2] = ($matches[3] >= 0 && $matches[3] <= 255) ? intval($matches[3]) : 0; - $result[3] = 1; - } elseif (preg_match($rgbaPattern, $value, $matches)) { - $result = array(); - $result[0] = ($matches[1] >= 0 && $matches[1] <= 255) ? intval($matches[1]) : 0; - $result[1] = ($matches[2] >= 0 && $matches[2] <= 255) ? intval($matches[2]) : 0; - $result[2] = ($matches[3] >= 0 && $matches[3] <= 255) ? intval($matches[3]) : 0; - $result[3] = ($matches[4] >= 0 && $matches[4] <= 1) ? $matches[4] : 0; - } else { - throw new \Intervention\Image\Exception\NotReadableException( - "Unable to read color ({$value})." - ); - } - - return $result; - } -} diff --git a/src/Intervention/Image/AbstractDecoder.php b/src/Intervention/Image/AbstractDecoder.php deleted file mode 100644 index 9adc57f98..000000000 --- a/src/Intervention/Image/AbstractDecoder.php +++ /dev/null @@ -1,348 +0,0 @@ -data = $data; - } - - /** - * Init from fiven URL - * - * @param string $url - * @return \Intervention\Image\Image - */ - public function initFromUrl($url) - { - - $options = array( - 'http' => array( - 'method'=>"GET", - 'header'=>"Accept-language: en\r\n". - "User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2\r\n" - ) - ); - - $context = stream_context_create($options); - - - if ($data = @file_get_contents($url, false, $context)) { - return $this->initFromBinary($data); - } - - throw new \Intervention\Image\Exception\NotReadableException( - "Unable to init from given url (".$url.")." - ); - } - - /** - * Init from given stream - * - * @param $stream - * @return \Intervention\Image\Image - */ - public function initFromStream($stream) - { - $offset = ftell($stream); - $shouldAndCanSeek = $offset !== 0 && $this->isStreamSeekable($stream); - - if ($shouldAndCanSeek) { - rewind($stream); - } - - $data = @stream_get_contents($stream); - - if ($shouldAndCanSeek) { - fseek($stream, $offset); - } - - if ($data) { - return $this->initFromBinary($data); - } - - throw new \Intervention\Image\Exception\NotReadableException( - "Unable to init from given stream" - ); - } - - /** - * Checks if we can move the pointer for this stream - * - * @param resource $stream - * @return bool - */ - private function isStreamSeekable($stream) - { - $metadata = stream_get_meta_data($stream); - return $metadata['seekable']; - } - - /** - * Determines if current source data is GD resource - * - * @return boolean - */ - public function isGdResource() - { - if (is_resource($this->data)) { - return (get_resource_type($this->data) == 'gd'); - } - - return false; - } - - /** - * Determines if current source data is Imagick object - * - * @return boolean - */ - public function isImagick() - { - return is_a($this->data, 'Imagick'); - } - - /** - * Determines if current source data is Intervention\Image\Image object - * - * @return boolean - */ - public function isInterventionImage() - { - return is_a($this->data, '\Intervention\Image\Image'); - } - - /** - * Determines if current data is SplFileInfo object - * - * @return boolean - */ - public function isSplFileInfo() - { - return is_a($this->data, 'SplFileInfo'); - } - - /** - * Determines if current data is Symfony UploadedFile component - * - * @return boolean - */ - public function isSymfonyUpload() - { - return is_a($this->data, 'Symfony\Component\HttpFoundation\File\UploadedFile'); - } - - /** - * Determines if current source data is file path - * - * @return boolean - */ - public function isFilePath() - { - if (is_string($this->data)) { - return is_file($this->data); - } - - return false; - } - - /** - * Determines if current source data is url - * - * @return boolean - */ - public function isUrl() - { - return (bool) filter_var($this->data, FILTER_VALIDATE_URL); - } - - /** - * Determines if current source data is a stream resource - * - * @return boolean - */ - public function isStream() - { - if (!is_resource($this->data)) return false; - if (get_resource_type($this->data) !== 'stream') return false; - - return true; - } - - /** - * Determines if current source data is binary data - * - * @return boolean - */ - public function isBinary() - { - if (is_string($this->data)) { - $mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $this->data); - return (substr($mime, 0, 4) != 'text' && $mime != 'application/x-empty'); - } - - return false; - } - - /** - * Determines if current source data is data-url - * - * @return boolean - */ - public function isDataUrl() - { - $data = $this->decodeDataUrl($this->data); - - return is_null($data) ? false : true; - } - - /** - * Determines if current source data is base64 encoded - * - * @return boolean - */ - public function isBase64() - { - if (!is_string($this->data)) { - return false; - } - - return base64_encode(base64_decode($this->data)) === $this->data; - } - - /** - * Initiates new Image from Intervention\Image\Image - * - * @param Image $object - * @return \Intervention\Image\Image - */ - public function initFromInterventionImage($object) - { - return $object; - } - - /** - * Parses and decodes binary image data from data-url - * - * @param string $data_url - * @return string - */ - private function decodeDataUrl($data_url) - { - if (!is_string($data_url)) { - return null; - } - - $pattern = "/^data:(?:image\/[a-zA-Z\-\.]+)(?:charset=\".+\")?;base64,(?P.+)$/"; - preg_match($pattern, $data_url, $matches); - - if (is_array($matches) && array_key_exists('data', $matches)) { - return base64_decode($matches['data']); - } - - return null; - } - - /** - * Initiates new image from mixed data - * - * @param mixed $data - * @return \Intervention\Image\Image - */ - public function init($data) - { - $this->data = $data; - - switch (true) { - - case $this->isGdResource(): - return $this->initFromGdResource($this->data); - - case $this->isImagick(): - return $this->initFromImagick($this->data); - - case $this->isInterventionImage(): - return $this->initFromInterventionImage($this->data); - - case $this->isSplFileInfo(): - return $this->initFromPath($this->data->getRealPath()); - - case $this->isBinary(): - return $this->initFromBinary($this->data); - - case $this->isUrl(): - return $this->initFromUrl($this->data); - - case $this->isStream(): - return $this->initFromStream($this->data); - - case $this->isFilePath(): - return $this->initFromPath($this->data); - - case $this->isDataUrl(): - return $this->initFromBinary($this->decodeDataUrl($this->data)); - - case $this->isBase64(): - return $this->initFromBinary(base64_decode($this->data)); - - default: - throw new Exception\NotReadableException("Image source not readable"); - } - } - - /** - * Decoder object transforms to string source data - * - * @return string - */ - public function __toString() - { - return (string) $this->data; - } -} diff --git a/src/Intervention/Image/AbstractDriver.php b/src/Intervention/Image/AbstractDriver.php deleted file mode 100644 index dfa1649b8..000000000 --- a/src/Intervention/Image/AbstractDriver.php +++ /dev/null @@ -1,132 +0,0 @@ -decoder->init($data); - } - - /** - * Encodes given image - * - * @param Image $image - * @param string $format - * @param integer $quality - * @return \Intervention\Image\Image - */ - public function encode($image, $format, $quality) - { - return $this->encoder->process($image, $format, $quality); - } - - /** - * Executes named command on given image - * - * @param Image $image - * @param string $name - * @param array $arguments - * @return \Intervention\Image\Commands\AbstractCommand - */ - public function executeCommand($image, $name, $arguments) - { - $commandName = $this->getCommandClassName($name); - $command = new $commandName($arguments); - $command->execute($image); - - return $command; - } - - /** - * Returns classname of given command name - * - * @param string $name - * @return string - */ - private function getCommandClassName($name) - { - $drivername = $this->getDriverName(); - $classnameLocal = sprintf('\Intervention\Image\%s\Commands\%sCommand', $drivername, ucfirst($name)); - $classnameGlobal = sprintf('\Intervention\Image\Commands\%sCommand', ucfirst($name)); - - if (class_exists($classnameLocal)) { - return $classnameLocal; - } elseif (class_exists($classnameGlobal)) { - return $classnameGlobal; - } - - throw new \Intervention\Image\Exception\NotSupportedException( - "Command ({$name}) is not available for driver ({$drivername})." - ); - } - - /** - * Returns name of current driver instance - * - * @return string - */ - public function getDriverName() - { - $reflect = new \ReflectionClass($this); - $namespace = $reflect->getNamespaceName(); - - return substr(strrchr($namespace, "\\"), 1); - } -} diff --git a/src/Intervention/Image/AbstractEncoder.php b/src/Intervention/Image/AbstractEncoder.php deleted file mode 100644 index 5e0cce2f3..000000000 --- a/src/Intervention/Image/AbstractEncoder.php +++ /dev/null @@ -1,221 +0,0 @@ -setImage($image); - $this->setFormat($format); - $this->setQuality($quality); - - switch (strtolower($this->format)) { - - case 'data-url': - $this->result = $this->processDataUrl(); - break; - - case 'gif': - case 'image/gif': - $this->result = $this->processGif(); - break; - - case 'png': - case 'image/png': - case 'image/x-png': - $this->result = $this->processPng(); - break; - - case 'jpg': - case 'jpeg': - case 'image/jpg': - case 'image/jpeg': - case 'image/pjpeg': - $this->result = $this->processJpeg(); - break; - - case 'tif': - case 'tiff': - case 'image/tiff': - case 'image/tif': - case 'image/x-tif': - case 'image/x-tiff': - $this->result = $this->processTiff(); - break; - - case 'bmp': - case 'image/bmp': - case 'image/ms-bmp': - case 'image/x-bitmap': - case 'image/x-bmp': - case 'image/x-ms-bmp': - case 'image/x-win-bitmap': - case 'image/x-windows-bmp': - case 'image/x-xbitmap': - $this->result = $this->processBmp(); - break; - - case 'ico': - case 'image/x-ico': - case 'image/x-icon': - case 'image/vnd.microsoft.icon': - $this->result = $this->processIco(); - break; - - case 'psd': - case 'image/vnd.adobe.photoshop': - $this->result = $this->processPsd(); - break; - - default: - throw new \Intervention\Image\Exception\NotSupportedException( - "Encoding format ({$format}) is not supported." - ); - } - - $this->setImage(null); - - return $image->setEncoded($this->result); - } - - /** - * Processes and returns encoded image as data-url string - * - * @return string - */ - protected function processDataUrl() - { - $mime = $this->image->mime ? $this->image->mime : 'image/png'; - - return sprintf('data:%s;base64,%s', - $mime, - base64_encode($this->process($this->image, $mime, $this->quality)) - ); - } - - /** - * Sets image to process - * - * @param Image $image - */ - protected function setImage($image) - { - $this->image = $image; - } - - /** - * Determines output format - * - * @param string $format - */ - protected function setFormat($format = null) - { - if ($format == '' && $this->image instanceof Image) { - $format = $this->image->mime; - } - - $this->format = $format ? $format : 'jpg'; - - return $this; - } - - /** - * Determines output quality - * - * @param integer $quality - */ - protected function setQuality($quality) - { - $quality = is_null($quality) ? 90 : $quality; - $quality = $quality === 0 ? 1 : $quality; - - if ($quality < 0 || $quality > 100) { - throw new \Intervention\Image\Exception\InvalidArgumentException( - 'Quality must range from 0 to 100.' - ); - } - - $this->quality = intval($quality); - - return $this; - } -} diff --git a/src/Intervention/Image/AbstractFont.php b/src/Intervention/Image/AbstractFont.php deleted file mode 100644 index 8bcf3b286..000000000 --- a/src/Intervention/Image/AbstractFont.php +++ /dev/null @@ -1,260 +0,0 @@ -text = $text; - } - - /** - * Set text to be written - * - * @param String $text - * @return void - */ - public function text($text) - { - $this->text = $text; - - return $this; - } - - /** - * Get text to be written - * - * @return String - */ - public function getText() - { - return $this->text; - } - - /** - * Set font size in pixels - * - * @param integer $size - * @return void - */ - public function size($size) - { - $this->size = $size; - - return $this; - } - - /** - * Get font size in pixels - * - * @return integer - */ - public function getSize() - { - return $this->size; - } - - /** - * Set color of text to be written - * - * @param mixed $color - * @return void - */ - public function color($color) - { - $this->color = $color; - - return $this; - } - - /** - * Get color of text - * - * @return mixed - */ - public function getColor() - { - return $this->color; - } - - /** - * Set rotation angle of text - * - * @param integer $angle - * @return void - */ - public function angle($angle) - { - $this->angle = $angle; - - return $this; - } - - /** - * Get rotation angle of text - * - * @return integer - */ - public function getAngle() - { - return $this->angle; - } - - /** - * Set horizontal text alignment - * - * @param string $align - * @return void - */ - public function align($align) - { - $this->align = $align; - - return $this; - } - - /** - * Get horizontal text alignment - * - * @return string - */ - public function getAlign() - { - return $this->align; - } - - /** - * Set vertical text alignment - * - * @param string $valign - * @return void - */ - public function valign($valign) - { - $this->valign = $valign; - - return $this; - } - - /** - * Get vertical text alignment - * - * @return string - */ - public function getValign() - { - return $this->valign; - } - - /** - * Set path to font file - * - * @param string $file - * @return void - */ - public function file($file) - { - $this->file = $file; - - return $this; - } - - /** - * Get path to font file - * - * @return string - */ - public function getFile() - { - return $this->file; - } - - /** - * Checks if current font has access to an applicable font file - * - * @return boolean - */ - protected function hasApplicableFontFile() - { - if (is_string($this->file)) { - return file_exists($this->file); - } - - return false; - } - - /** - * Counts lines of text to be written - * - * @return integer - */ - public function countLines() - { - return count(explode(PHP_EOL, $this->text)); - } -} diff --git a/src/Intervention/Image/AbstractShape.php b/src/Intervention/Image/AbstractShape.php deleted file mode 100644 index 0ffc59ea2..000000000 --- a/src/Intervention/Image/AbstractShape.php +++ /dev/null @@ -1,71 +0,0 @@ -background = $color; - } - - /** - * Set border width and color of current shape - * - * @param integer $width - * @param string $color - * @return void - */ - public function border($width, $color = null) - { - $this->border_width = is_numeric($width) ? intval($width) : 0; - $this->border_color = is_null($color) ? '#000000' : $color; - } - - /** - * Determines if current shape has border - * - * @return boolean - */ - public function hasBorder() - { - return ($this->border_width >= 1); - } -} diff --git a/src/Intervention/Image/Commands/AbstractCommand.php b/src/Intervention/Image/Commands/AbstractCommand.php deleted file mode 100644 index daa79bcd4..000000000 --- a/src/Intervention/Image/Commands/AbstractCommand.php +++ /dev/null @@ -1,79 +0,0 @@ -arguments = $arguments; - } - - /** - * Creates new argument instance from given argument key - * - * @param integer $key - * @return \Intervention\Image\Commands\Argument - */ - public function argument($key) - { - return new \Intervention\Image\Commands\Argument($this, $key); - } - - /** - * Returns output data of current command - * - * @return mixed - */ - public function getOutput() - { - return $this->output ? $this->output : null; - } - - /** - * Determines if current instance has output data - * - * @return boolean - */ - public function hasOutput() - { - return ! is_null($this->output); - } - - /** - * Sets output data of current command - * - * @param mixed $value - */ - public function setOutput($value) - { - $this->output = $value; - } -} diff --git a/src/Intervention/Image/Commands/Argument.php b/src/Intervention/Image/Commands/Argument.php deleted file mode 100644 index ee33dcefb..000000000 --- a/src/Intervention/Image/Commands/Argument.php +++ /dev/null @@ -1,225 +0,0 @@ -command = $command; - $this->key = $key; - } - - /** - * Returns name of current arguments command - * - * @return string - */ - public function getCommandName() - { - preg_match("/\\\\([\w]+)Command$/", get_class($this->command), $matches); - return isset($matches[1]) ? lcfirst($matches[1]).'()' : 'Method'; - } - - /** - * Returns value of current argument - * - * @param mixed $default - * @return mixed - */ - public function value($default = null) - { - $arguments = $this->command->arguments; - - if (is_array($arguments)) { - return isset($arguments[$this->key]) ? $arguments[$this->key] : $default; - } - - return $default; - } - - /** - * Defines current argument as required - * - * @return \Intervention\Image\Commands\Argument - */ - public function required() - { - if ( ! array_key_exists($this->key, $this->command->arguments)) { - throw new \Intervention\Image\Exception\InvalidArgumentException( - sprintf("Missing argument %d for %s", $this->key + 1, $this->getCommandName()) - ); - } - - return $this; - } - - /** - * Determines that current argument must be of given type - * - * @return \Intervention\Image\Commands\Argument - */ - public function type($type) - { - $fail = false; - - $value = $this->value(); - - if (is_null($value)) { - return $this; - } - - switch (strtolower($type)) { - - case 'bool': - case 'boolean': - $fail = ! is_bool($value); - $message = sprintf('%s accepts only boolean values as argument %d.', $this->getCommandName(), $this->key + 1); - break; - - case 'int': - case 'integer': - $fail = ! is_integer($value); - $message = sprintf('%s accepts only integer values as argument %d.', $this->getCommandName(), $this->key + 1); - break; - - case 'num': - case 'numeric': - $fail = ! is_numeric($value); - $message = sprintf('%s accepts only numeric values as argument %d.', $this->getCommandName(), $this->key + 1); - break; - - case 'str': - case 'string': - $fail = ! is_string($value); - $message = sprintf('%s accepts only string values as argument %d.', $this->getCommandName(), $this->key + 1); - break; - - case 'array': - $fail = ! is_array($value); - $message = sprintf('%s accepts only array as argument %d.', $this->getCommandName(), $this->key + 1); - break; - - case 'closure': - $fail = ! is_a($value, '\Closure'); - $message = sprintf('%s accepts only Closure as argument %d.', $this->getCommandName(), $this->key + 1); - break; - - case 'digit': - $fail = ! $this->isDigit($value); - $message = sprintf('%s accepts only integer values as argument %d.', $this->getCommandName(), $this->key + 1); - break; - } - - if ($fail) { - - $message = isset($message) ? $message : sprintf("Missing argument for %d.", $this->key); - - throw new \Intervention\Image\Exception\InvalidArgumentException( - $message - ); - } - - return $this; - } - - /** - * Determines that current argument value must be numeric between given values - * - * @return \Intervention\Image\Commands\Argument - */ - public function between($x, $y) - { - $value = $this->type('numeric')->value(); - - if (is_null($value)) { - return $this; - } - - $alpha = min($x, $y); - $omega = max($x, $y); - - if ($value < $alpha || $value > $omega) { - throw new \Intervention\Image\Exception\InvalidArgumentException( - sprintf('Argument %d must be between %s and %s.', $this->key, $x, $y) - ); - } - - return $this; - } - - /** - * Determines that current argument must be over a minimum value - * - * @return \Intervention\Image\Commands\Argument - */ - public function min($value) - { - $v = $this->type('numeric')->value(); - - if (is_null($v)) { - return $this; - } - - if ($v < $value) { - throw new \Intervention\Image\Exception\InvalidArgumentException( - sprintf('Argument %d must be at least %s.', $this->key, $value) - ); - } - - return $this; - } - - /** - * Determines that current argument must be under a maxiumum value - * - * @return \Intervention\Image\Commands\Argument - */ - public function max($value) - { - $v = $this->type('numeric')->value(); - - if (is_null($v)) { - return $this; - } - - if ($v > $value) { - throw new \Intervention\Image\Exception\InvalidArgumentException( - sprintf('Argument %d may not be greater than %s.', $this->key, $value) - ); - } - - return $this; - } - - /** - * Checks if value is "PHP" integer (120 but also 120.0) - * - * @param mixed $value - * @return boolean - */ - private function isDigit($value) - { - return is_numeric($value) ? intval($value) == $value : false; - } -} diff --git a/src/Intervention/Image/Commands/ChecksumCommand.php b/src/Intervention/Image/Commands/ChecksumCommand.php deleted file mode 100644 index f8944bf18..000000000 --- a/src/Intervention/Image/Commands/ChecksumCommand.php +++ /dev/null @@ -1,29 +0,0 @@ -getSize(); - - for ($x=0; $x <= ($size->width-1); $x++) { - for ($y=0; $y <= ($size->height-1); $y++) { - $colors[] = $image->pickColor($x, $y, 'array'); - } - } - - $this->setOutput(md5(serialize($colors))); - - return true; - } -} diff --git a/src/Intervention/Image/Commands/CircleCommand.php b/src/Intervention/Image/Commands/CircleCommand.php deleted file mode 100644 index 2fc38ddf8..000000000 --- a/src/Intervention/Image/Commands/CircleCommand.php +++ /dev/null @@ -1,35 +0,0 @@ -argument(0)->type('numeric')->required()->value(); - $x = $this->argument(1)->type('numeric')->required()->value(); - $y = $this->argument(2)->type('numeric')->required()->value(); - $callback = $this->argument(3)->type('closure')->value(); - - $circle_classname = sprintf('\Intervention\Image\%s\Shapes\CircleShape', - $image->getDriver()->getDriverName()); - - $circle = new $circle_classname($diameter); - - if ($callback instanceof Closure) { - $callback($circle); - } - - $circle->applyToImage($image, $x, $y); - - return true; - } -} diff --git a/src/Intervention/Image/Commands/EllipseCommand.php b/src/Intervention/Image/Commands/EllipseCommand.php deleted file mode 100644 index 4f364eca1..000000000 --- a/src/Intervention/Image/Commands/EllipseCommand.php +++ /dev/null @@ -1,36 +0,0 @@ -argument(0)->type('numeric')->required()->value(); - $height = $this->argument(1)->type('numeric')->required()->value(); - $x = $this->argument(2)->type('numeric')->required()->value(); - $y = $this->argument(3)->type('numeric')->required()->value(); - $callback = $this->argument(4)->type('closure')->value(); - - $ellipse_classname = sprintf('\Intervention\Image\%s\Shapes\EllipseShape', - $image->getDriver()->getDriverName()); - - $ellipse = new $ellipse_classname($width, $height); - - if ($callback instanceof Closure) { - $callback($ellipse); - } - - $ellipse->applyToImage($image, $x, $y); - - return true; - } -} diff --git a/src/Intervention/Image/Commands/ExifCommand.php b/src/Intervention/Image/Commands/ExifCommand.php deleted file mode 100644 index 2986cae8c..000000000 --- a/src/Intervention/Image/Commands/ExifCommand.php +++ /dev/null @@ -1,37 +0,0 @@ -argument(0)->value(); - - // try to read exif data from image file - $data = @exif_read_data($image->dirname .'/'. $image->basename); - - if (! is_null($key) && is_array($data)) { - $data = array_key_exists($key, $data) ? $data[$key] : false; - } - - $this->setOutput($data); - - return true; - } -} diff --git a/src/Intervention/Image/Commands/IptcCommand.php b/src/Intervention/Image/Commands/IptcCommand.php deleted file mode 100644 index 43239ed3c..000000000 --- a/src/Intervention/Image/Commands/IptcCommand.php +++ /dev/null @@ -1,64 +0,0 @@ -argument(0)->value(); - - $info = array(); - @getimagesize($image->dirname .'/'. $image->basename, $info); - - $data = array(); - - if (array_key_exists('APP13', $info)) { - $iptc = iptcparse($info['APP13']); - - if (is_array($iptc)) { - $data['DocumentTitle'] = isset($iptc["2#005"][0]) ? $iptc["2#005"][0] : null; - $data['Urgency'] = isset($iptc["2#010"][0]) ? $iptc["2#010"][0] : null; - $data['Category'] = isset($iptc["2#015"][0]) ? $iptc["2#015"][0] : null; - $data['Subcategories'] = isset($iptc["2#020"][0]) ? $iptc["2#020"][0] : null; - $data['Keywords'] = isset($iptc["2#025"][0]) ? $iptc["2#025"] : null; - $data['SpecialInstructions'] = isset($iptc["2#040"][0]) ? $iptc["2#040"][0] : null; - $data['CreationDate'] = isset($iptc["2#055"][0]) ? $iptc["2#055"][0] : null; - $data['CreationTime'] = isset($iptc["2#060"][0]) ? $iptc["2#060"][0] : null; - $data['AuthorByline'] = isset($iptc["2#080"][0]) ? $iptc["2#080"][0] : null; - $data['AuthorTitle'] = isset($iptc["2#085"][0]) ? $iptc["2#085"][0] : null; - $data['City'] = isset($iptc["2#090"][0]) ? $iptc["2#090"][0] : null; - $data['SubLocation'] = isset($iptc["2#092"][0]) ? $iptc["2#092"][0] : null; - $data['State'] = isset($iptc["2#095"][0]) ? $iptc["2#095"][0] : null; - $data['Country'] = isset($iptc["2#101"][0]) ? $iptc["2#101"][0] : null; - $data['OTR'] = isset($iptc["2#103"][0]) ? $iptc["2#103"][0] : null; - $data['Headline'] = isset($iptc["2#105"][0]) ? $iptc["2#105"][0] : null; - $data['Source'] = isset($iptc["2#110"][0]) ? $iptc["2#110"][0] : null; - $data['PhotoSource'] = isset($iptc["2#115"][0]) ? $iptc["2#115"][0] : null; - $data['Copyright'] = isset($iptc["2#116"][0]) ? $iptc["2#116"][0] : null; - $data['Caption'] = isset($iptc["2#120"][0]) ? $iptc["2#120"][0] : null; - $data['CaptionWriter'] = isset($iptc["2#122"][0]) ? $iptc["2#122"][0] : null; - } - } - - if (! is_null($key) && is_array($data)) { - $data = array_key_exists($key, $data) ? $data[$key] : false; - } - - $this->setOutput($data); - - return true; - } -} diff --git a/src/Intervention/Image/Commands/LineCommand.php b/src/Intervention/Image/Commands/LineCommand.php deleted file mode 100644 index 0089c649a..000000000 --- a/src/Intervention/Image/Commands/LineCommand.php +++ /dev/null @@ -1,36 +0,0 @@ -argument(0)->type('numeric')->required()->value(); - $y1 = $this->argument(1)->type('numeric')->required()->value(); - $x2 = $this->argument(2)->type('numeric')->required()->value(); - $y2 = $this->argument(3)->type('numeric')->required()->value(); - $callback = $this->argument(4)->type('closure')->value(); - - $line_classname = sprintf('\Intervention\Image\%s\Shapes\LineShape', - $image->getDriver()->getDriverName()); - - $line = new $line_classname($x2, $y2); - - if ($callback instanceof Closure) { - $callback($line); - } - - $line->applyToImage($image, $x1, $y1); - - return true; - } -} diff --git a/src/Intervention/Image/Commands/OrientateCommand.php b/src/Intervention/Image/Commands/OrientateCommand.php deleted file mode 100644 index 552482cb4..000000000 --- a/src/Intervention/Image/Commands/OrientateCommand.php +++ /dev/null @@ -1,48 +0,0 @@ -exif('Orientation')) { - - case 2: - $image->flip(); - break; - - case 3: - $image->rotate(180); - break; - - case 4: - $image->rotate(180)->flip(); - break; - - case 5: - $image->rotate(270)->flip(); - break; - - case 6: - $image->rotate(270); - break; - - case 7: - $image->rotate(90)->flip(); - break; - - case 8: - $image->rotate(90); - break; - } - - return true; - } -} diff --git a/src/Intervention/Image/Commands/PolygonCommand.php b/src/Intervention/Image/Commands/PolygonCommand.php deleted file mode 100644 index e46e3fffb..000000000 --- a/src/Intervention/Image/Commands/PolygonCommand.php +++ /dev/null @@ -1,48 +0,0 @@ -argument(0)->type('array')->required()->value(); - $callback = $this->argument(1)->type('closure')->value(); - - $vertices_count = count($points); - - // check if number if coordinates is even - if ($vertices_count % 2 !== 0) { - throw new \Intervention\Image\Exception\InvalidArgumentException( - "The number of given polygon vertices must be even." - ); - } - - if ($vertices_count < 6) { - throw new \Intervention\Image\Exception\InvalidArgumentException( - "You must have at least 3 points in your array." - ); - } - - $polygon_classname = sprintf('\Intervention\Image\%s\Shapes\PolygonShape', - $image->getDriver()->getDriverName()); - - $polygon = new $polygon_classname($points); - - if ($callback instanceof Closure) { - $callback($polygon); - } - - $polygon->applyToImage($image); - - return true; - } -} diff --git a/src/Intervention/Image/Commands/PsrResponseCommand.php b/src/Intervention/Image/Commands/PsrResponseCommand.php deleted file mode 100644 index ab47be10c..000000000 --- a/src/Intervention/Image/Commands/PsrResponseCommand.php +++ /dev/null @@ -1,45 +0,0 @@ -argument(0)->value(); - $quality = $this->argument(1)->between(0, 100)->value(); - - //Encoded property will be populated at this moment - $stream = $image->stream($format, $quality); - - $mimetype = finfo_buffer( - finfo_open(FILEINFO_MIME_TYPE), - $image->getEncoded() - ); - - $this->setOutput(new Response( - 200, - array( - 'Content-Type' => $mimetype, - 'Content-Length' => strlen($image->getEncoded()) - ), - $stream - )); - - return true; - } -} \ No newline at end of file diff --git a/src/Intervention/Image/Commands/RectangleCommand.php b/src/Intervention/Image/Commands/RectangleCommand.php deleted file mode 100644 index 3a2074c57..000000000 --- a/src/Intervention/Image/Commands/RectangleCommand.php +++ /dev/null @@ -1,36 +0,0 @@ -argument(0)->type('numeric')->required()->value(); - $y1 = $this->argument(1)->type('numeric')->required()->value(); - $x2 = $this->argument(2)->type('numeric')->required()->value(); - $y2 = $this->argument(3)->type('numeric')->required()->value(); - $callback = $this->argument(4)->type('closure')->value(); - - $rectangle_classname = sprintf('\Intervention\Image\%s\Shapes\RectangleShape', - $image->getDriver()->getDriverName()); - - $rectangle = new $rectangle_classname($x1, $y1, $x2, $y2); - - if ($callback instanceof Closure) { - $callback($rectangle); - } - - $rectangle->applyToImage($image, $x1, $y1); - - return true; - } -} diff --git a/src/Intervention/Image/Commands/ResponseCommand.php b/src/Intervention/Image/Commands/ResponseCommand.php deleted file mode 100644 index 7903b5af4..000000000 --- a/src/Intervention/Image/Commands/ResponseCommand.php +++ /dev/null @@ -1,26 +0,0 @@ -argument(0)->value(); - $quality = $this->argument(1)->between(0, 100)->value(); - - $response = new Response($image, $format, $quality); - - $this->setOutput($response->make()); - - return true; - } -} diff --git a/src/Intervention/Image/Commands/StreamCommand.php b/src/Intervention/Image/Commands/StreamCommand.php deleted file mode 100644 index 111c47569..000000000 --- a/src/Intervention/Image/Commands/StreamCommand.php +++ /dev/null @@ -1,25 +0,0 @@ -argument(0)->value(); - $quality = $this->argument(1)->between(0, 100)->value(); - - $this->setOutput(\GuzzleHttp\Psr7\stream_for( - $image->encode($format, $quality)->getEncoded() - )); - - return true; - } -} \ No newline at end of file diff --git a/src/Intervention/Image/Commands/TextCommand.php b/src/Intervention/Image/Commands/TextCommand.php deleted file mode 100644 index 4aebd8e89..000000000 --- a/src/Intervention/Image/Commands/TextCommand.php +++ /dev/null @@ -1,34 +0,0 @@ -argument(0)->required()->value(); - $x = $this->argument(1)->type('numeric')->value(0); - $y = $this->argument(2)->type('numeric')->value(0); - $callback = $this->argument(3)->type('closure')->value(); - - $fontclassname = sprintf('\Intervention\Image\%s\Font', - $image->getDriver()->getDriverName()); - - $font = new $fontclassname($text); - - if ($callback instanceof Closure) { - $callback($font); - } - - $font->applyToImage($image, $x, $y); - - return true; - } -} diff --git a/src/Intervention/Image/Constraint.php b/src/Intervention/Image/Constraint.php deleted file mode 100644 index a247c99c2..000000000 --- a/src/Intervention/Image/Constraint.php +++ /dev/null @@ -1,91 +0,0 @@ -size = $size; - } - - /** - * Returns current size of constraint - * - * @return \Intervention\Image\Size - */ - public function getSize() - { - return $this->size; - } - - /** - * Fix the given argument in current constraint - * @param integer $type - * @return void - */ - public function fix($type) - { - $this->fixed = ($this->fixed & ~(1 << $type)) | (1 << $type); - } - - /** - * Checks if given argument is fixed in current constraint - * - * @param integer $type - * @return boolean - */ - public function isFixed($type) - { - return (bool) ($this->fixed & (1 << $type)); - } - - /** - * Fixes aspect ratio in current constraint - * - * @return void - */ - public function aspectRatio() - { - $this->fix(self::ASPECTRATIO); - } - - /** - * Fixes possibility to size up in current constraint - * - * @return void - */ - public function upsize() - { - $this->fix(self::UPSIZE); - } -} diff --git a/src/Intervention/Image/Exception/ImageException.php b/src/Intervention/Image/Exception/ImageException.php deleted file mode 100644 index 83e6b91f2..000000000 --- a/src/Intervention/Image/Exception/ImageException.php +++ /dev/null @@ -1,8 +0,0 @@ -dirname = array_key_exists('dirname', $info) ? $info['dirname'] : null; - $this->basename = array_key_exists('basename', $info) ? $info['basename'] : null; - $this->extension = array_key_exists('extension', $info) ? $info['extension'] : null; - $this->filename = array_key_exists('filename', $info) ? $info['filename'] : null; - - if (file_exists($path) && is_file($path)) { - $this->mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $path); - } - - return $this; - } - - /** - * Get file size - * - * @return mixed - */ - public function filesize() - { - $path = $this->basePath(); - - if (file_exists($path) && is_file($path)) { - return filesize($path); - } - - return false; - } - - /** - * Get fully qualified path - * - * @return string - */ - public function basePath() - { - if ($this->dirname && $this->basename) { - return ($this->dirname .'/'. $this->basename); - } - - return null; - } - -} diff --git a/src/Intervention/Image/Filters/DemoFilter.php b/src/Intervention/Image/Filters/DemoFilter.php deleted file mode 100644 index 17e926e24..000000000 --- a/src/Intervention/Image/Filters/DemoFilter.php +++ /dev/null @@ -1,42 +0,0 @@ -size = is_numeric($size) ? intval($size) : self::DEFAULT_SIZE; - } - - /** - * Applies filter effects to given image - * - * @param \Intervention\Image\Image $image - * @return \Intervention\Image\Image - */ - public function applyFilter(\Intervention\Image\Image $image) - { - $image->pixelate($this->size); - $image->greyscale(); - - return $image; - } -} diff --git a/src/Intervention/Image/Filters/FilterInterface.php b/src/Intervention/Image/Filters/FilterInterface.php deleted file mode 100644 index 27c0beef4..000000000 --- a/src/Intervention/Image/Filters/FilterInterface.php +++ /dev/null @@ -1,14 +0,0 @@ -a = ($value >> 24) & 0xFF; - $this->r = ($value >> 16) & 0xFF; - $this->g = ($value >> 8) & 0xFF; - $this->b = $value & 0xFF; - } - - /** - * Initiates color object from given array - * - * @param array $value - * @return \Intervention\Image\AbstractColor - */ - public function initFromArray($array) - { - $array = array_values($array); - - if (count($array) == 4) { - - // color array with alpha value - list($r, $g, $b, $a) = $array; - $this->a = $this->alpha2gd($a); - - } elseif (count($array) == 3) { - - // color array without alpha value - list($r, $g, $b) = $array; - $this->a = 0; - - } - - $this->r = $r; - $this->g = $g; - $this->b = $b; - } - - /** - * Initiates color object from given string - * - * @param string $value - * @return \Intervention\Image\AbstractColor - */ - public function initFromString($value) - { - if ($color = $this->rgbaFromString($value)) { - $this->r = $color[0]; - $this->g = $color[1]; - $this->b = $color[2]; - $this->a = $this->alpha2gd($color[3]); - } - } - - /** - * Initiates color object from given R, G and B values - * - * @param integer $r - * @param integer $g - * @param integer $b - * @return \Intervention\Image\AbstractColor - */ - public function initFromRgb($r, $g, $b) - { - $this->r = intval($r); - $this->g = intval($g); - $this->b = intval($b); - $this->a = 0; - } - - /** - * Initiates color object from given R, G, B and A values - * - * @param integer $r - * @param integer $g - * @param integer $b - * @param float $a - * @return \Intervention\Image\AbstractColor - */ - public function initFromRgba($r, $g, $b, $a = 1) - { - $this->r = intval($r); - $this->g = intval($g); - $this->b = intval($b); - $this->a = $this->alpha2gd($a); - } - - /** - * Initiates color object from given ImagickPixel object - * - * @param ImagickPixel $value - * @return \Intervention\Image\AbstractColor - */ - public function initFromObject($value) - { - throw new \Intervention\Image\Exception\NotSupportedException( - "GD colors cannot init from ImagickPixel objects." - ); - } - - /** - * Calculates integer value of current color instance - * - * @return integer - */ - public function getInt() - { - return ($this->a << 24) + ($this->r << 16) + ($this->g << 8) + $this->b; - } - - /** - * Calculates hexadecimal value of current color instance - * - * @param string $prefix - * @return string - */ - public function getHex($prefix = '') - { - return sprintf('%s%02x%02x%02x', $prefix, $this->r, $this->g, $this->b); - } - - /** - * Calculates RGB(A) in array format of current color instance - * - * @return array - */ - public function getArray() - { - return array($this->r, $this->g, $this->b, round(1 - $this->a / 127, 2)); - } - - /** - * Calculates RGBA in string format of current color instance - * - * @return string - */ - public function getRgba() - { - return sprintf('rgba(%d, %d, %d, %.2F)', $this->r, $this->g, $this->b, round(1 - $this->a / 127, 2)); - } - - /** - * Determines if current color is different from given color - * - * @param AbstractColor $color - * @param integer $tolerance - * @return boolean - */ - public function differs(AbstractColor $color, $tolerance = 0) - { - $color_tolerance = round($tolerance * 2.55); - $alpha_tolerance = round($tolerance * 1.27); - - $delta = array( - 'r' => abs($color->r - $this->r), - 'g' => abs($color->g - $this->g), - 'b' => abs($color->b - $this->b), - 'a' => abs($color->a - $this->a) - ); - - return ( - $delta['r'] > $color_tolerance or - $delta['g'] > $color_tolerance or - $delta['b'] > $color_tolerance or - $delta['a'] > $alpha_tolerance - ); - } - - /** - * Convert rgba alpha (0-1) value to gd value (0-127) - * - * @param float $input - * @return int - */ - private function alpha2gd($input) - { - $oldMin = 0; - $oldMax = 1; - - $newMin = 127; - $newMax = 0; - - return ceil(((($input- $oldMin) * ($newMax - $newMin)) / ($oldMax - $oldMin)) + $newMin); - } -} diff --git a/src/Intervention/Image/Gd/Commands/BackupCommand.php b/src/Intervention/Image/Gd/Commands/BackupCommand.php deleted file mode 100644 index 347daea6b..000000000 --- a/src/Intervention/Image/Gd/Commands/BackupCommand.php +++ /dev/null @@ -1,32 +0,0 @@ -argument(0)->value(); - - // clone current image resource - $size = $image->getSize(); - $clone = imagecreatetruecolor($size->width, $size->height); - imagealphablending($clone, false); - imagesavealpha($clone, true); - $transparency = imagecolorallocatealpha($clone, 0, 0, 0, 127); - imagefill($clone, 0, 0, $transparency); - - // copy image to clone - imagecopy($clone, $image->getCore(), 0, 0, 0, 0, $size->width, $size->height); - - $image->setBackup($clone, $backupName); - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/BlurCommand.php b/src/Intervention/Image/Gd/Commands/BlurCommand.php deleted file mode 100644 index d53f59d7c..000000000 --- a/src/Intervention/Image/Gd/Commands/BlurCommand.php +++ /dev/null @@ -1,23 +0,0 @@ -argument(0)->between(0, 100)->value(1); - - for ($i=0; $i < intval($amount); $i++) { - imagefilter($image->getCore(), IMG_FILTER_GAUSSIAN_BLUR); - } - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/BrightnessCommand.php b/src/Intervention/Image/Gd/Commands/BrightnessCommand.php deleted file mode 100644 index de4263f74..000000000 --- a/src/Intervention/Image/Gd/Commands/BrightnessCommand.php +++ /dev/null @@ -1,19 +0,0 @@ -argument(0)->between(-100, 100)->required()->value(); - - return imagefilter($image->getCore(), IMG_FILTER_BRIGHTNESS, ($level * 2.55)); - } -} diff --git a/src/Intervention/Image/Gd/Commands/ColorizeCommand.php b/src/Intervention/Image/Gd/Commands/ColorizeCommand.php deleted file mode 100644 index 8f539638b..000000000 --- a/src/Intervention/Image/Gd/Commands/ColorizeCommand.php +++ /dev/null @@ -1,27 +0,0 @@ -argument(0)->between(-100, 100)->required()->value(); - $green = $this->argument(1)->between(-100, 100)->required()->value(); - $blue = $this->argument(2)->between(-100, 100)->required()->value(); - - // normalize colorize levels - $red = round($red * 2.55); - $green = round($green * 2.55); - $blue = round($blue * 2.55); - - // apply filter - return imagefilter($image->getCore(), IMG_FILTER_COLORIZE, $red, $green, $blue); - } -} diff --git a/src/Intervention/Image/Gd/Commands/ContrastCommand.php b/src/Intervention/Image/Gd/Commands/ContrastCommand.php deleted file mode 100644 index e43b761af..000000000 --- a/src/Intervention/Image/Gd/Commands/ContrastCommand.php +++ /dev/null @@ -1,19 +0,0 @@ -argument(0)->between(-100, 100)->required()->value(); - - return imagefilter($image->getCore(), IMG_FILTER_CONTRAST, ($level * -1)); - } -} diff --git a/src/Intervention/Image/Gd/Commands/CropCommand.php b/src/Intervention/Image/Gd/Commands/CropCommand.php deleted file mode 100644 index b7f595421..000000000 --- a/src/Intervention/Image/Gd/Commands/CropCommand.php +++ /dev/null @@ -1,40 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $height = $this->argument(1)->type('digit')->required()->value(); - $x = $this->argument(2)->type('digit')->value(); - $y = $this->argument(3)->type('digit')->value(); - - if (is_null($width) || is_null($height)) { - throw new \Intervention\Image\Exception\InvalidArgumentException( - "Width and height of cutout needs to be defined." - ); - } - - $cropped = new Size($width, $height); - $position = new Point($x, $y); - - // align boxes - if (is_null($x) && is_null($y)) { - $position = $image->getSize()->align('center')->relativePosition($cropped->align('center')); - } - - // crop image core - return $this->modify($image, 0, 0, $position->x, $position->y, $cropped->width, $cropped->height, $cropped->width, $cropped->height); - } -} diff --git a/src/Intervention/Image/Gd/Commands/DestroyCommand.php b/src/Intervention/Image/Gd/Commands/DestroyCommand.php deleted file mode 100644 index 403e8b801..000000000 --- a/src/Intervention/Image/Gd/Commands/DestroyCommand.php +++ /dev/null @@ -1,25 +0,0 @@ -getCore()); - - // destroy backups - foreach ($image->getBackups() as $backup) { - imagedestroy($backup); - } - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/FillCommand.php b/src/Intervention/Image/Gd/Commands/FillCommand.php deleted file mode 100644 index aaecb7fb9..000000000 --- a/src/Intervention/Image/Gd/Commands/FillCommand.php +++ /dev/null @@ -1,68 +0,0 @@ -argument(0)->value(); - $x = $this->argument(1)->type('digit')->value(); - $y = $this->argument(2)->type('digit')->value(); - - $width = $image->getWidth(); - $height = $image->getHeight(); - $resource = $image->getCore(); - - try { - - // set image tile filling - $source = new Decoder; - $tile = $source->init($filling); - imagesettile($image->getCore(), $tile->getCore()); - $filling = IMG_COLOR_TILED; - - } catch (\Intervention\Image\Exception\NotReadableException $e) { - - // set solid color filling - $color = new Color($filling); - $filling = $color->getInt(); - } - - imagealphablending($resource, true); - - if (is_int($x) && is_int($y)) { - - // resource should be visible through transparency - $base = $image->getDriver()->newImage($width, $height)->getCore(); - imagecopy($base, $resource, 0, 0, 0, 0, $width, $height); - - // floodfill if exact position is defined - imagefill($resource, $x, $y, $filling); - - // copy filled original over base - imagecopy($base, $resource, 0, 0, 0, 0, $width, $height); - - // set base as new resource-core - $image->setCore($base); - imagedestroy($resource); - - } else { - // fill whole image otherwise - imagefilledrectangle($resource, 0, 0, $width - 1, $height - 1, $filling); - } - - isset($tile) ? imagedestroy($tile->getCore()) : null; - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/FitCommand.php b/src/Intervention/Image/Gd/Commands/FitCommand.php deleted file mode 100644 index d861ad94c..000000000 --- a/src/Intervention/Image/Gd/Commands/FitCommand.php +++ /dev/null @@ -1,32 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $height = $this->argument(1)->type('digit')->value($width); - $constraints = $this->argument(2)->type('closure')->value(); - $position = $this->argument(3)->type('string')->value('center'); - - // calculate size - $cropped = $image->getSize()->fit(new Size($width, $height), $position); - $resized = clone $cropped; - $resized = $resized->resize($width, $height, $constraints); - - // modify image - $this->modify($image, 0, 0, $cropped->pivot->x, $cropped->pivot->y, $resized->getWidth(), $resized->getHeight(), $cropped->getWidth(), $cropped->getHeight()); - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/FlipCommand.php b/src/Intervention/Image/Gd/Commands/FlipCommand.php deleted file mode 100644 index aa8f230e8..000000000 --- a/src/Intervention/Image/Gd/Commands/FlipCommand.php +++ /dev/null @@ -1,37 +0,0 @@ -argument(0)->value('h'); - - $size = $image->getSize(); - $dst = clone $size; - - switch (strtolower($mode)) { - case 2: - case 'v': - case 'vert': - case 'vertical': - $size->pivot->y = $size->height - 1; - $size->height = $size->height * (-1); - break; - - default: - $size->pivot->x = $size->width - 1; - $size->width = $size->width * (-1); - break; - } - - return $this->modify($image, 0, 0, $size->pivot->x, $size->pivot->y, $dst->width, $dst->height, $size->width, $size->height); - } -} diff --git a/src/Intervention/Image/Gd/Commands/GammaCommand.php b/src/Intervention/Image/Gd/Commands/GammaCommand.php deleted file mode 100644 index 366f11808..000000000 --- a/src/Intervention/Image/Gd/Commands/GammaCommand.php +++ /dev/null @@ -1,19 +0,0 @@ -argument(0)->type('numeric')->required()->value(); - - return imagegammacorrect($image->getCore(), 1, $gamma); - } -} diff --git a/src/Intervention/Image/Gd/Commands/GetSizeCommand.php b/src/Intervention/Image/Gd/Commands/GetSizeCommand.php deleted file mode 100644 index 89ee2848f..000000000 --- a/src/Intervention/Image/Gd/Commands/GetSizeCommand.php +++ /dev/null @@ -1,24 +0,0 @@ -setOutput(new Size( - imagesx($image->getCore()), - imagesy($image->getCore()) - )); - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/GreyscaleCommand.php b/src/Intervention/Image/Gd/Commands/GreyscaleCommand.php deleted file mode 100644 index ded8e0d8f..000000000 --- a/src/Intervention/Image/Gd/Commands/GreyscaleCommand.php +++ /dev/null @@ -1,17 +0,0 @@ -getCore(), IMG_FILTER_GRAYSCALE); - } -} diff --git a/src/Intervention/Image/Gd/Commands/HeightenCommand.php b/src/Intervention/Image/Gd/Commands/HeightenCommand.php deleted file mode 100644 index 51e0abdf5..000000000 --- a/src/Intervention/Image/Gd/Commands/HeightenCommand.php +++ /dev/null @@ -1,28 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $additionalConstraints = $this->argument(1)->type('closure')->value(); - - $this->arguments[0] = null; - $this->arguments[1] = $height; - $this->arguments[2] = function ($constraint) use ($additionalConstraints) { - $constraint->aspectRatio(); - if(is_callable($additionalConstraints)) - $additionalConstraints($constraint); - }; - - return parent::execute($image); - } -} diff --git a/src/Intervention/Image/Gd/Commands/InsertCommand.php b/src/Intervention/Image/Gd/Commands/InsertCommand.php deleted file mode 100644 index eba75f012..000000000 --- a/src/Intervention/Image/Gd/Commands/InsertCommand.php +++ /dev/null @@ -1,32 +0,0 @@ -argument(0)->required()->value(); - $position = $this->argument(1)->type('string')->value(); - $x = $this->argument(2)->type('digit')->value(0); - $y = $this->argument(3)->type('digit')->value(0); - - // build watermark - $watermark = $image->getDriver()->init($source); - - // define insertion point - $image_size = $image->getSize()->align($position, $x, $y); - $watermark_size = $watermark->getSize()->align($position); - $target = $image_size->relativePosition($watermark_size); - - // insert image at position - imagealphablending($image->getCore(), true); - return imagecopy($image->getCore(), $watermark->getCore(), $target->x, $target->y, 0, 0, $watermark_size->width, $watermark_size->height); - } -} diff --git a/src/Intervention/Image/Gd/Commands/InterlaceCommand.php b/src/Intervention/Image/Gd/Commands/InterlaceCommand.php deleted file mode 100644 index e8f4b184c..000000000 --- a/src/Intervention/Image/Gd/Commands/InterlaceCommand.php +++ /dev/null @@ -1,21 +0,0 @@ -argument(0)->type('bool')->value(true); - - imageinterlace($image->getCore(), $mode); - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/InvertCommand.php b/src/Intervention/Image/Gd/Commands/InvertCommand.php deleted file mode 100644 index f72e7e305..000000000 --- a/src/Intervention/Image/Gd/Commands/InvertCommand.php +++ /dev/null @@ -1,17 +0,0 @@ -getCore(), IMG_FILTER_NEGATE); - } -} diff --git a/src/Intervention/Image/Gd/Commands/LimitColorsCommand.php b/src/Intervention/Image/Gd/Commands/LimitColorsCommand.php deleted file mode 100644 index 27955e79a..000000000 --- a/src/Intervention/Image/Gd/Commands/LimitColorsCommand.php +++ /dev/null @@ -1,51 +0,0 @@ -argument(0)->value(); - $matte = $this->argument(1)->value(); - - // get current image size - $size = $image->getSize(); - - // create empty canvas - $resource = imagecreatetruecolor($size->width, $size->height); - - // define matte - if (is_null($matte)) { - $matte = imagecolorallocatealpha($resource, 255, 255, 255, 127); - } else { - $matte = $image->getDriver()->parseColor($matte)->getInt(); - } - - // fill with matte and copy original image - imagefill($resource, 0, 0, $matte); - - // set transparency - imagecolortransparent($resource, $matte); - - // copy original image - imagecopy($resource, $image->getCore(), 0, 0, 0, 0, $size->width, $size->height); - - if (is_numeric($count) && $count <= 256) { - // decrease colors - imagetruecolortopalette($resource, true, $count); - } - - // set new resource - $image->setCore($resource); - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/MaskCommand.php b/src/Intervention/Image/Gd/Commands/MaskCommand.php deleted file mode 100644 index 944f0449c..000000000 --- a/src/Intervention/Image/Gd/Commands/MaskCommand.php +++ /dev/null @@ -1,81 +0,0 @@ -argument(0)->value(); - $mask_w_alpha = $this->argument(1)->type('bool')->value(false); - - $image_size = $image->getSize(); - - // create empty canvas - $canvas = $image->getDriver()->newImage($image_size->width, $image_size->height, array(0,0,0,0)); - - // build mask image from source - $mask = $image->getDriver()->init($mask_source); - $mask_size = $mask->getSize(); - - // resize mask to size of current image (if necessary) - if ($mask_size != $image_size) { - $mask->resize($image_size->width, $image_size->height); - } - - imagealphablending($canvas->getCore(), false); - - if ( ! $mask_w_alpha) { - // mask from greyscale image - imagefilter($mask->getCore(), IMG_FILTER_GRAYSCALE); - } - - // redraw old image pixel by pixel considering alpha map - for ($x=0; $x < $image_size->width; $x++) { - for ($y=0; $y < $image_size->height; $y++) { - - $color = $image->pickColor($x, $y, 'array'); - $alpha = $mask->pickColor($x, $y, 'array'); - - if ($mask_w_alpha) { - $alpha = $alpha[3]; // use alpha channel as mask - } else { - - if ($alpha[3] == 0) { // transparent as black - $alpha = 0; - } else { - - // $alpha = floatval(round((($alpha[0] + $alpha[1] + $alpha[3]) / 3) / 255, 2)); - - // image is greyscale, so channel doesn't matter (use red channel) - $alpha = floatval(round($alpha[0] / 255, 2)); - } - - } - - // preserve alpha of original image... - if ($color[3] < $alpha) { - $alpha = $color[3]; - } - - // replace alpha value - $color[3] = $alpha; - - // redraw pixel - $canvas->pixel($color, $x, $y); - } - } - - - // replace current image with masked instance - $image->setCore($canvas->getCore()); - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/OpacityCommand.php b/src/Intervention/Image/Gd/Commands/OpacityCommand.php deleted file mode 100644 index 081e68a4a..000000000 --- a/src/Intervention/Image/Gd/Commands/OpacityCommand.php +++ /dev/null @@ -1,29 +0,0 @@ -argument(0)->between(0, 100)->required()->value(); - - // get size of image - $size = $image->getSize(); - - // build temp alpha mask - $mask_color = sprintf('rgba(0, 0, 0, %.1F)', $transparency / 100); - $mask = $image->getDriver()->newImage($size->width, $size->height, $mask_color); - - // mask image - $image->mask($mask->getCore(), true); - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/PickColorCommand.php b/src/Intervention/Image/Gd/Commands/PickColorCommand.php deleted file mode 100644 index 9fb4bb422..000000000 --- a/src/Intervention/Image/Gd/Commands/PickColorCommand.php +++ /dev/null @@ -1,36 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $y = $this->argument(1)->type('digit')->required()->value(); - $format = $this->argument(2)->type('string')->value('array'); - - // pick color - $color = imagecolorat($image->getCore(), $x, $y); - - if ( ! imageistruecolor($image->getCore())) { - $color = imagecolorsforindex($image->getCore(), $color); - $color['alpha'] = round(1 - $color['alpha'] / 127, 2); - } - - $color = new Color($color); - - // format to output - $this->setOutput($color->format($format)); - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/PixelCommand.php b/src/Intervention/Image/Gd/Commands/PixelCommand.php deleted file mode 100644 index 67f3e3b95..000000000 --- a/src/Intervention/Image/Gd/Commands/PixelCommand.php +++ /dev/null @@ -1,24 +0,0 @@ -argument(0)->required()->value(); - $color = new Color($color); - $x = $this->argument(1)->type('digit')->required()->value(); - $y = $this->argument(2)->type('digit')->required()->value(); - - return imagesetpixel($image->getCore(), $x, $y, $color->getInt()); - } -} diff --git a/src/Intervention/Image/Gd/Commands/PixelateCommand.php b/src/Intervention/Image/Gd/Commands/PixelateCommand.php deleted file mode 100644 index 2e2093d7d..000000000 --- a/src/Intervention/Image/Gd/Commands/PixelateCommand.php +++ /dev/null @@ -1,19 +0,0 @@ -argument(0)->type('digit')->value(10); - - return imagefilter($image->getCore(), IMG_FILTER_PIXELATE, $size, true); - } -} diff --git a/src/Intervention/Image/Gd/Commands/ResetCommand.php b/src/Intervention/Image/Gd/Commands/ResetCommand.php deleted file mode 100644 index c8d2e4a12..000000000 --- a/src/Intervention/Image/Gd/Commands/ResetCommand.php +++ /dev/null @@ -1,35 +0,0 @@ -argument(0)->value(); - - if (is_resource($backup = $image->getBackup($backupName))) { - - // destroy current resource - imagedestroy($image->getCore()); - - // clone backup - $backup = $image->getDriver()->cloneCore($backup); - - // reset to new resource - $image->setCore($backup); - - return true; - } - - throw new \Intervention\Image\Exception\RuntimeException( - "Backup not available. Call backup() before reset()." - ); - } -} diff --git a/src/Intervention/Image/Gd/Commands/ResizeCanvasCommand.php b/src/Intervention/Image/Gd/Commands/ResizeCanvasCommand.php deleted file mode 100644 index c88e6f19d..000000000 --- a/src/Intervention/Image/Gd/Commands/ResizeCanvasCommand.php +++ /dev/null @@ -1,81 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $height = $this->argument(1)->type('digit')->required()->value(); - $anchor = $this->argument(2)->value('center'); - $relative = $this->argument(3)->type('boolean')->value(); - $bgcolor = $this->argument(4)->value(); - - $original_width = $image->getWidth(); - $original_height = $image->getHeight(); - - // check of only width or height is set - $width = is_null($width) ? $original_width : intval($width); - $height = is_null($height) ? $original_height : intval($height); - - // check on relative width/height - if ($relative) { - $width = $original_width + $width; - $height = $original_height + $height; - } - - // check for negative width/height - $width = ($width <= 0) ? $width + $original_width : $width; - $height = ($height <= 0) ? $height + $original_height : $height; - - // create new canvas - $canvas = $image->getDriver()->newImage($width, $height, $bgcolor); - - // set copy position - $canvas_size = $canvas->getSize()->align($anchor); - $image_size = $image->getSize()->align($anchor); - $canvas_pos = $image_size->relativePosition($canvas_size); - $image_pos = $canvas_size->relativePosition($image_size); - - if ($width <= $original_width) { - $dst_x = 0; - $src_x = $canvas_pos->x; - $src_w = $canvas_size->width; - } else { - $dst_x = $image_pos->x; - $src_x = 0; - $src_w = $original_width; - } - - if ($height <= $original_height) { - $dst_y = 0; - $src_y = $canvas_pos->y; - $src_h = $canvas_size->height; - } else { - $dst_y = $image_pos->y; - $src_y = 0; - $src_h = $original_height; - } - - // make image area transparent to keep transparency - // even if background-color is set - $transparent = imagecolorallocatealpha($canvas->getCore(), 255, 255, 255, 127); - imagealphablending($canvas->getCore(), false); // do not blend / just overwrite - imagefilledrectangle($canvas->getCore(), $dst_x, $dst_y, $dst_x + $src_w - 1, $dst_y + $src_h - 1, $transparent); - - // copy image into new canvas - imagecopy($canvas->getCore(), $image->getCore(), $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); - - // set new core to canvas - $image->setCore($canvas->getCore()); - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/ResizeCommand.php b/src/Intervention/Image/Gd/Commands/ResizeCommand.php deleted file mode 100644 index 2b5700f1b..000000000 --- a/src/Intervention/Image/Gd/Commands/ResizeCommand.php +++ /dev/null @@ -1,82 +0,0 @@ -argument(0)->value(); - $height = $this->argument(1)->value(); - $constraints = $this->argument(2)->type('closure')->value(); - - // resize box - $resized = $image->getSize()->resize($width, $height, $constraints); - - // modify image - $this->modify($image, 0, 0, 0, 0, $resized->getWidth(), $resized->getHeight(), $image->getWidth(), $image->getHeight()); - - return true; - } - - /** - * Wrapper function for 'imagecopyresampled' - * - * @param Image $image - * @param integer $dst_x - * @param integer $dst_y - * @param integer $src_x - * @param integer $src_y - * @param integer $dst_w - * @param integer $dst_h - * @param integer $src_w - * @param integer $src_h - * @return boolean - */ - protected function modify($image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h) - { - // create new image - $modified = imagecreatetruecolor($dst_w, $dst_h); - - // get current image - $resource = $image->getCore(); - - // preserve transparency - $transIndex = imagecolortransparent($resource); - - if ($transIndex != -1) { - $rgba = imagecolorsforindex($modified, $transIndex); - $transColor = imagecolorallocatealpha($modified, $rgba['red'], $rgba['green'], $rgba['blue'], 127); - imagefill($modified, 0, 0, $transColor); - imagecolortransparent($modified, $transColor); - } else { - imagealphablending($modified, false); - imagesavealpha($modified, true); - } - - // copy content from resource - $result = imagecopyresampled( - $modified, - $resource, - $dst_x, - $dst_y, - $src_x, - $src_y, - $dst_w, - $dst_h, - $src_w, - $src_h - ); - - // set new content as recource - $image->setCore($modified); - - return $result; - } -} diff --git a/src/Intervention/Image/Gd/Commands/RotateCommand.php b/src/Intervention/Image/Gd/Commands/RotateCommand.php deleted file mode 100644 index 26a460db4..000000000 --- a/src/Intervention/Image/Gd/Commands/RotateCommand.php +++ /dev/null @@ -1,26 +0,0 @@ -argument(0)->type('numeric')->required()->value(); - $color = $this->argument(1)->value(); - $color = new Color($color); - - // rotate image - $image->setCore(imagerotate($image->getCore(), $angle, $color->getInt())); - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Commands/SharpenCommand.php b/src/Intervention/Image/Gd/Commands/SharpenCommand.php deleted file mode 100644 index 9c1ca2063..000000000 --- a/src/Intervention/Image/Gd/Commands/SharpenCommand.php +++ /dev/null @@ -1,32 +0,0 @@ -argument(0)->between(0, 100)->value(10); - - // build matrix - $min = $amount >= 10 ? $amount * -0.01 : 0; - $max = $amount * -0.025; - $abs = ((4 * $min + 4 * $max) * -1) + 1; - $div = 1; - - $matrix = array( - array($min, $max, $min), - array($max, $abs, $max), - array($min, $max, $min) - ); - - // apply the matrix - return imageconvolution($image->getCore(), $matrix, $div, 0); - } -} diff --git a/src/Intervention/Image/Gd/Commands/TrimCommand.php b/src/Intervention/Image/Gd/Commands/TrimCommand.php deleted file mode 100644 index 49a9ac6ea..000000000 --- a/src/Intervention/Image/Gd/Commands/TrimCommand.php +++ /dev/null @@ -1,176 +0,0 @@ -argument(0)->type('string')->value(); - $away = $this->argument(1)->value(); - $tolerance = $this->argument(2)->type('numeric')->value(0); - $feather = $this->argument(3)->type('numeric')->value(0); - - $width = $image->getWidth(); - $height = $image->getHeight(); - - // default values - $checkTransparency = false; - - // define borders to trim away - if (is_null($away)) { - $away = array('top', 'right', 'bottom', 'left'); - } elseif (is_string($away)) { - $away = array($away); - } - - // lower border names - foreach ($away as $key => $value) { - $away[$key] = strtolower($value); - } - - // define base color position - switch (strtolower($base)) { - case 'transparent': - case 'trans': - $checkTransparency = true; - $base_x = 0; - $base_y = 0; - break; - - case 'bottom-right': - case 'right-bottom': - $base_x = $width - 1; - $base_y = $height - 1; - break; - - default: - case 'top-left': - case 'left-top': - $base_x = 0; - $base_y = 0; - break; - } - - // pick base color - if ($checkTransparency) { - $color = new Color; // color will only be used to compare alpha channel - } else { - $color = $image->pickColor($base_x, $base_y, 'object'); - } - - $top_x = 0; - $top_y = 0; - $bottom_x = $width; - $bottom_y = $height; - - // search upper part of image for colors to trim away - if (in_array('top', $away)) { - - for ($y=0; $y < ceil($height/2); $y++) { - for ($x=0; $x < $width; $x++) { - - $checkColor = $image->pickColor($x, $y, 'object'); - - if ($checkTransparency) { - $checkColor->r = $color->r; - $checkColor->g = $color->g; - $checkColor->b = $color->b; - } - - if ($color->differs($checkColor, $tolerance)) { - $top_y = max(0, $y - $feather); - break 2; - } - - } - } - - } - - // search left part of image for colors to trim away - if (in_array('left', $away)) { - - for ($x=0; $x < ceil($width/2); $x++) { - for ($y=$top_y; $y < $height; $y++) { - - $checkColor = $image->pickColor($x, $y, 'object'); - - if ($checkTransparency) { - $checkColor->r = $color->r; - $checkColor->g = $color->g; - $checkColor->b = $color->b; - } - - if ($color->differs($checkColor, $tolerance)) { - $top_x = max(0, $x - $feather); - break 2; - } - - } - } - - } - - // search lower part of image for colors to trim away - if (in_array('bottom', $away)) { - - for ($y=($height-1); $y >= floor($height/2)-1; $y--) { - for ($x=$top_x; $x < $width; $x++) { - - $checkColor = $image->pickColor($x, $y, 'object'); - - if ($checkTransparency) { - $checkColor->r = $color->r; - $checkColor->g = $color->g; - $checkColor->b = $color->b; - } - - if ($color->differs($checkColor, $tolerance)) { - $bottom_y = min($height, $y+1 + $feather); - break 2; - } - - } - } - - } - - // search right part of image for colors to trim away - if (in_array('right', $away)) { - - for ($x=($width-1); $x >= floor($width/2)-1; $x--) { - for ($y=$top_y; $y < $bottom_y; $y++) { - - $checkColor = $image->pickColor($x, $y, 'object'); - - if ($checkTransparency) { - $checkColor->r = $color->r; - $checkColor->g = $color->g; - $checkColor->b = $color->b; - } - - if ($color->differs($checkColor, $tolerance)) { - $bottom_x = min($width, $x+1 + $feather); - break 2; - } - - } - } - - } - - - // trim parts of image - return $this->modify($image, 0, 0, $top_x, $top_y, ($bottom_x-$top_x), ($bottom_y-$top_y), ($bottom_x-$top_x), ($bottom_y-$top_y)); - - } -} diff --git a/src/Intervention/Image/Gd/Commands/WidenCommand.php b/src/Intervention/Image/Gd/Commands/WidenCommand.php deleted file mode 100644 index c7d396f1b..000000000 --- a/src/Intervention/Image/Gd/Commands/WidenCommand.php +++ /dev/null @@ -1,28 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $additionalConstraints = $this->argument(1)->type('closure')->value(); - - $this->arguments[0] = $width; - $this->arguments[1] = null; - $this->arguments[2] = function ($constraint) use ($additionalConstraints) { - $constraint->aspectRatio(); - if(is_callable($additionalConstraints)) - $additionalConstraints($constraint); - }; - - return parent::execute($image); - } -} diff --git a/src/Intervention/Image/Gd/Decoder.php b/src/Intervention/Image/Gd/Decoder.php deleted file mode 100644 index 313e62619..000000000 --- a/src/Intervention/Image/Gd/Decoder.php +++ /dev/null @@ -1,136 +0,0 @@ -gdResourceToTruecolor($core); - - // build image - $image = $this->initFromGdResource($core); - $image->mime = $info['mime']; - $image->setFileInfoFromPath($path); - - return $image; - } - - /** - * Initiates new image from GD resource - * - * @param Resource $resource - * @return \Intervention\Image\Image - */ - public function initFromGdResource($resource) - { - return new Image(new Driver, $resource); - } - - /** - * Initiates new image from Imagick object - * - * @param Imagick $object - * @return \Intervention\Image\Image - */ - public function initFromImagick(\Imagick $object) - { - throw new \Intervention\Image\Exception\NotSupportedException( - "Gd driver is unable to init from Imagick object." - ); - } - - /** - * Initiates new image from binary data - * - * @param string $data - * @return \Intervention\Image\Image - */ - public function initFromBinary($binary) - { - $resource = @imagecreatefromstring($binary); - - if ($resource === false) { - throw new \Intervention\Image\Exception\NotReadableException( - "Unable to init from given binary data." - ); - } - - $image = $this->initFromGdResource($resource); - $image->mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $binary); - - return $image; - } - - /** - * Transform GD resource into Truecolor version - * - * @param resource $resource - * @return bool - */ - public function gdResourceToTruecolor(&$resource) - { - $width = imagesx($resource); - $height = imagesy($resource); - - // new canvas - $canvas = imagecreatetruecolor($width, $height); - - // fill with transparent color - imagealphablending($canvas, false); - $transparent = imagecolorallocatealpha($canvas, 255, 255, 255, 127); - imagefilledrectangle($canvas, 0, 0, $width, $height, $transparent); - imagecolortransparent($canvas, $transparent); - imagealphablending($canvas, true); - - // copy original - imagecopy($canvas, $resource, 0, 0, 0, 0, $width, $height); - imagedestroy($resource); - - $resource = $canvas; - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Driver.php b/src/Intervention/Image/Gd/Driver.php deleted file mode 100644 index 28612fc9c..000000000 --- a/src/Intervention/Image/Gd/Driver.php +++ /dev/null @@ -1,84 +0,0 @@ -coreAvailable()) { - throw new \Intervention\Image\Exception\NotSupportedException( - "GD Library extension not available with this PHP installation." - ); - } - - $this->decoder = $decoder ? $decoder : new Decoder; - $this->encoder = $encoder ? $encoder : new Encoder; - } - - /** - * Creates new image instance - * - * @param integer $width - * @param integer $height - * @param string $background - * @return \Intervention\Image\Image - */ - public function newImage($width, $height, $background = null) - { - // create empty resource - $core = imagecreatetruecolor($width, $height); - $image = new \Intervention\Image\Image(new static, $core); - - // set background color - $background = new Color($background); - imagefill($image->getCore(), 0, 0, $background->getInt()); - - return $image; - } - - /** - * Reads given string into color object - * - * @param string $value - * @return AbstractColor - */ - public function parseColor($value) - { - return new Color($value); - } - - /** - * Checks if core module installation is available - * - * @return boolean - */ - protected function coreAvailable() - { - return (extension_loaded('gd') && function_exists('gd_info')); - } - - /** - * Returns clone of given core - * - * @return mixed - */ - public function cloneCore($core) - { - $width = imagesx($core); - $height = imagesy($core); - $clone = imagecreatetruecolor($width, $height); - imagealphablending($clone, false); - imagesavealpha($clone, true); - - imagecopy($clone, $core, 0, 0, 0, 0, $width, $height); - - return $clone; - } -} diff --git a/src/Intervention/Image/Gd/Encoder.php b/src/Intervention/Image/Gd/Encoder.php deleted file mode 100644 index 97a2c271e..000000000 --- a/src/Intervention/Image/Gd/Encoder.php +++ /dev/null @@ -1,105 +0,0 @@ -image->getCore(), null, $this->quality); - $this->image->mime = image_type_to_mime_type(IMAGETYPE_JPEG); - $buffer = ob_get_contents(); - ob_end_clean(); - - return $buffer; - } - - /** - * Processes and returns encoded image as PNG string - * - * @return string - */ - protected function processPng() - { - ob_start(); - $resource = $this->image->getCore(); - imagealphablending($resource, false); - imagesavealpha($resource, true); - imagepng($resource, null, -1); - $this->image->mime = image_type_to_mime_type(IMAGETYPE_PNG); - $buffer = ob_get_contents(); - ob_end_clean(); - - return $buffer; - } - - /** - * Processes and returns encoded image as GIF string - * - * @return string - */ - protected function processGif() - { - ob_start(); - imagegif($this->image->getCore()); - $this->image->mime = image_type_to_mime_type(IMAGETYPE_GIF); - $buffer = ob_get_contents(); - ob_end_clean(); - - return $buffer; - } - - /** - * Processes and returns encoded image as TIFF string - * - * @return string - */ - protected function processTiff() - { - throw new \Intervention\Image\Exception\NotSupportedException( - "TIFF format is not supported by Gd Driver." - ); - } - - /** - * Processes and returns encoded image as BMP string - * - * @return string - */ - protected function processBmp() - { - throw new \Intervention\Image\Exception\NotSupportedException( - "BMP format is not supported by Gd Driver." - ); - } - - /** - * Processes and returns encoded image as ICO string - * - * @return string - */ - protected function processIco() - { - throw new \Intervention\Image\Exception\NotSupportedException( - "ICO format is not supported by Gd Driver." - ); - } - - /** - * Processes and returns encoded image as PSD string - * - * @return string - */ - protected function processPsd() - { - throw new \Intervention\Image\Exception\NotSupportedException( - "PSD format is not supported by Gd Driver." - ); - } -} diff --git a/src/Intervention/Image/Gd/Font.php b/src/Intervention/Image/Gd/Font.php deleted file mode 100644 index df2d082d0..000000000 --- a/src/Intervention/Image/Gd/Font.php +++ /dev/null @@ -1,255 +0,0 @@ -size * 0.75)); - } - - /** - * Filter function to access internal integer font values - * - * @return integer - */ - private function getInternalFont() - { - $internalfont = is_null($this->file) ? 1 : $this->file; - $internalfont = is_numeric($internalfont) ? $internalfont : false; - - if ( ! in_array($internalfont, array(1, 2, 3, 4, 5))) { - throw new \Intervention\Image\Exception\NotSupportedException( - sprintf('Internal GD font (%s) not available. Use only 1-5.', $internalfont) - ); - } - - return intval($internalfont); - } - - /** - * Get width of an internal font character - * - * @return integer - */ - private function getInternalFontWidth() - { - return $this->getInternalFont() + 4; - } - - /** - * Get height of an internal font character - * - * @return integer - */ - private function getInternalFontHeight() - { - switch ($this->getInternalFont()) { - case 1: - return 8; - - case 2: - return 14; - - case 3: - return 14; - - case 4: - return 16; - - case 5: - return 16; - } - } - - /** - * Calculates bounding box of current font setting - * - * @return Array - */ - public function getBoxSize() - { - $box = array(); - - if ($this->hasApplicableFontFile()) { - - // get bounding box with angle 0 - $box = imagettfbbox($this->getPointSize(), 0, $this->file, $this->text); - - // rotate points manually - if ($this->angle != 0) { - - $angle = pi() * 2 - $this->angle * pi() * 2 / 360; - - for ($i=0; $i<4; $i++) { - $x = $box[$i * 2]; - $y = $box[$i * 2 + 1]; - $box[$i * 2] = cos($angle) * $x - sin($angle) * $y; - $box[$i * 2 + 1] = sin($angle) * $x + cos($angle) * $y; - } - } - - $box['width'] = intval(abs($box[4] - $box[0])); - $box['height'] = intval(abs($box[5] - $box[1])); - - } else { - - // get current internal font size - $width = $this->getInternalFontWidth(); - $height = $this->getInternalFontHeight(); - - if (strlen($this->text) == 0) { - // no text -> no boxsize - $box['width'] = 0; - $box['height'] = 0; - } else { - // calculate boxsize - $box['width'] = strlen($this->text) * $width; - $box['height'] = $height; - } - } - - return $box; - } - - /** - * Draws font to given image at given position - * - * @param Image $image - * @param integer $posx - * @param integer $posy - * @return void - */ - public function applyToImage(Image $image, $posx = 0, $posy = 0) - { - // parse text color - $color = new Color($this->color); - - if ($this->hasApplicableFontFile()) { - - if ($this->angle != 0 || is_string($this->align) || is_string($this->valign)) { - - $box = $this->getBoxSize(); - - $align = is_null($this->align) ? 'left' : strtolower($this->align); - $valign = is_null($this->valign) ? 'bottom' : strtolower($this->valign); - - // correction on position depending on v/h alignment - switch ($align.'-'.$valign) { - - case 'center-top': - $posx = $posx - round(($box[6]+$box[4])/2); - $posy = $posy - round(($box[7]+$box[5])/2); - break; - - case 'right-top': - $posx = $posx - $box[4]; - $posy = $posy - $box[5]; - break; - - case 'left-top': - $posx = $posx - $box[6]; - $posy = $posy - $box[7]; - break; - - case 'center-center': - case 'center-middle': - $posx = $posx - round(($box[0]+$box[4])/2); - $posy = $posy - round(($box[1]+$box[5])/2); - break; - - case 'right-center': - case 'right-middle': - $posx = $posx - round(($box[2]+$box[4])/2); - $posy = $posy - round(($box[3]+$box[5])/2); - break; - - case 'left-center': - case 'left-middle': - $posx = $posx - round(($box[0]+$box[6])/2); - $posy = $posy - round(($box[1]+$box[7])/2); - break; - - case 'center-bottom': - $posx = $posx - round(($box[0]+$box[2])/2); - $posy = $posy - round(($box[1]+$box[3])/2); - break; - - case 'right-bottom': - $posx = $posx - $box[2]; - $posy = $posy - $box[3]; - break; - - case 'left-bottom': - $posx = $posx - $box[0]; - $posy = $posy - $box[1]; - break; - } - } - - // enable alphablending for imagettftext - imagealphablending($image->getCore(), true); - - // draw ttf text - imagettftext($image->getCore(), $this->getPointSize(), $this->angle, $posx, $posy, $color->getInt(), $this->file, $this->text); - - } else { - - // get box size - $box = $this->getBoxSize(); - $width = $box['width']; - $height = $box['height']; - - // internal font specific position corrections - if ($this->getInternalFont() == 1) { - $top_correction = 1; - $bottom_correction = 2; - } elseif ($this->getInternalFont() == 3) { - $top_correction = 2; - $bottom_correction = 4; - } else { - $top_correction = 3; - $bottom_correction = 4; - } - - // x-position corrections for horizontal alignment - switch (strtolower($this->align)) { - case 'center': - $posx = ceil($posx - ($width / 2)); - break; - - case 'right': - $posx = ceil($posx - $width) + 1; - break; - } - - // y-position corrections for vertical alignment - switch (strtolower($this->valign)) { - case 'center': - case 'middle': - $posy = ceil($posy - ($height / 2)); - break; - - case 'top': - $posy = ceil($posy - $top_correction); - break; - - default: - case 'bottom': - $posy = round($posy - $height + $bottom_correction); - break; - } - - // draw text - imagestring($image->getCore(), $this->getInternalFont(), $posx, $posy, $this->text, $color->getInt()); - } - } -} diff --git a/src/Intervention/Image/Gd/Shapes/CircleShape.php b/src/Intervention/Image/Gd/Shapes/CircleShape.php deleted file mode 100644 index c512d86ac..000000000 --- a/src/Intervention/Image/Gd/Shapes/CircleShape.php +++ /dev/null @@ -1,40 +0,0 @@ -width = is_numeric($diameter) ? intval($diameter) : $this->diameter; - $this->height = is_numeric($diameter) ? intval($diameter) : $this->diameter; - $this->diameter = is_numeric($diameter) ? intval($diameter) : $this->diameter; - } - - /** - * Draw current circle on given image - * - * @param Image $image - * @param integer $x - * @param integer $y - * @return boolean - */ - public function applyToImage(Image $image, $x = 0, $y = 0) - { - return parent::applyToImage($image, $x, $y); - } -} diff --git a/src/Intervention/Image/Gd/Shapes/EllipseShape.php b/src/Intervention/Image/Gd/Shapes/EllipseShape.php deleted file mode 100644 index ae5de9559..000000000 --- a/src/Intervention/Image/Gd/Shapes/EllipseShape.php +++ /dev/null @@ -1,64 +0,0 @@ -width = is_numeric($width) ? intval($width) : $this->width; - $this->height = is_numeric($height) ? intval($height) : $this->height; - } - - /** - * Draw ellipse instance on given image - * - * @param Image $image - * @param integer $x - * @param integer $y - * @return boolean - */ - public function applyToImage(Image $image, $x = 0, $y = 0) - { - // parse background color - $background = new Color($this->background); - - if ($this->hasBorder()) { - // slightly smaller ellipse to keep 1px bordered edges clean - imagefilledellipse($image->getCore(), $x, $y, $this->width-1, $this->height-1, $background->getInt()); - - $border_color = new Color($this->border_color); - imagesetthickness($image->getCore(), $this->border_width); - - // gd's imageellipse doesn't respect imagesetthickness so i use imagearc with 359.9 degrees here - imagearc($image->getCore(), $x, $y, $this->width, $this->height, 0, 359.99, $border_color->getInt()); - } else { - imagefilledellipse($image->getCore(), $x, $y, $this->width, $this->height, $background->getInt()); - } - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Shapes/LineShape.php b/src/Intervention/Image/Gd/Shapes/LineShape.php deleted file mode 100644 index 92e240157..000000000 --- a/src/Intervention/Image/Gd/Shapes/LineShape.php +++ /dev/null @@ -1,89 +0,0 @@ -x = is_numeric($x) ? intval($x) : $this->x; - $this->y = is_numeric($y) ? intval($y) : $this->y; - } - - /** - * Set current line color - * - * @param string $color - * @return void - */ - public function color($color) - { - $this->color = $color; - } - - /** - * Set current line width in pixels - * - * @param integer $width - * @return void - */ - public function width($width) - { - throw new \Intervention\Image\Exception\NotSupportedException( - "Line width is not supported by GD driver." - ); - } - - /** - * Draw current instance of line to given endpoint on given image - * - * @param Image $image - * @param integer $x - * @param integer $y - * @return boolean - */ - public function applyToImage(Image $image, $x = 0, $y = 0) - { - $color = new Color($this->color); - imageline($image->getCore(), $x, $y, $this->x, $this->y, $color->getInt()); - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Shapes/PolygonShape.php b/src/Intervention/Image/Gd/Shapes/PolygonShape.php deleted file mode 100644 index c739fbb59..000000000 --- a/src/Intervention/Image/Gd/Shapes/PolygonShape.php +++ /dev/null @@ -1,48 +0,0 @@ -points = $points; - } - - /** - * Draw polygon on given image - * - * @param Image $image - * @param integer $x - * @param integer $y - * @return boolean - */ - public function applyToImage(Image $image, $x = 0, $y = 0) - { - $background = new Color($this->background); - imagefilledpolygon($image->getCore(), $this->points, intval(count($this->points) / 2), $background->getInt()); - - if ($this->hasBorder()) { - $border_color = new Color($this->border_color); - imagesetthickness($image->getCore(), $this->border_width); - imagepolygon($image->getCore(), $this->points, intval(count($this->points) / 2), $border_color->getInt()); - } - - return true; - } -} diff --git a/src/Intervention/Image/Gd/Shapes/RectangleShape.php b/src/Intervention/Image/Gd/Shapes/RectangleShape.php deleted file mode 100644 index 757eb5d75..000000000 --- a/src/Intervention/Image/Gd/Shapes/RectangleShape.php +++ /dev/null @@ -1,75 +0,0 @@ -x1 = is_numeric($x1) ? intval($x1) : $this->x1; - $this->y1 = is_numeric($y1) ? intval($y1) : $this->y1; - $this->x2 = is_numeric($x2) ? intval($x2) : $this->x2; - $this->y2 = is_numeric($y2) ? intval($y2) : $this->y2; - } - - /** - * Draw rectangle to given image at certain position - * - * @param Image $image - * @param integer $x - * @param integer $y - * @return boolean - */ - public function applyToImage(Image $image, $x = 0, $y = 0) - { - $background = new Color($this->background); - imagefilledrectangle($image->getCore(), $this->x1, $this->y1, $this->x2, $this->y2, $background->getInt()); - - if ($this->hasBorder()) { - $border_color = new Color($this->border_color); - imagesetthickness($image->getCore(), $this->border_width); - imagerectangle($image->getCore(), $this->x1, $this->y1, $this->x2, $this->y2, $border_color->getInt()); - } - - return true; - } -} diff --git a/src/Intervention/Image/Image.php b/src/Intervention/Image/Image.php deleted file mode 100644 index 4c8e690e0..000000000 --- a/src/Intervention/Image/Image.php +++ /dev/null @@ -1,363 +0,0 @@ -driver = $driver; - $this->core = $core; - } - - /** - * Magic method to catch all image calls - * usually any AbstractCommand - * - * @param string $name - * @param Array $arguments - * @return mixed - */ - public function __call($name, $arguments) - { - $command = $this->driver->executeCommand($this, $name, $arguments); - return $command->hasOutput() ? $command->getOutput() : $this; - } - - /** - * Starts encoding of current image - * - * @param string $format - * @param integer $quality - * @return \Intervention\Image\Image - */ - public function encode($format = null, $quality = 90) - { - return $this->driver->encode($this, $format, $quality); - } - - /** - * Saves encoded image in filesystem - * - * @param string $path - * @param integer $quality - * @return \Intervention\Image\Image - */ - public function save($path = null, $quality = null) - { - $path = is_null($path) ? $this->basePath() : $path; - - if (is_null($path)) { - throw new Exception\NotWritableException( - "Can't write to undefined path." - ); - } - - $data = $this->encode(pathinfo($path, PATHINFO_EXTENSION), $quality); - $saved = @file_put_contents($path, $data); - - if ($saved === false) { - throw new Exception\NotWritableException( - "Can't write image data to path ({$path})" - ); - } - - // set new file info - $this->setFileInfoFromPath($path); - - return $this; - } - - /** - * Runs a given filter on current image - * - * @param FiltersFilterInterface $filter - * @return \Intervention\Image\Image - */ - public function filter(Filters\FilterInterface $filter) - { - return $filter->applyFilter($this); - } - - /** - * Returns current image driver - * - * @return \Intervention\Image\AbstractDriver - */ - public function getDriver() - { - return $this->driver; - } - - /** - * Sets current image driver - * @param AbstractDriver $driver - */ - public function setDriver(AbstractDriver $driver) - { - $this->driver = $driver; - - return $this; - } - - /** - * Returns current image resource/obj - * - * @return mixed - */ - public function getCore() - { - return $this->core; - } - - /** - * Sets current image resource - * - * @param mixed $core - */ - public function setCore($core) - { - $this->core = $core; - - return $this; - } - - /** - * Returns current image backup - * - * @param string $name - * @return mixed - */ - public function getBackup($name = null) - { - $name = is_null($name) ? 'default' : $name; - - if ( ! $this->backupExists($name)) { - throw new \Intervention\Image\Exception\RuntimeException( - "Backup with name ({$name}) not available. Call backup() before reset()." - ); - } - - return $this->backups[$name]; - } - - /** - * Returns all backups attached to image - * - * @return array - */ - public function getBackups() - { - return $this->backups; - } - - /** - * Sets current image backup - * - * @param mixed $resource - * @param string $name - * @return self - */ - public function setBackup($resource, $name = null) - { - $name = is_null($name) ? 'default' : $name; - - $this->backups[$name] = $resource; - - return $this; - } - - /** - * Checks if named backup exists - * - * @param string $name - * @return bool - */ - private function backupExists($name) - { - return array_key_exists($name, $this->backups); - } - - /** - * Checks if current image is already encoded - * - * @return boolean - */ - public function isEncoded() - { - return ! empty($this->encoded); - } - - /** - * Returns encoded image data of current image - * - * @return string - */ - public function getEncoded() - { - return $this->encoded; - } - - /** - * Sets encoded image buffer - * - * @param string $value - */ - public function setEncoded($value) - { - $this->encoded = $value; - - return $this; - } - - /** - * Calculates current image width - * - * @return integer - */ - public function getWidth() - { - return $this->getSize()->width; - } - - /** - * Alias of getWidth() - * - * @return integer - */ - public function width() - { - return $this->getWidth(); - } - - /** - * Calculates current image height - * - * @return integer - */ - public function getHeight() - { - return $this->getSize()->height; - } - - /** - * Alias of getHeight - * - * @return integer - */ - public function height() - { - return $this->getHeight(); - } - - /** - * Reads mime type - * - * @return string - */ - public function mime() - { - return $this->mime; - } - - /** - * Returns encoded image data in string conversion - * - * @return string - */ - public function __toString() - { - return $this->encoded; - } - - /** - * Cloning an image - */ - public function __clone() - { - $this->core = $this->driver->cloneCore($this->core); - } -} diff --git a/src/Intervention/Image/ImageManager.php b/src/Intervention/Image/ImageManager.php deleted file mode 100644 index 205d17f04..000000000 --- a/src/Intervention/Image/ImageManager.php +++ /dev/null @@ -1,138 +0,0 @@ - 'gd' - ); - - /** - * Creates new instance of Image Manager - * - * @param array $config - */ - public function __construct(array $config = array()) - { - $this->checkRequirements(); - $this->configure($config); - } - - /** - * Overrides configuration settings - * - * @param array $config - */ - public function configure(array $config = array()) - { - $this->config = array_replace($this->config, $config); - - return $this; - } - - /** - * Initiates an Image instance from different input types - * - * @param mixed $data - * - * @return \Intervention\Image\Image - */ - public function make($data) - { - return $this->createDriver()->init($data); - } - - /** - * Creates an empty image canvas - * - * @param integer $width - * @param integer $height - * @param mixed $background - * - * @return \Intervention\Image\Image - */ - public function canvas($width, $height, $background = null) - { - return $this->createDriver()->newImage($width, $height, $background); - } - - /** - * Create new cached image and run callback - * (requires additional package intervention/imagecache) - * - * @param Closure $callback - * @param integer $lifetime - * @param boolean $returnObj - * - * @return Image - */ - public function cache(Closure $callback, $lifetime = null, $returnObj = false) - { - if (class_exists('Intervention\\Image\\ImageCache')) { - // create imagecache - $imagecache = new ImageCache($this); - - // run callback - if (is_callable($callback)) { - $callback($imagecache); - } - - return $imagecache->get($lifetime, $returnObj); - } - - throw new \Intervention\Image\Exception\MissingDependencyException( - "Please install package intervention/imagecache before running this function." - ); - } - - /** - * Creates a driver instance according to config settings - * - * @return \Intervention\Image\AbstractDriver - */ - private function createDriver() - { - if (is_string($this->config['driver'])) { - $drivername = ucfirst($this->config['driver']); - $driverclass = sprintf('Intervention\\Image\\%s\\Driver', $drivername); - - if (class_exists($driverclass)) { - return new $driverclass; - } - - throw new \Intervention\Image\Exception\NotSupportedException( - "Driver ({$drivername}) could not be instantiated." - ); - } - - if ($this->config['driver'] instanceof AbstractDriver) { - return $this->config['driver']; - } - - throw new \Intervention\Image\Exception\NotSupportedException( - "Unknown driver type." - ); - } - - /** - * Check if all requirements are available - * - * @return void - */ - private function checkRequirements() - { - if ( ! function_exists('finfo_buffer')) { - throw new \Intervention\Image\Exception\MissingDependencyException( - "PHP Fileinfo extension must be installed/enabled to use Intervention Image." - ); - } - } -} diff --git a/src/Intervention/Image/ImageManagerStatic.php b/src/Intervention/Image/ImageManagerStatic.php deleted file mode 100644 index 3088bf55b..000000000 --- a/src/Intervention/Image/ImageManagerStatic.php +++ /dev/null @@ -1,87 +0,0 @@ -configure($config); - } - - /** - * Statically initiates an Image instance from different input types - * - * @param mixed $data - * - * @return \Intervention\Image\Image - */ - public static function make($data) - { - return self::getManager()->make($data); - } - - /** - * Statically creates an empty image canvas - * - * @param integer $width - * @param integer $height - * @param mixed $background - * - * @return \Intervention\Image\Image - */ - public static function canvas($width, $height, $background = null) - { - return self::getManager()->canvas($width, $height, $background); - } - - /** - * Create new cached image and run callback statically - * - * @param Closure $callback - * @param integer $lifetime - * @param boolean $returnObj - * - * @return mixed - */ - public static function cache(Closure $callback, $lifetime = null, $returnObj = false) - { - return self::getManager()->cache($callback, $lifetime, $returnObj); - } -} diff --git a/src/Intervention/Image/ImageServiceProvider.php b/src/Intervention/Image/ImageServiceProvider.php deleted file mode 100644 index 234e06cfb..000000000 --- a/src/Intervention/Image/ImageServiceProvider.php +++ /dev/null @@ -1,83 +0,0 @@ -provider = $this->getProvider(); - } - - /** - * Bootstrap the application events. - * - * @return void - */ - public function boot() - { - return $this->provider->boot(); - } - - /** - * Register the service provider. - * - * @return void - */ - public function register() - { - return $this->provider->register(); - } - - /** - * Return ServiceProvider according to Laravel version - * - * @return \Intervention\Image\Provider\ProviderInterface - */ - private function getProvider() - { - if ($this->app instanceof \Laravel\Lumen\Application) { - $provider = '\Intervention\Image\ImageServiceProviderLumen'; - } elseif (version_compare(\Illuminate\Foundation\Application::VERSION, '5.0', '<')) { - $provider = '\Intervention\Image\ImageServiceProviderLaravel4'; - } else { - $provider = '\Intervention\Image\ImageServiceProviderLaravel5'; - } - - return new $provider($this->app); - } - - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return array('image'); - } -} diff --git a/src/Intervention/Image/ImageServiceProviderLaravel4.php b/src/Intervention/Image/ImageServiceProviderLaravel4.php deleted file mode 100755 index 6c73a207b..000000000 --- a/src/Intervention/Image/ImageServiceProviderLaravel4.php +++ /dev/null @@ -1,112 +0,0 @@ -package('intervention/image'); - - // try to create imagecache route only if imagecache is present - if (class_exists('Intervention\\Image\\ImageCache')) { - - $app = $this->app; - - // load imagecache config - $app['config']->package('intervention/imagecache', __DIR__.'/../../../../imagecache/src/config', 'imagecache'); - $config = $app['config']; - - // create dynamic manipulation route - if (is_string($config->get('imagecache::route'))) { - - // add original to route templates - $config->set('imagecache::templates.original', null); - - // setup image manipulator route - $app['router']->get($config->get('imagecache::route').'/{template}/{filename}', array('as' => 'imagecache', function ($template, $filename) use ($app, $config) { - - // disable session cookies for image route - $app['config']->set('session.driver', 'array'); - - // find file - foreach ($config->get('imagecache::paths') as $path) { - // don't allow '..' in filenames - $image_path = $path.'/'.str_replace('..', '', $filename); - if (file_exists($image_path) && is_file($image_path)) { - break; - } else { - $image_path = false; - } - } - - // abort if file not found - if ($image_path === false) { - $app->abort(404); - } - - // define template callback - $callback = $config->get("imagecache::templates.{$template}"); - - if (is_callable($callback) || class_exists($callback)) { - - // image manipulation based on callback - $content = $app['image']->cache(function ($image) use ($image_path, $callback) { - - switch (true) { - case is_callable($callback): - return $callback($image->make($image_path)); - break; - - case class_exists($callback): - return $image->make($image_path)->filter(new $callback); - break; - } - - }, $config->get('imagecache::lifetime')); - - } else { - - // get original image file contents - $content = file_get_contents($image_path); - } - - // define mime type - $mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $content); - - // return http response - return new IlluminateResponse($content, 200, array( - 'Content-Type' => $mime, - 'Cache-Control' => 'max-age='.($config->get('imagecache::lifetime')*60).', public', - 'Etag' => md5($content) - )); - - }))->where(array('template' => join('|', array_keys($config->get('imagecache::templates'))), 'filename' => '[ \w\\.\\/\\-]+')); - } - } - } - - /** - * Register the service provider. - * - * @return void - */ - public function register() - { - $app = $this->app; - - $app['image'] = $app->share(function ($app) { - return new ImageManager($app['config']->get('image::config')); - }); - - $app->alias('image', 'Intervention\Image\ImageManager'); - } -} diff --git a/src/Intervention/Image/ImageServiceProviderLaravel5.php b/src/Intervention/Image/ImageServiceProviderLaravel5.php deleted file mode 100644 index 9d770b877..000000000 --- a/src/Intervention/Image/ImageServiceProviderLaravel5.php +++ /dev/null @@ -1,89 +0,0 @@ -publishes(array( - __DIR__.'/../../config/config.php' => config_path('image.php') - )); - - // setup intervention/imagecache if package is installed - $this->cacheIsInstalled() ? $this->bootstrapImageCache() : null; - } - - /** - * Register the service provider. - * - * @return void - */ - public function register() - { - $app = $this->app; - - // merge default config - $this->mergeConfigFrom( - __DIR__.'/../../config/config.php', - 'image' - ); - - // create image - $app->singleton('image', function ($app) { - return new ImageManager($app['config']->get('image')); - }); - - $app->alias('image', 'Intervention\Image\ImageManager'); - } - - /** - * Bootstrap imagecache - * - * @return void - */ - private function bootstrapImageCache() - { - $app = $this->app; - $config = __DIR__.'/../../../../imagecache/src/config/config.php'; - - $this->publishes(array( - $config => config_path('imagecache.php') - )); - - // merge default config - $this->mergeConfigFrom( - $config, - 'imagecache' - ); - - // imagecache route - if (is_string(config('imagecache.route'))) { - - $filename_pattern = '[ \w\\.\\/\\-\\@]+'; - - // route to access template applied image file - $app['router']->get(config('imagecache.route').'/{template}/{filename}', array( - 'uses' => 'Intervention\Image\ImageCacheController@getResponse', - 'as' => 'imagecache' - ))->where(array('filename' => $filename_pattern)); - } - } -} diff --git a/src/Intervention/Image/ImageServiceProviderLeague.php b/src/Intervention/Image/ImageServiceProviderLeague.php deleted file mode 100644 index 621ccd1fb..000000000 --- a/src/Intervention/Image/ImageServiceProviderLeague.php +++ /dev/null @@ -1,42 +0,0 @@ -config = $config; - } - - /** - * Register the server provider. - * - * @return void - */ - public function register() - { - $this->getContainer()->share('Intervention\Image\ImageManager', function () { - return new ImageManager($this->config); - }); - } -} diff --git a/src/Intervention/Image/ImageServiceProviderLumen.php b/src/Intervention/Image/ImageServiceProviderLumen.php deleted file mode 100644 index 4a381ccd4..000000000 --- a/src/Intervention/Image/ImageServiceProviderLumen.php +++ /dev/null @@ -1,34 +0,0 @@ -app; - - // merge default config - $this->mergeConfigFrom( - __DIR__.'/../../config/config.php', - 'image' - ); - - // set configuration - $app->configure('image'); - - // create image - $app->singleton('image',function ($app) { - return new ImageManager($app['config']->get('image')); - }); - - $app->alias('image', 'Intervention\Image\ImageManager'); - } -} diff --git a/src/Intervention/Image/Imagick/Color.php b/src/Intervention/Image/Imagick/Color.php deleted file mode 100644 index 3940993ed..000000000 --- a/src/Intervention/Image/Imagick/Color.php +++ /dev/null @@ -1,277 +0,0 @@ -> 24) & 0xFF; - $r = ($value >> 16) & 0xFF; - $g = ($value >> 8) & 0xFF; - $b = $value & 0xFF; - $a = $this->rgb2alpha($a); - - $this->setPixel($r, $g, $b, $a); - } - - /** - * Initiates color object from given array - * - * @param array $value - * @return \Intervention\Image\AbstractColor - */ - public function initFromArray($array) - { - $array = array_values($array); - - if (count($array) == 4) { - - // color array with alpha value - list($r, $g, $b, $a) = $array; - - } elseif (count($array) == 3) { - - // color array without alpha value - list($r, $g, $b) = $array; - $a = 1; - } - - $this->setPixel($r, $g, $b, $a); - } - - /** - * Initiates color object from given string - * - * @param string $value - * - * @return \Intervention\Image\AbstractColor - */ - public function initFromString($value) - { - if ($color = $this->rgbaFromString($value)) { - $this->setPixel($color[0], $color[1], $color[2], $color[3]); - } - } - - /** - * Initiates color object from given ImagickPixel object - * - * @param ImagickPixel $value - * - * @return \Intervention\Image\AbstractColor - */ - public function initFromObject($value) - { - if (is_a($value, '\ImagickPixel')) { - $this->pixel = $value; - } - } - - /** - * Initiates color object from given R, G and B values - * - * @param integer $r - * @param integer $g - * @param integer $b - * - * @return \Intervention\Image\AbstractColor - */ - public function initFromRgb($r, $g, $b) - { - $this->setPixel($r, $g, $b); - } - - /** - * Initiates color object from given R, G, B and A values - * - * @param integer $r - * @param integer $g - * @param integer $b - * @param float $a - * - * @return \Intervention\Image\AbstractColor - */ - public function initFromRgba($r, $g, $b, $a) - { - $this->setPixel($r, $g, $b, $a); - } - - /** - * Calculates integer value of current color instance - * - * @return integer - */ - public function getInt() - { - $r = $this->getRedValue(); - $g = $this->getGreenValue(); - $b = $this->getBlueValue(); - $a = intval(round($this->getAlphaValue() * 255)); - - return intval(($a << 24) + ($r << 16) + ($g << 8) + $b); - } - - /** - * Calculates hexadecimal value of current color instance - * - * @param string $prefix - * - * @return string - */ - public function getHex($prefix = '') - { - return sprintf('%s%02x%02x%02x', $prefix, - $this->getRedValue(), - $this->getGreenValue(), - $this->getBlueValue() - ); - } - - /** - * Calculates RGB(A) in array format of current color instance - * - * @return array - */ - public function getArray() - { - return array( - $this->getRedValue(), - $this->getGreenValue(), - $this->getBlueValue(), - $this->getAlphaValue() - ); - } - - /** - * Calculates RGBA in string format of current color instance - * - * @return string - */ - public function getRgba() - { - return sprintf('rgba(%d, %d, %d, %.2F)', - $this->getRedValue(), - $this->getGreenValue(), - $this->getBlueValue(), - $this->getAlphaValue() - ); - } - - /** - * Determines if current color is different from given color - * - * @param AbstractColor $color - * @param integer $tolerance - * @return boolean - */ - public function differs(\Intervention\Image\AbstractColor $color, $tolerance = 0) - { - $color_tolerance = round($tolerance * 2.55); - $alpha_tolerance = round($tolerance); - - $delta = array( - 'r' => abs($color->getRedValue() - $this->getRedValue()), - 'g' => abs($color->getGreenValue() - $this->getGreenValue()), - 'b' => abs($color->getBlueValue() - $this->getBlueValue()), - 'a' => abs($color->getAlphaValue() - $this->getAlphaValue()) - ); - - return ( - $delta['r'] > $color_tolerance or - $delta['g'] > $color_tolerance or - $delta['b'] > $color_tolerance or - $delta['a'] > $alpha_tolerance - ); - } - - /** - * Returns RGB red value of current color - * - * @return integer - */ - public function getRedValue() - { - return intval(round($this->pixel->getColorValue(\Imagick::COLOR_RED) * 255)); - } - - /** - * Returns RGB green value of current color - * - * @return integer - */ - public function getGreenValue() - { - return intval(round($this->pixel->getColorValue(\Imagick::COLOR_GREEN) * 255)); - } - - /** - * Returns RGB blue value of current color - * - * @return integer - */ - public function getBlueValue() - { - return intval(round($this->pixel->getColorValue(\Imagick::COLOR_BLUE) * 255)); - } - - /** - * Returns RGB alpha value of current color - * - * @return float - */ - public function getAlphaValue() - { - return round($this->pixel->getColorValue(\Imagick::COLOR_ALPHA), 2); - } - - /** - * Initiates ImagickPixel from given RGBA values - * - * @return \ImagickPixel - */ - private function setPixel($r, $g, $b, $a = null) - { - $a = is_null($a) ? 1 : $a; - - return $this->pixel = new \ImagickPixel( - sprintf('rgba(%d, %d, %d, %.2F)', $r, $g, $b, $a) - ); - } - - /** - * Returns current color as ImagickPixel - * - * @return \ImagickPixel - */ - public function getPixel() - { - return $this->pixel; - } - - /** - * Calculates RGA integer alpha value into float value - * - * @param integer $value - * @return float - */ - private function rgb2alpha($value) - { - // (255 -> 1.0) / (0 -> 0.0) - return (float) round($value/255, 2); - } - -} diff --git a/src/Intervention/Image/Imagick/Commands/BackupCommand.php b/src/Intervention/Image/Imagick/Commands/BackupCommand.php deleted file mode 100644 index 90f2d7216..000000000 --- a/src/Intervention/Image/Imagick/Commands/BackupCommand.php +++ /dev/null @@ -1,22 +0,0 @@ -argument(0)->value(); - - // clone current image resource - $image->setBackup(clone $image->getCore(), $backupName); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/BlurCommand.php b/src/Intervention/Image/Imagick/Commands/BlurCommand.php deleted file mode 100644 index b037c1516..000000000 --- a/src/Intervention/Image/Imagick/Commands/BlurCommand.php +++ /dev/null @@ -1,19 +0,0 @@ -argument(0)->between(0, 100)->value(1); - - return $image->getCore()->blurImage(1 * $amount, 0.5 * $amount); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/BrightnessCommand.php b/src/Intervention/Image/Imagick/Commands/BrightnessCommand.php deleted file mode 100644 index eefb1802c..000000000 --- a/src/Intervention/Image/Imagick/Commands/BrightnessCommand.php +++ /dev/null @@ -1,19 +0,0 @@ -argument(0)->between(-100, 100)->required()->value(); - - return $image->getCore()->modulateImage(100 + $level, 100, 100); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/ColorizeCommand.php b/src/Intervention/Image/Imagick/Commands/ColorizeCommand.php deleted file mode 100644 index 51142be27..000000000 --- a/src/Intervention/Image/Imagick/Commands/ColorizeCommand.php +++ /dev/null @@ -1,42 +0,0 @@ -argument(0)->between(-100, 100)->required()->value(); - $green = $this->argument(1)->between(-100, 100)->required()->value(); - $blue = $this->argument(2)->between(-100, 100)->required()->value(); - - // normalize colorize levels - $red = $this->normalizeLevel($red); - $green = $this->normalizeLevel($green); - $blue = $this->normalizeLevel($blue); - - $qrange = $image->getCore()->getQuantumRange(); - - // apply - $image->getCore()->levelImage(0, $red, $qrange['quantumRangeLong'], \Imagick::CHANNEL_RED); - $image->getCore()->levelImage(0, $green, $qrange['quantumRangeLong'], \Imagick::CHANNEL_GREEN); - $image->getCore()->levelImage(0, $blue, $qrange['quantumRangeLong'], \Imagick::CHANNEL_BLUE); - - return true; - } - - private function normalizeLevel($level) - { - if ($level > 0) { - return $level/5; - } else { - return ($level+100)/100; - } - } -} diff --git a/src/Intervention/Image/Imagick/Commands/ContrastCommand.php b/src/Intervention/Image/Imagick/Commands/ContrastCommand.php deleted file mode 100644 index 113a2186c..000000000 --- a/src/Intervention/Image/Imagick/Commands/ContrastCommand.php +++ /dev/null @@ -1,19 +0,0 @@ -argument(0)->between(-100, 100)->required()->value(); - - return $image->getCore()->sigmoidalContrastImage($level > 0, $level / 4, 0); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/CropCommand.php b/src/Intervention/Image/Imagick/Commands/CropCommand.php deleted file mode 100644 index 21c7184b7..000000000 --- a/src/Intervention/Image/Imagick/Commands/CropCommand.php +++ /dev/null @@ -1,43 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $height = $this->argument(1)->type('digit')->required()->value(); - $x = $this->argument(2)->type('digit')->value(); - $y = $this->argument(3)->type('digit')->value(); - - if (is_null($width) || is_null($height)) { - throw new \Intervention\Image\Exception\InvalidArgumentException( - "Width and height of cutout needs to be defined." - ); - } - - $cropped = new Size($width, $height); - $position = new Point($x, $y); - - // align boxes - if (is_null($x) && is_null($y)) { - $position = $image->getSize()->align('center')->relativePosition($cropped->align('center')); - } - - // crop image core - $image->getCore()->cropImage($cropped->width, $cropped->height, $position->x, $position->y); - $image->getCore()->setImagePage(0,0,0,0); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/DestroyCommand.php b/src/Intervention/Image/Imagick/Commands/DestroyCommand.php deleted file mode 100644 index 28b9a4c3a..000000000 --- a/src/Intervention/Image/Imagick/Commands/DestroyCommand.php +++ /dev/null @@ -1,25 +0,0 @@ -getCore()->clear(); - - // destroy backups - foreach ($image->getBackups() as $backup) { - $backup->clear(); - } - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/ExifCommand.php b/src/Intervention/Image/Imagick/Commands/ExifCommand.php deleted file mode 100644 index 924522caf..000000000 --- a/src/Intervention/Image/Imagick/Commands/ExifCommand.php +++ /dev/null @@ -1,62 +0,0 @@ -preferExtension = false; - } - - /** - * Read Exif data from the given image - * - * @param \Intervention\Image\Image $image - * @return boolean - */ - public function execute($image) - { - if ($this->preferExtension && function_exists('exif_read_data')) { - return parent::execute($image); - } - - $core = $image->getCore(); - - if ( ! method_exists($core, 'getImageProperties')) { - throw new \Intervention\Image\Exception\NotSupportedException( - "Reading Exif data is not supported by this PHP installation." - ); - } - - $requestedKey = $this->argument(0)->value(); - if ($requestedKey !== null) { - $this->setOutput($core->getImageProperty('exif:' . $requestedKey)); - return true; - } - - $exif = []; - $properties = $core->getImageProperties(); - foreach ($properties as $key => $value) { - if (substr($key, 0, 5) !== 'exif:') { - continue; - } - - $exif[substr($key, 5)] = $value; - } - - $this->setOutput($exif); - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/FillCommand.php b/src/Intervention/Image/Imagick/Commands/FillCommand.php deleted file mode 100644 index bfac75fbf..000000000 --- a/src/Intervention/Image/Imagick/Commands/FillCommand.php +++ /dev/null @@ -1,103 +0,0 @@ -argument(0)->value(); - $x = $this->argument(1)->type('digit')->value(); - $y = $this->argument(2)->type('digit')->value(); - - $imagick = $image->getCore(); - - try { - // set image filling - $source = new Decoder; - $filling = $source->init($filling); - - } catch (\Intervention\Image\Exception\NotReadableException $e) { - - // set solid color filling - $filling = new Color($filling); - } - - // flood fill if coordinates are set - if (is_int($x) && is_int($y)) { - - // flood fill with texture - if ($filling instanceof Image) { - - // create tile - $tile = clone $image->getCore(); - - // mask away color at position - $tile->transparentPaintImage($tile->getImagePixelColor($x, $y), 0, 0, false); - - // create canvas - $canvas = clone $image->getCore(); - - // fill canvas with texture - $canvas = $canvas->textureImage($filling->getCore()); - - // merge canvas and tile - $canvas->compositeImage($tile, \Imagick::COMPOSITE_DEFAULT, 0, 0); - - // replace image core - $image->setCore($canvas); - - // flood fill with color - } elseif ($filling instanceof Color) { - - // create canvas with filling - $canvas = new \Imagick; - $canvas->newImage($image->getWidth(), $image->getHeight(), $filling->getPixel(), 'png'); - - // create tile to put on top - $tile = clone $image->getCore(); - - // mask away color at pos. - $tile->transparentPaintImage($tile->getImagePixelColor($x, $y), 0, 0, false); - - // save alpha channel of original image - $alpha = clone $image->getCore(); - - // merge original with canvas and tile - $image->getCore()->compositeImage($canvas, \Imagick::COMPOSITE_DEFAULT, 0, 0); - $image->getCore()->compositeImage($tile, \Imagick::COMPOSITE_DEFAULT, 0, 0); - - // restore alpha channel of original image - $image->getCore()->compositeImage($alpha, \Imagick::COMPOSITE_COPYOPACITY, 0, 0); - } - - } else { - - if ($filling instanceof Image) { - - // fill whole image with texture - $image->setCore($image->getCore()->textureImage($filling->getCore())); - - } elseif ($filling instanceof Color) { - - // fill whole image with color - $draw = new \ImagickDraw(); - $draw->setFillColor($filling->getPixel()); - $draw->rectangle(0, 0, $image->getWidth(), $image->getHeight()); - $image->getCore()->drawImage($draw); - } - } - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/FitCommand.php b/src/Intervention/Image/Imagick/Commands/FitCommand.php deleted file mode 100644 index f2c60d219..000000000 --- a/src/Intervention/Image/Imagick/Commands/FitCommand.php +++ /dev/null @@ -1,41 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $height = $this->argument(1)->type('digit')->value($width); - $constraints = $this->argument(2)->type('closure')->value(); - $position = $this->argument(3)->type('string')->value('center'); - - // calculate size - $cropped = $image->getSize()->fit(new Size($width, $height), $position); - $resized = clone $cropped; - $resized = $resized->resize($width, $height, $constraints); - - // crop image - $image->getCore()->cropImage( - $cropped->width, - $cropped->height, - $cropped->pivot->x, - $cropped->pivot->y - ); - - // resize image - $image->getCore()->scaleImage($resized->getWidth(), $resized->getHeight()); - $image->getCore()->setImagePage(0,0,0,0); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/FlipCommand.php b/src/Intervention/Image/Imagick/Commands/FlipCommand.php deleted file mode 100644 index 746650c1c..000000000 --- a/src/Intervention/Image/Imagick/Commands/FlipCommand.php +++ /dev/null @@ -1,25 +0,0 @@ -argument(0)->value('h'); - - if (in_array(strtolower($mode), array(2, 'v', 'vert', 'vertical'))) { - // flip vertical - return $image->getCore()->flipImage(); - } else { - // flip horizontal - return $image->getCore()->flopImage(); - } - } -} diff --git a/src/Intervention/Image/Imagick/Commands/GammaCommand.php b/src/Intervention/Image/Imagick/Commands/GammaCommand.php deleted file mode 100644 index e70cbdd36..000000000 --- a/src/Intervention/Image/Imagick/Commands/GammaCommand.php +++ /dev/null @@ -1,19 +0,0 @@ -argument(0)->type('numeric')->required()->value(); - - return $image->getCore()->gammaImage($gamma); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/GetSizeCommand.php b/src/Intervention/Image/Imagick/Commands/GetSizeCommand.php deleted file mode 100644 index 65b1078d1..000000000 --- a/src/Intervention/Image/Imagick/Commands/GetSizeCommand.php +++ /dev/null @@ -1,27 +0,0 @@ -getCore(); - - $this->setOutput(new Size( - $core->getImageWidth(), - $core->getImageHeight() - )); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/GreyscaleCommand.php b/src/Intervention/Image/Imagick/Commands/GreyscaleCommand.php deleted file mode 100644 index bb3f47260..000000000 --- a/src/Intervention/Image/Imagick/Commands/GreyscaleCommand.php +++ /dev/null @@ -1,17 +0,0 @@ -getCore()->modulateImage(100, 0, 100); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/HeightenCommand.php b/src/Intervention/Image/Imagick/Commands/HeightenCommand.php deleted file mode 100644 index 9d10973f2..000000000 --- a/src/Intervention/Image/Imagick/Commands/HeightenCommand.php +++ /dev/null @@ -1,28 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $additionalConstraints = $this->argument(1)->type('closure')->value(); - - $this->arguments[0] = null; - $this->arguments[1] = $height; - $this->arguments[2] = function ($constraint) use ($additionalConstraints) { - $constraint->aspectRatio(); - if(is_callable($additionalConstraints)) - $additionalConstraints($constraint); - }; - - return parent::execute($image); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/InsertCommand.php b/src/Intervention/Image/Imagick/Commands/InsertCommand.php deleted file mode 100644 index 542feb2ae..000000000 --- a/src/Intervention/Image/Imagick/Commands/InsertCommand.php +++ /dev/null @@ -1,31 +0,0 @@ -argument(0)->required()->value(); - $position = $this->argument(1)->type('string')->value(); - $x = $this->argument(2)->type('digit')->value(0); - $y = $this->argument(3)->type('digit')->value(0); - - // build watermark - $watermark = $image->getDriver()->init($source); - - // define insertion point - $image_size = $image->getSize()->align($position, $x, $y); - $watermark_size = $watermark->getSize()->align($position); - $target = $image_size->relativePosition($watermark_size); - - // insert image at position - return $image->getCore()->compositeImage($watermark->getCore(), \Imagick::COMPOSITE_DEFAULT, $target->x, $target->y); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/InterlaceCommand.php b/src/Intervention/Image/Imagick/Commands/InterlaceCommand.php deleted file mode 100644 index 82cddd4c6..000000000 --- a/src/Intervention/Image/Imagick/Commands/InterlaceCommand.php +++ /dev/null @@ -1,27 +0,0 @@ -argument(0)->type('bool')->value(true); - - if ($mode) { - $mode = \Imagick::INTERLACE_LINE; - } else { - $mode = \Imagick::INTERLACE_NO; - } - - $image->getCore()->setInterlaceScheme($mode); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/InvertCommand.php b/src/Intervention/Image/Imagick/Commands/InvertCommand.php deleted file mode 100644 index 125fbddee..000000000 --- a/src/Intervention/Image/Imagick/Commands/InvertCommand.php +++ /dev/null @@ -1,17 +0,0 @@ -getCore()->negateImage(false); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/LimitColorsCommand.php b/src/Intervention/Image/Imagick/Commands/LimitColorsCommand.php deleted file mode 100644 index 7308180f6..000000000 --- a/src/Intervention/Image/Imagick/Commands/LimitColorsCommand.php +++ /dev/null @@ -1,57 +0,0 @@ -argument(0)->value(); - $matte = $this->argument(1)->value(); - - // get current image size - $size = $image->getSize(); - - // build 2 color alpha mask from original alpha - $alpha = clone $image->getCore(); - $alpha->separateImageChannel(\Imagick::CHANNEL_ALPHA); - $alpha->transparentPaintImage('#ffffff', 0, 0, false); - $alpha->separateImageChannel(\Imagick::CHANNEL_ALPHA); - $alpha->negateImage(false); - - if ($matte) { - - // get matte color - $mattecolor = $image->getDriver()->parseColor($matte)->getPixel(); - - // create matte image - $canvas = new \Imagick; - $canvas->newImage($size->width, $size->height, $mattecolor, 'png'); - - // lower colors of original and copy to matte - $image->getCore()->quantizeImage($count, \Imagick::COLORSPACE_RGB, 0, false, false); - $canvas->compositeImage($image->getCore(), \Imagick::COMPOSITE_DEFAULT, 0, 0); - - // copy new alpha to canvas - $canvas->compositeImage($alpha, \Imagick::COMPOSITE_COPYOPACITY, 0, 0); - - // replace core - $image->setCore($canvas); - - } else { - - $image->getCore()->quantizeImage($count, \Imagick::COLORSPACE_RGB, 0, false, false); - $image->getCore()->compositeImage($alpha, \Imagick::COMPOSITE_COPYOPACITY, 0, 0); - - } - - return true; - - } -} diff --git a/src/Intervention/Image/Imagick/Commands/MaskCommand.php b/src/Intervention/Image/Imagick/Commands/MaskCommand.php deleted file mode 100644 index 2dfc697b0..000000000 --- a/src/Intervention/Image/Imagick/Commands/MaskCommand.php +++ /dev/null @@ -1,58 +0,0 @@ -argument(0)->value(); - $mask_w_alpha = $this->argument(1)->type('bool')->value(false); - - // get imagick - $imagick = $image->getCore(); - - // build mask image from source - $mask = $image->getDriver()->init($mask_source); - - // resize mask to size of current image (if necessary) - $image_size = $image->getSize(); - if ($mask->getSize() != $image_size) { - $mask->resize($image_size->width, $image_size->height); - } - - $imagick->setImageMatte(true); - - if ($mask_w_alpha) { - - // just mask with alpha map - $imagick->compositeImage($mask->getCore(), \Imagick::COMPOSITE_DSTIN, 0, 0); - - } else { - - // get alpha channel of original as greyscale image - $original_alpha = clone $imagick; - $original_alpha->separateImageChannel(\Imagick::CHANNEL_ALPHA); - - // use red channel from mask ask alpha - $mask_alpha = clone $mask->getCore(); - $mask_alpha->compositeImage($mask->getCore(), \Imagick::COMPOSITE_DEFAULT, 0, 0); - // $mask_alpha->setImageAlphaChannel(\Imagick::ALPHACHANNEL_DEACTIVATE); - $mask_alpha->separateImageChannel(\Imagick::CHANNEL_ALL); - - // combine both alphas from original and mask - $original_alpha->compositeImage($mask_alpha, \Imagick::COMPOSITE_COPYOPACITY, 0, 0); - - // mask the image with the alpha combination - $imagick->compositeImage($original_alpha, \Imagick::COMPOSITE_DSTIN, 0, 0); - } - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/OpacityCommand.php b/src/Intervention/Image/Imagick/Commands/OpacityCommand.php deleted file mode 100644 index 57ed006b8..000000000 --- a/src/Intervention/Image/Imagick/Commands/OpacityCommand.php +++ /dev/null @@ -1,21 +0,0 @@ -argument(0)->between(0, 100)->required()->value(); - - $transparency = $transparency > 0 ? (100 / $transparency) : 1000; - - return $image->getCore()->evaluateImage(\Imagick::EVALUATE_DIVIDE, $transparency, \Imagick::CHANNEL_ALPHA); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/PickColorCommand.php b/src/Intervention/Image/Imagick/Commands/PickColorCommand.php deleted file mode 100644 index 8daa0f95a..000000000 --- a/src/Intervention/Image/Imagick/Commands/PickColorCommand.php +++ /dev/null @@ -1,29 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $y = $this->argument(1)->type('digit')->required()->value(); - $format = $this->argument(2)->type('string')->value('array'); - - // pick color - $color = new Color($image->getCore()->getImagePixelColor($x, $y)); - - // format to output - $this->setOutput($color->format($format)); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/PixelCommand.php b/src/Intervention/Image/Imagick/Commands/PixelCommand.php deleted file mode 100644 index b9e6d395d..000000000 --- a/src/Intervention/Image/Imagick/Commands/PixelCommand.php +++ /dev/null @@ -1,30 +0,0 @@ -argument(0)->required()->value(); - $color = new Color($color); - $x = $this->argument(1)->type('digit')->required()->value(); - $y = $this->argument(2)->type('digit')->required()->value(); - - // prepare pixel - $draw = new \ImagickDraw; - $draw->setFillColor($color->getPixel()); - $draw->point($x, $y); - - // apply pixel - return $image->getCore()->drawImage($draw); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/PixelateCommand.php b/src/Intervention/Image/Imagick/Commands/PixelateCommand.php deleted file mode 100644 index 75f2218f5..000000000 --- a/src/Intervention/Image/Imagick/Commands/PixelateCommand.php +++ /dev/null @@ -1,25 +0,0 @@ -argument(0)->type('digit')->value(10); - - $width = $image->getWidth(); - $height = $image->getHeight(); - - $image->getCore()->scaleImage(max(1, ($width / $size)), max(1, ($height / $size))); - $image->getCore()->scaleImage($width, $height); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/ResetCommand.php b/src/Intervention/Image/Imagick/Commands/ResetCommand.php deleted file mode 100644 index ee5a2cdf3..000000000 --- a/src/Intervention/Image/Imagick/Commands/ResetCommand.php +++ /dev/null @@ -1,37 +0,0 @@ -argument(0)->value(); - - $backup = $image->getBackup($backupName); - - if ($backup instanceof \Imagick) { - - // destroy current core - $image->getCore()->clear(); - - // clone backup - $backup = clone $backup; - - // reset to new resource - $image->setCore($backup); - - return true; - } - - throw new \Intervention\Image\Exception\RuntimeException( - "Backup not available. Call backup({$backupName}) before reset()." - ); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/ResizeCanvasCommand.php b/src/Intervention/Image/Imagick/Commands/ResizeCanvasCommand.php deleted file mode 100644 index 8884230eb..000000000 --- a/src/Intervention/Image/Imagick/Commands/ResizeCanvasCommand.php +++ /dev/null @@ -1,89 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $height = $this->argument(1)->type('digit')->required()->value(); - $anchor = $this->argument(2)->value('center'); - $relative = $this->argument(3)->type('boolean')->value(); - $bgcolor = $this->argument(4)->value(); - - $original_width = $image->getWidth(); - $original_height = $image->getHeight(); - - // check of only width or height is set - $width = is_null($width) ? $original_width : intval($width); - $height = is_null($height) ? $original_height : intval($height); - - // check on relative width/height - if ($relative) { - $width = $original_width + $width; - $height = $original_height + $height; - } - - // check for negative width/height - $width = ($width <= 0) ? $width + $original_width : $width; - $height = ($height <= 0) ? $height + $original_height : $height; - - // create new canvas - $canvas = $image->getDriver()->newImage($width, $height, $bgcolor); - - // set copy position - $canvas_size = $canvas->getSize()->align($anchor); - $image_size = $image->getSize()->align($anchor); - $canvas_pos = $image_size->relativePosition($canvas_size); - $image_pos = $canvas_size->relativePosition($image_size); - - if ($width <= $original_width) { - $dst_x = 0; - $src_x = $canvas_pos->x; - $src_w = $canvas_size->width; - } else { - $dst_x = $image_pos->x; - $src_x = 0; - $src_w = $original_width; - } - - if ($height <= $original_height) { - $dst_y = 0; - $src_y = $canvas_pos->y; - $src_h = $canvas_size->height; - } else { - $dst_y = $image_pos->y; - $src_y = 0; - $src_h = $original_height; - } - - // make image area transparent to keep transparency - // even if background-color is set - $rect = new \ImagickDraw; - $fill = $canvas->pickColor(0, 0, 'hex'); - $fill = $fill == '#ff0000' ? '#00ff00' : '#ff0000'; - $rect->setFillColor($fill); - $rect->rectangle($dst_x, $dst_y, $dst_x + $src_w - 1, $dst_y + $src_h - 1); - $canvas->getCore()->drawImage($rect); - $canvas->getCore()->transparentPaintImage($fill, 0, 0, false); - - $canvas->getCore()->setImageColorspace($image->getCore()->getImageColorspace()); - - // copy image into new canvas - $image->getCore()->cropImage($src_w, $src_h, $src_x, $src_y); - $canvas->getCore()->compositeImage($image->getCore(), \Imagick::COMPOSITE_DEFAULT, $dst_x, $dst_y); - $canvas->getCore()->setImagePage(0,0,0,0); - - // set new core to canvas - $image->setCore($canvas->getCore()); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/ResizeCommand.php b/src/Intervention/Image/Imagick/Commands/ResizeCommand.php deleted file mode 100644 index 9ccc202c2..000000000 --- a/src/Intervention/Image/Imagick/Commands/ResizeCommand.php +++ /dev/null @@ -1,27 +0,0 @@ -argument(0)->value(); - $height = $this->argument(1)->value(); - $constraints = $this->argument(2)->type('closure')->value(); - - // resize box - $resized = $image->getSize()->resize($width, $height, $constraints); - - // modify image - $image->getCore()->scaleImage($resized->getWidth(), $resized->getHeight()); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/RotateCommand.php b/src/Intervention/Image/Imagick/Commands/RotateCommand.php deleted file mode 100644 index 3d0eb99cc..000000000 --- a/src/Intervention/Image/Imagick/Commands/RotateCommand.php +++ /dev/null @@ -1,26 +0,0 @@ -argument(0)->type('numeric')->required()->value(); - $color = $this->argument(1)->value(); - $color = new Color($color); - - // rotate image - $image->getCore()->rotateImage($color->getPixel(), ($angle * -1)); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/SharpenCommand.php b/src/Intervention/Image/Imagick/Commands/SharpenCommand.php deleted file mode 100644 index 4f2fc8c29..000000000 --- a/src/Intervention/Image/Imagick/Commands/SharpenCommand.php +++ /dev/null @@ -1,19 +0,0 @@ -argument(0)->between(0, 100)->value(10); - - return $image->getCore()->unsharpMaskImage(1, 1, $amount / 6.25, 0); - } -} diff --git a/src/Intervention/Image/Imagick/Commands/TrimCommand.php b/src/Intervention/Image/Imagick/Commands/TrimCommand.php deleted file mode 100644 index f2ee46af4..000000000 --- a/src/Intervention/Image/Imagick/Commands/TrimCommand.php +++ /dev/null @@ -1,120 +0,0 @@ -argument(0)->type('string')->value(); - $away = $this->argument(1)->value(); - $tolerance = $this->argument(2)->type('numeric')->value(0); - $feather = $this->argument(3)->type('numeric')->value(0); - - $width = $image->getWidth(); - $height = $image->getHeight(); - - $checkTransparency = false; - - // define borders to trim away - if (is_null($away)) { - $away = array('top', 'right', 'bottom', 'left'); - } elseif (is_string($away)) { - $away = array($away); - } - - // lower border names - foreach ($away as $key => $value) { - $away[$key] = strtolower($value); - } - - // define base color position - switch (strtolower($base)) { - case 'transparent': - case 'trans': - $checkTransparency = true; - $base_x = 0; - $base_y = 0; - break; - - case 'bottom-right': - case 'right-bottom': - $base_x = $width - 1; - $base_y = $height - 1; - break; - - default: - case 'top-left': - case 'left-top': - $base_x = 0; - $base_y = 0; - break; - } - - // pick base color - if ($checkTransparency) { - $base_color = new Color; // color will only be used to compare alpha channel - } else { - $base_color = $image->pickColor($base_x, $base_y, 'object'); - } - - // trim on clone to get only coordinates - $trimed = clone $image->getCore(); - - // add border to trim specific color - $trimed->borderImage($base_color->getPixel(), 1, 1); - - // trim image - $trimed->trimImage(65850 / 100 * $tolerance); - - // get coordinates of trim - $imagePage = $trimed->getImagePage(); - list($crop_x, $crop_y) = array($imagePage['x']-1, $imagePage['y']-1); - // $trimed->setImagePage(0, 0, 0, 0); - list($crop_width, $crop_height) = array($trimed->width, $trimed->height); - - // adjust settings if right should not be trimed - if ( ! in_array('right', $away)) { - $crop_width = $crop_width + ($width - ($width - $crop_x)); - } - - // adjust settings if bottom should not be trimed - if ( ! in_array('bottom', $away)) { - $crop_height = $crop_height + ($height - ($height - $crop_y)); - } - - // adjust settings if left should not be trimed - if ( ! in_array('left', $away)) { - $crop_width = $crop_width + $crop_x; - $crop_x = 0; - } - - // adjust settings if top should not be trimed - if ( ! in_array('top', $away)) { - $crop_height = $crop_height + $crop_y; - $crop_y = 0; - } - - // add feather - $crop_width = min($width, ($crop_width + $feather * 2)); - $crop_height = min($height, ($crop_height + $feather * 2)); - $crop_x = max(0, ($crop_x - $feather)); - $crop_y = max(0, ($crop_y - $feather)); - - // finally crop based on page - $image->getCore()->cropImage($crop_width, $crop_height, $crop_x, $crop_y); - $image->getCore()->setImagePage(0,0,0,0); - - $trimed->destroy(); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Commands/WidenCommand.php b/src/Intervention/Image/Imagick/Commands/WidenCommand.php deleted file mode 100644 index 467fe800a..000000000 --- a/src/Intervention/Image/Imagick/Commands/WidenCommand.php +++ /dev/null @@ -1,28 +0,0 @@ -argument(0)->type('digit')->required()->value(); - $additionalConstraints = $this->argument(1)->type('closure')->value(); - - $this->arguments[0] = $width; - $this->arguments[1] = null; - $this->arguments[2] = function ($constraint) use ($additionalConstraints) { - $constraint->aspectRatio(); - if(is_callable($additionalConstraints)) - $additionalConstraints($constraint); - }; - - return parent::execute($image); - } -} diff --git a/src/Intervention/Image/Imagick/Decoder.php b/src/Intervention/Image/Imagick/Decoder.php deleted file mode 100644 index 6b2b40daf..000000000 --- a/src/Intervention/Image/Imagick/Decoder.php +++ /dev/null @@ -1,120 +0,0 @@ -setBackgroundColor(new \ImagickPixel('transparent')); - $core->readImage($path); - $core->setImageType(defined('\Imagick::IMGTYPE_TRUECOLORALPHA') ? \Imagick::IMGTYPE_TRUECOLORALPHA : \Imagick::IMGTYPE_TRUECOLORMATTE); - - } catch (\ImagickException $e) { - throw new \Intervention\Image\Exception\NotReadableException( - "Unable to read image from path ({$path}).", - 0, - $e - ); - } - - // build image - $image = $this->initFromImagick($core); - $image->setFileInfoFromPath($path); - - return $image; - } - - /** - * Initiates new image from GD resource - * - * @param Resource $resource - * @return \Intervention\Image\Image - */ - public function initFromGdResource($resource) - { - throw new \Intervention\Image\Exception\NotSupportedException( - 'Imagick driver is unable to init from GD resource.' - ); - } - - /** - * Initiates new image from Imagick object - * - * @param Imagick $object - * @return \Intervention\Image\Image - */ - public function initFromImagick(\Imagick $object) - { - // currently animations are not supported - // so all images are turned into static - $object = $this->removeAnimation($object); - - // reset image orientation - $object->setImageOrientation(\Imagick::ORIENTATION_UNDEFINED); - - return new Image(new Driver, $object); - } - - /** - * Initiates new image from binary data - * - * @param string $data - * @return \Intervention\Image\Image - */ - public function initFromBinary($binary) - { - $core = new \Imagick; - - try { - - $core->readImageBlob($binary); - - } catch (\ImagickException $e) { - throw new \Intervention\Image\Exception\NotReadableException( - "Unable to read image from binary data.", - 0, - $e - ); - } - - // build image - $image = $this->initFromImagick($core); - $image->mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $binary); - - return $image; - } - - /** - * Turns object into one frame Imagick object - * by removing all frames except first - * - * @param Imagick $object - * @return Imagick - */ - private function removeAnimation(\Imagick $object) - { - $imagick = new \Imagick; - - foreach ($object as $frame) { - $imagick->addImage($frame->getImage()); - break; - } - - $object->destroy(); - - return $imagick; - } -} diff --git a/src/Intervention/Image/Imagick/Driver.php b/src/Intervention/Image/Imagick/Driver.php deleted file mode 100644 index 2fbd78b82..000000000 --- a/src/Intervention/Image/Imagick/Driver.php +++ /dev/null @@ -1,70 +0,0 @@ -coreAvailable()) { - throw new \Intervention\Image\Exception\NotSupportedException( - "ImageMagick module not available with this PHP installation." - ); - } - - $this->decoder = $decoder ? $decoder : new Decoder; - $this->encoder = $encoder ? $encoder : new Encoder; - } - - /** - * Creates new image instance - * - * @param integer $width - * @param integer $height - * @param string $background - * @return \Intervention\Image\Image - */ - public function newImage($width, $height, $background = null) - { - $background = new Color($background); - - // create empty core - $core = new \Imagick; - $core->newImage($width, $height, $background->getPixel(), 'png'); - $core->setType(\Imagick::IMGTYPE_UNDEFINED); - $core->setImageType(\Imagick::IMGTYPE_UNDEFINED); - $core->setColorspace(\Imagick::COLORSPACE_UNDEFINED); - - // build image - $image = new \Intervention\Image\Image(new static, $core); - - return $image; - } - - /** - * Reads given string into color object - * - * @param string $value - * @return AbstractColor - */ - public function parseColor($value) - { - return new Color($value); - } - - /** - * Checks if core module installation is available - * - * @return boolean - */ - protected function coreAvailable() - { - return (extension_loaded('imagick') && class_exists('Imagick')); - } -} diff --git a/src/Intervention/Image/Imagick/Encoder.php b/src/Intervention/Image/Imagick/Encoder.php deleted file mode 100644 index 1c193237d..000000000 --- a/src/Intervention/Image/Imagick/Encoder.php +++ /dev/null @@ -1,146 +0,0 @@ -image->getCore(); - $imagick->setImageBackgroundColor('white'); - $imagick->setBackgroundColor('white'); - $imagick = $imagick->mergeImageLayers(\Imagick::LAYERMETHOD_MERGE); - $imagick->setFormat($format); - $imagick->setImageFormat($format); - $imagick->setCompression($compression); - $imagick->setImageCompression($compression); - $imagick->setCompressionQuality($this->quality); - $imagick->setImageCompressionQuality($this->quality); - - return $imagick->getImagesBlob(); - } - - /** - * Processes and returns encoded image as PNG string - * - * @return string - */ - protected function processPng() - { - $format = 'png'; - $compression = \Imagick::COMPRESSION_ZIP; - - $imagick = $this->image->getCore(); - $imagick->setFormat($format); - $imagick->setImageFormat($format); - $imagick->setCompression($compression); - $imagick->setImageCompression($compression); - - return $imagick->getImagesBlob(); - } - - /** - * Processes and returns encoded image as GIF string - * - * @return string - */ - protected function processGif() - { - $format = 'gif'; - $compression = \Imagick::COMPRESSION_LZW; - - $imagick = $this->image->getCore(); - $imagick->setFormat($format); - $imagick->setImageFormat($format); - $imagick->setCompression($compression); - $imagick->setImageCompression($compression); - - return $imagick->getImagesBlob(); - } - - /** - * Processes and returns encoded image as TIFF string - * - * @return string - */ - protected function processTiff() - { - $format = 'tiff'; - $compression = \Imagick::COMPRESSION_UNDEFINED; - - $imagick = $this->image->getCore(); - $imagick->setFormat($format); - $imagick->setImageFormat($format); - $imagick->setCompression($compression); - $imagick->setImageCompression($compression); - $imagick->setCompressionQuality($this->quality); - $imagick->setImageCompressionQuality($this->quality); - - return $imagick->getImagesBlob(); - } - - /** - * Processes and returns encoded image as BMP string - * - * @return string - */ - protected function processBmp() - { - $format = 'bmp'; - $compression = \Imagick::COMPRESSION_UNDEFINED; - - $imagick = $this->image->getCore(); - $imagick->setFormat($format); - $imagick->setImageFormat($format); - $imagick->setCompression($compression); - $imagick->setImageCompression($compression); - - return $imagick->getImagesBlob(); - } - - /** - * Processes and returns encoded image as ICO string - * - * @return string - */ - protected function processIco() - { - $format = 'ico'; - $compression = \Imagick::COMPRESSION_UNDEFINED; - - $imagick = $this->image->getCore(); - $imagick->setFormat($format); - $imagick->setImageFormat($format); - $imagick->setCompression($compression); - $imagick->setImageCompression($compression); - - return $imagick->getImagesBlob(); - } - - /** - * Processes and returns encoded image as PSD string - * - * @return string - */ - protected function processPsd() - { - $format = 'psd'; - $compression = \Imagick::COMPRESSION_UNDEFINED; - - $imagick = $this->image->getCore(); - $imagick->setFormat($format); - $imagick->setImageFormat($format); - $imagick->setCompression($compression); - $imagick->setImageCompression($compression); - - return $imagick->getImagesBlob(); - } -} diff --git a/src/Intervention/Image/Imagick/Font.php b/src/Intervention/Image/Imagick/Font.php deleted file mode 100644 index 9ae2f9786..000000000 --- a/src/Intervention/Image/Imagick/Font.php +++ /dev/null @@ -1,78 +0,0 @@ -setStrokeAntialias(true); - $draw->setTextAntialias(true); - - // set font file - if ($this->hasApplicableFontFile()) { - $draw->setFont($this->file); - } else { - throw new \Intervention\Image\Exception\RuntimeException( - "Font file must be provided to apply text to image." - ); - } - - // parse text color - $color = new Color($this->color); - - $draw->setFontSize($this->size); - $draw->setFillColor($color->getPixel()); - - // align horizontal - switch (strtolower($this->align)) { - case 'center': - $align = \Imagick::ALIGN_CENTER; - break; - - case 'right': - $align = \Imagick::ALIGN_RIGHT; - break; - - default: - $align = \Imagick::ALIGN_LEFT; - break; - } - - $draw->setTextAlignment($align); - - // align vertical - if (strtolower($this->valign) != 'bottom') { - - // calculate box size - $dimensions = $image->getCore()->queryFontMetrics($draw, $this->text); - - // corrections on y-position - switch (strtolower($this->valign)) { - case 'center': - case 'middle': - $posy = $posy + $dimensions['textHeight'] * 0.65 / 2; - break; - - case 'top': - $posy = $posy + $dimensions['textHeight'] * 0.65; - break; - } - } - - // apply to image - $image->getCore()->annotateImage($draw, $posx, $posy, $this->angle * (-1), $this->text); - } -} diff --git a/src/Intervention/Image/Imagick/Shapes/CircleShape.php b/src/Intervention/Image/Imagick/Shapes/CircleShape.php deleted file mode 100644 index ebf85a9e4..000000000 --- a/src/Intervention/Image/Imagick/Shapes/CircleShape.php +++ /dev/null @@ -1,40 +0,0 @@ -width = is_numeric($diameter) ? intval($diameter) : $this->diameter; - $this->height = is_numeric($diameter) ? intval($diameter) : $this->diameter; - $this->diameter = is_numeric($diameter) ? intval($diameter) : $this->diameter; - } - - /** - * Draw current circle on given image - * - * @param Image $image - * @param integer $x - * @param integer $y - * @return boolean - */ - public function applyToImage(Image $image, $x = 0, $y = 0) - { - return parent::applyToImage($image, $x, $y); - } -} diff --git a/src/Intervention/Image/Imagick/Shapes/EllipseShape.php b/src/Intervention/Image/Imagick/Shapes/EllipseShape.php deleted file mode 100644 index a5431249d..000000000 --- a/src/Intervention/Image/Imagick/Shapes/EllipseShape.php +++ /dev/null @@ -1,65 +0,0 @@ -width = is_numeric($width) ? intval($width) : $this->width; - $this->height = is_numeric($height) ? intval($height) : $this->height; - } - - /** - * Draw ellipse instance on given image - * - * @param Image $image - * @param integer $x - * @param integer $y - * @return boolean - */ - public function applyToImage(Image $image, $x = 0, $y = 0) - { - $circle = new \ImagickDraw; - - // set background - $bgcolor = new Color($this->background); - $circle->setFillColor($bgcolor->getPixel()); - - // set border - if ($this->hasBorder()) { - $border_color = new Color($this->border_color); - $circle->setStrokeWidth($this->border_width); - $circle->setStrokeColor($border_color->getPixel()); - } - - $circle->ellipse($x, $y, $this->width / 2, $this->height / 2, 0, 360); - - $image->getCore()->drawImage($circle); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Shapes/LineShape.php b/src/Intervention/Image/Imagick/Shapes/LineShape.php deleted file mode 100644 index 3e403631c..000000000 --- a/src/Intervention/Image/Imagick/Shapes/LineShape.php +++ /dev/null @@ -1,93 +0,0 @@ -x = is_numeric($x) ? intval($x) : $this->x; - $this->y = is_numeric($y) ? intval($y) : $this->y; - } - - /** - * Set current line color - * - * @param string $color - * @return void - */ - public function color($color) - { - $this->color = $color; - } - - /** - * Set current line width in pixels - * - * @param integer $width - * @return void - */ - public function width($width) - { - $this->width = $width; - } - - /** - * Draw current instance of line to given endpoint on given image - * - * @param Image $image - * @param integer $x - * @param integer $y - * @return boolean - */ - public function applyToImage(Image $image, $x = 0, $y = 0) - { - $line = new \ImagickDraw; - - $color = new Color($this->color); - $line->setStrokeColor($color->getPixel()); - $line->setStrokeWidth($this->width); - - $line->line($this->x, $this->y, $x, $y); - $image->getCore()->drawImage($line); - - return true; - } -} diff --git a/src/Intervention/Image/Imagick/Shapes/PolygonShape.php b/src/Intervention/Image/Imagick/Shapes/PolygonShape.php deleted file mode 100644 index 1fa167d5f..000000000 --- a/src/Intervention/Image/Imagick/Shapes/PolygonShape.php +++ /dev/null @@ -1,80 +0,0 @@ -points = $this->formatPoints($points); - } - - /** - * Draw polygon on given image - * - * @param Image $image - * @param integer $x - * @param integer $y - * @return boolean - */ - public function applyToImage(Image $image, $x = 0, $y = 0) - { - $polygon = new \ImagickDraw; - - // set background - $bgcolor = new Color($this->background); - $polygon->setFillColor($bgcolor->getPixel()); - - // set border - if ($this->hasBorder()) { - $border_color = new Color($this->border_color); - $polygon->setStrokeWidth($this->border_width); - $polygon->setStrokeColor($border_color->getPixel()); - } - - $polygon->polygon($this->points); - - $image->getCore()->drawImage($polygon); - - return true; - } - - /** - * Format polygon points to Imagick format - * - * @param Array $points - * @return Array - */ - private function formatPoints($points) - { - $ipoints = array(); - $count = 1; - - foreach ($points as $key => $value) { - if ($count%2 === 0) { - $y = $value; - $ipoints[] = array('x' => $x, 'y' => $y); - } else { - $x = $value; - } - $count++; - } - - return $ipoints; - } -} diff --git a/src/Intervention/Image/Imagick/Shapes/RectangleShape.php b/src/Intervention/Image/Imagick/Shapes/RectangleShape.php deleted file mode 100644 index ad23019c5..000000000 --- a/src/Intervention/Image/Imagick/Shapes/RectangleShape.php +++ /dev/null @@ -1,83 +0,0 @@ -x1 = is_numeric($x1) ? intval($x1) : $this->x1; - $this->y1 = is_numeric($y1) ? intval($y1) : $this->y1; - $this->x2 = is_numeric($x2) ? intval($x2) : $this->x2; - $this->y2 = is_numeric($y2) ? intval($y2) : $this->y2; - } - - /** - * Draw rectangle to given image at certain position - * - * @param Image $image - * @param integer $x - * @param integer $y - * @return boolean - */ - public function applyToImage(Image $image, $x = 0, $y = 0) - { - $rectangle = new \ImagickDraw; - - // set background - $bgcolor = new Color($this->background); - $rectangle->setFillColor($bgcolor->getPixel()); - - // set border - if ($this->hasBorder()) { - $border_color = new Color($this->border_color); - $rectangle->setStrokeWidth($this->border_width); - $rectangle->setStrokeColor($border_color->getPixel()); - } - - $rectangle->rectangle($this->x1, $this->y1, $this->x2, $this->y2); - - $image->getCore()->drawImage($rectangle); - - return true; - } -} diff --git a/src/Intervention/Image/Point.php b/src/Intervention/Image/Point.php deleted file mode 100644 index bb17fb7c1..000000000 --- a/src/Intervention/Image/Point.php +++ /dev/null @@ -1,64 +0,0 @@ -x = is_numeric($x) ? intval($x) : 0; - $this->y = is_numeric($y) ? intval($y) : 0; - } - - /** - * Sets X coordinate - * - * @param integer $x - */ - public function setX($x) - { - $this->x = intval($x); - } - - /** - * Sets Y coordinate - * - * @param integer $y - */ - public function setY($y) - { - $this->y = intval($y); - } - - /** - * Sets both X and Y coordinate - * - * @param integer $x - * @param integer $y - */ - public function setPosition($x, $y) - { - $this->setX($x); - $this->setY($y); - } -} diff --git a/src/Intervention/Image/Response.php b/src/Intervention/Image/Response.php deleted file mode 100644 index ce27e795c..000000000 --- a/src/Intervention/Image/Response.php +++ /dev/null @@ -1,69 +0,0 @@ -image = $image; - $this->format = $format ? $format : $image->mime; - $this->quality = $quality ? $quality : 90; - } - - /** - * Builds response according to settings - * - * @return mixed - */ - public function make() - { - $this->image->encode($this->format, $this->quality); - $data = $this->image->getEncoded(); - $mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data); - $length = strlen($data); - - if (function_exists('app') && is_a($app = app(), 'Illuminate\Foundation\Application')) { - - $response = \Illuminate\Support\Facades\Response::make($data); - $response->header('Content-Type', $mime); - $response->header('Content-Length', $length); - - } else { - - header('Content-Type: ' . $mime); - header('Content-Length: ' . $length); - $response = $data; - } - - return $response; - } -} diff --git a/src/Intervention/Image/Size.php b/src/Intervention/Image/Size.php deleted file mode 100644 index f2babe1a5..000000000 --- a/src/Intervention/Image/Size.php +++ /dev/null @@ -1,373 +0,0 @@ -width = is_numeric($width) ? intval($width) : 1; - $this->height = is_numeric($height) ? intval($height) : 1; - $this->pivot = $pivot ? $pivot : new Point; - } - - /** - * Set the width and height absolutely - * - * @param integer $width - * @param integer $height - */ - public function set($width, $height) - { - $this->width = $width; - $this->height = $height; - } - - /** - * Set current pivot point - * - * @param Point $point - */ - public function setPivot(Point $point) - { - $this->pivot = $point; - } - - /** - * Get the current width - * - * @return integer - */ - public function getWidth() - { - return $this->width; - } - - /** - * Get the current height - * - * @return integer - */ - public function getHeight() - { - return $this->height; - } - - /** - * Calculate the current aspect ratio - * - * @return float - */ - public function getRatio() - { - return $this->width / $this->height; - } - - /** - * Resize to desired width and/or height - * - * @param integer $width - * @param integer $height - * @param Closure $callback - * @return Size - */ - public function resize($width, $height, Closure $callback = null) - { - if (is_null($width) && is_null($height)) { - throw new \Intervention\Image\Exception\InvalidArgumentException( - "Width or height needs to be defined." - ); - } - - // new size with dominant width - $dominant_w_size = clone $this; - $dominant_w_size->resizeHeight($height, $callback); - $dominant_w_size->resizeWidth($width, $callback); - - // new size with dominant height - $dominant_h_size = clone $this; - $dominant_h_size->resizeWidth($width, $callback); - $dominant_h_size->resizeHeight($height, $callback); - - // decide which size to use - if ($dominant_h_size->fitsInto(new self($width, $height))) { - $this->set($dominant_h_size->width, $dominant_h_size->height); - } else { - $this->set($dominant_w_size->width, $dominant_w_size->height); - } - - return $this; - } - - /** - * Scale size according to given constraints - * - * @param integer $width - * @param Closure $callback - * @return Size - */ - private function resizeWidth($width, Closure $callback = null) - { - $constraint = $this->getConstraint($callback); - - if ($constraint->isFixed(Constraint::UPSIZE)) { - $max_width = $constraint->getSize()->getWidth(); - $max_height = $constraint->getSize()->getHeight(); - } - - if (is_numeric($width)) { - - if ($constraint->isFixed(Constraint::UPSIZE)) { - $this->width = ($width > $max_width) ? $max_width : $width; - } else { - $this->width = $width; - } - - if ($constraint->isFixed(Constraint::ASPECTRATIO)) { - $h = intval(round($this->width / $constraint->getSize()->getRatio())); - - if ($constraint->isFixed(Constraint::UPSIZE)) { - $this->height = ($h > $max_height) ? $max_height : $h; - } else { - $this->height = $h; - } - } - } - } - - /** - * Scale size according to given constraints - * - * @param integer $height - * @param Closure $callback - * @return Size - */ - private function resizeHeight($height, Closure $callback = null) - { - $constraint = $this->getConstraint($callback); - - if ($constraint->isFixed(Constraint::UPSIZE)) { - $max_width = $constraint->getSize()->getWidth(); - $max_height = $constraint->getSize()->getHeight(); - } - - if (is_numeric($height)) { - - if ($constraint->isFixed(Constraint::UPSIZE)) { - $this->height = ($height > $max_height) ? $max_height : $height; - } else { - $this->height = $height; - } - - if ($constraint->isFixed(Constraint::ASPECTRATIO)) { - $w = intval(round($this->height * $constraint->getSize()->getRatio())); - - if ($constraint->isFixed(Constraint::UPSIZE)) { - $this->width = ($w > $max_width) ? $max_width : $w; - } else { - $this->width = $w; - } - } - } - } - - /** - * Calculate the relative position to another Size - * based on the pivot point settings of both sizes. - * - * @param Size $size - * @return \Intervention\Image\Point - */ - public function relativePosition(Size $size) - { - $x = $this->pivot->x - $size->pivot->x; - $y = $this->pivot->y - $size->pivot->y; - - return new Point($x, $y); - } - - /** - * Resize given Size to best fitting size of current size. - * - * @param Size $size - * @return \Intervention\Image\Size - */ - public function fit(Size $size, $position = 'center') - { - // create size with auto height - $auto_height = clone $size; - - $auto_height->resize($this->width, null, function ($constraint) { - $constraint->aspectRatio(); - }); - - // decide which version to use - if ($auto_height->fitsInto($this)) { - - $size = $auto_height; - - } else { - - // create size with auto width - $auto_width = clone $size; - - $auto_width->resize(null, $this->height, function ($constraint) { - $constraint->aspectRatio(); - }); - - $size = $auto_width; - } - - $this->align($position); - $size->align($position); - $size->setPivot($this->relativePosition($size)); - - return $size; - } - - /** - * Checks if given size fits into current size - * - * @param Size $size - * @return boolean - */ - public function fitsInto(Size $size) - { - return ($this->width <= $size->width) && ($this->height <= $size->height); - } - - /** - * Aligns current size's pivot point to given position - * and moves point automatically by offset. - * - * @param string $position - * @param integer $offset_x - * @param integer $offset_y - * @return \Intervention\Image\Size - */ - public function align($position, $offset_x = 0, $offset_y = 0) - { - switch (strtolower($position)) { - - case 'top': - case 'top-center': - case 'top-middle': - case 'center-top': - case 'middle-top': - $x = intval($this->width / 2); - $y = 0 + $offset_y; - break; - - case 'top-right': - case 'right-top': - $x = $this->width - $offset_x; - $y = 0 + $offset_y; - break; - - case 'left': - case 'left-center': - case 'left-middle': - case 'center-left': - case 'middle-left': - $x = 0 + $offset_x; - $y = intval($this->height / 2); - break; - - case 'right': - case 'right-center': - case 'right-middle': - case 'center-right': - case 'middle-right': - $x = $this->width - $offset_x; - $y = intval($this->height / 2); - break; - - case 'bottom-left': - case 'left-bottom': - $x = 0 + $offset_x; - $y = $this->height - $offset_y; - break; - - case 'bottom': - case 'bottom-center': - case 'bottom-middle': - case 'center-bottom': - case 'middle-bottom': - $x = intval($this->width / 2); - $y = $this->height - $offset_y; - break; - - case 'bottom-right': - case 'right-bottom': - $x = $this->width - $offset_x; - $y = $this->height - $offset_y; - break; - - case 'center': - case 'middle': - case 'center-center': - case 'middle-middle': - $x = intval($this->width / 2); - $y = intval($this->height / 2); - break; - - default: - case 'top-left': - case 'left-top': - $x = 0 + $offset_x; - $y = 0 + $offset_y; - break; - } - - $this->pivot->setPosition($x, $y); - - return $this; - } - - /** - * Runs constraints on current size - * - * @param Closure $callback - * @return \Intervention\Image\Constraint - */ - private function getConstraint(Closure $callback = null) - { - $constraint = new Constraint(clone $this); - - if (is_callable($callback)) { - $callback($constraint); - } - - return $constraint; - } -} diff --git a/src/Length.php b/src/Length.php new file mode 100644 index 000000000..2d80d8872 --- /dev/null +++ b/src/Length.php @@ -0,0 +1,11 @@ +mediaType(); + } catch (NotSupportedException $e) { + throw new InvalidArgumentException( + 'Unable to create media type from ' . $identifier::class, + previous: $e + ); + } + } + + if ($identifier instanceof FileExtension) { + try { + return $identifier->mediaType(); + } catch (NotSupportedException $e) { + throw new InvalidArgumentException( + 'Unable to create media type from "' . $identifier->value . '"', + previous: $e + ); + } + } + + try { + $type = self::from(strtolower($identifier)); + } catch (Error) { + try { + $type = FileExtension::from(strtolower($identifier))->mediaType(); + } catch (Error | NotSupportedException) { + throw new InvalidArgumentException('Unable to create media type from "' . $identifier . '"'); + } + } + + return $type; + } + + /** + * Try to create media type from given identifier and return null on failure. + */ + public static function tryCreate(string|self|Format|FileExtension $identifier): ?self + { + try { + return self::create($identifier); + } catch (InvalidArgumentException) { + return null; + } + } + + /** + * Return the matching format for the current media (MIME) type. + */ + public function format(): Format + { + return match ($this) { + self::IMAGE_JPEG, + self::IMAGE_JPG, + self::IMAGE_PJPEG, + self::IMAGE_X_JPEG => Format::JPEG, + self::IMAGE_WEBP, + self::IMAGE_X_WEBP => Format::WEBP, + self::IMAGE_GIF => Format::GIF, + self::IMAGE_PNG, + self::IMAGE_X_PNG => Format::PNG, + self::IMAGE_AVIF, + self::IMAGE_X_AVIF => Format::AVIF, + self::IMAGE_BMP, + self::IMAGE_MS_BMP, + self::IMAGE_X_BITMAP, + self::IMAGE_X_BMP, + self::IMAGE_X_MS_BMP, + self::IMAGE_X_XBITMAP, + self::IMAGE_X_WINDOWS_BMP, + self::IMAGE_X_BMP3, + self::IMAGE_X_WIN_BITMAP => Format::BMP, + self::IMAGE_TIFF => Format::TIFF, + self::IMAGE_JP2, + self::IMAGE_JPX, + self::IMAGE_X_JP2_CODESTREAM, + self::IMAGE_JPM => Format::JP2, + self::IMAGE_HEIF, + self::IMAGE_HEIC, + self::IMAGE_X_HEIC => Format::HEIC, + self::IMAGE_X_ICON, + self::IMAGE_VND_MICROSOFT_ICON => Format::ICO, + }; + } + + /** + * Return the possible file extension for the current media type. + * + * @return array + */ + public function fileExtensions(): array + { + return $this->format()->fileExtensions(); + } + + /** + * Return the first file extension for the current media type. + * + * @throws NotSupportedException + */ + public function fileExtension(): FileExtension + { + return $this->format()->fileExtension(); + } +} diff --git a/src/ModifierStack.php b/src/ModifierStack.php new file mode 100644 index 000000000..a12faadb3 --- /dev/null +++ b/src/ModifierStack.php @@ -0,0 +1,43 @@ + $modifiers + */ + public function __construct(protected array $modifiers) + { + // + } + + /** + * Apply all modifiers in stack to the given image. + */ + public function apply(ImageInterface $image): ImageInterface + { + foreach ($this->modifiers as $modifier) { + $modifier->apply($image); + } + + return $image; + } + + /** + * Append new modifier to the stack. + */ + public function push(ModifierInterface $modifier): self + { + $this->modifiers[] = $modifier; + + return $this; + } +} diff --git a/src/Modifiers/AbstractDrawModifier.php b/src/Modifiers/AbstractDrawModifier.php new file mode 100644 index 000000000..168af9fa6 --- /dev/null +++ b/src/Modifiers/AbstractDrawModifier.php @@ -0,0 +1,51 @@ +driver()->decodeColor($this->drawable()->backgroundColor()); + } catch (InvalidArgumentException) { + return Color::transparent(); + } + } + + /** + * Return the border color of the object rendered by the modifier. + * + * @throws StateException + * @throws ColorDecoderException + */ + protected function borderColor(): ColorInterface + { + try { + return $this->driver()->decodeColor($this->drawable()->borderColor()); + } catch (InvalidArgumentException) { + return Color::transparent(); + } + } +} diff --git a/src/Modifiers/BlurModifier.php b/src/Modifiers/BlurModifier.php new file mode 100644 index 000000000..54796c1a2 --- /dev/null +++ b/src/Modifiers/BlurModifier.php @@ -0,0 +1,21 @@ +level < 0) { + throw new InvalidArgumentException('Invalid blur level. Only use int<0, max>'); + } + } +} diff --git a/src/Modifiers/BrightnessModifier.php b/src/Modifiers/BrightnessModifier.php new file mode 100644 index 000000000..f23e4f54f --- /dev/null +++ b/src/Modifiers/BrightnessModifier.php @@ -0,0 +1,15 @@ + 100) { + throw new InvalidArgumentException('Color level for argument $red must be in range -100 to 100'); + } + + if ($green < -100 || $green > 100) { + throw new InvalidArgumentException('Color level for argument $green must be in range -100 to 100'); + } + + if ($blue < -100 || $blue > 100) { + throw new InvalidArgumentException('Color level for argument $blue must be in range -100 to 100'); + } + } +} diff --git a/src/Modifiers/ColorspaceModifier.php b/src/Modifiers/ColorspaceModifier.php new file mode 100644 index 000000000..bbd9db937 --- /dev/null +++ b/src/Modifiers/ColorspaceModifier.php @@ -0,0 +1,59 @@ +target instanceof ColorspaceInterface) { + return $this->target; + } + + if (class_exists($this->target)) { + $colorspace = new $this->target(); + + if (!$colorspace instanceof ColorspaceInterface) { + throw new NotSupportedException( + 'Target colorspace "' . $this->target . '" is not supported by driver' + ); + } + + return $colorspace; + } + + return match (strtolower($this->target)) { + 'rgb', 'srgb', 'rgba', 'srgba' => new Rgb(), + 'cmyk' => new Cmyk(), + 'hsl' => new Hsl(), + 'hsv', 'hsb' => new Hsv(), + 'oklab' => new Oklab(), + 'oklch' => new Oklch(), + default => throw new NotSupportedException( + 'Colorspace is not supported by driver', + ), + }; + } +} diff --git a/src/Modifiers/ContainDownModifier.php b/src/Modifiers/ContainDownModifier.php new file mode 100644 index 000000000..f4e0de57b --- /dev/null +++ b/src/Modifiers/ContainDownModifier.php @@ -0,0 +1,30 @@ +size() + ->containDown( + $this->width, + $this->height + ) + ->alignPivotTo( + $this->resizeSize($image), + $this->alignment + ); + } +} diff --git a/src/Modifiers/ContainModifier.php b/src/Modifiers/ContainModifier.php new file mode 100644 index 000000000..1d606fd84 --- /dev/null +++ b/src/Modifiers/ContainModifier.php @@ -0,0 +1,66 @@ +size() + ->contain( + $this->width, + $this->height + ) + ->alignPivotTo( + $this->resizeSize($image), + $this->alignment + ); + } + + /** + * Calculate the resize target size of the contain resizing process + * + * @throws InvalidArgumentException + */ + protected function resizeSize(ImageInterface $image): SizeInterface + { + return new Size($this->width, $this->height); + } + + /** + * Return color to fill the newly created areas after resizing + * + * @throws StateException + */ + protected function backgroundColor(): ColorInterface + { + return $this->driver()->decodeColor( + $this->background ?? $this->driver()->config()->backgroundColor, + ); + } +} diff --git a/src/Modifiers/ContrastModifier.php b/src/Modifiers/ContrastModifier.php new file mode 100644 index 000000000..e0ab9105e --- /dev/null +++ b/src/Modifiers/ContrastModifier.php @@ -0,0 +1,18 @@ +size(); + $crop = new Size($this->width, $this->height); + + return $crop->contain( + $imagesize->width(), + $imagesize->height() + )->alignPivotTo($imagesize, $this->alignment); + } + + /** + * Calculate size for the resizing step of the cover modifier + */ + protected function resizeSize(SizeInterface $size): SizeInterface + { + return $size->resize($this->width, $this->height); + } +} diff --git a/src/Modifiers/CropModifier.php b/src/Modifiers/CropModifier.php new file mode 100644 index 000000000..d587728fc --- /dev/null +++ b/src/Modifiers/CropModifier.php @@ -0,0 +1,57 @@ +width, $this->height); + $crop->movePivot($this->alignment); + + return $crop->alignPivotTo( + $image->size(), + $this->alignment + ); + } + + /** + * Return color to fill the newly created areas after resizing + * + * @throws StateException + */ + protected function backgroundColor(): ColorInterface + { + return $this->driver()->decodeColor( + $this->background ?? $this->driver()->config()->backgroundColor, + ); + } +} diff --git a/src/Modifiers/DrawBezierModifier.php b/src/Modifiers/DrawBezierModifier.php new file mode 100644 index 000000000..45f85a38a --- /dev/null +++ b/src/Modifiers/DrawBezierModifier.php @@ -0,0 +1,27 @@ +drawable; + } +} diff --git a/src/Modifiers/DrawEllipseModifier.php b/src/Modifiers/DrawEllipseModifier.php new file mode 100644 index 000000000..85b5c2920 --- /dev/null +++ b/src/Modifiers/DrawEllipseModifier.php @@ -0,0 +1,24 @@ +drawable; + } +} diff --git a/src/Modifiers/DrawLineModifier.php b/src/Modifiers/DrawLineModifier.php new file mode 100644 index 000000000..7b668219e --- /dev/null +++ b/src/Modifiers/DrawLineModifier.php @@ -0,0 +1,27 @@ +drawable; + } +} diff --git a/src/Modifiers/DrawPixelModifier.php b/src/Modifiers/DrawPixelModifier.php new file mode 100644 index 000000000..0830032f4 --- /dev/null +++ b/src/Modifiers/DrawPixelModifier.php @@ -0,0 +1,35 @@ +driver()->decodeColor($this->color); + } +} diff --git a/src/Modifiers/DrawPolygonModifier.php b/src/Modifiers/DrawPolygonModifier.php new file mode 100644 index 000000000..6c83746b1 --- /dev/null +++ b/src/Modifiers/DrawPolygonModifier.php @@ -0,0 +1,30 @@ +count() < 3) { + throw new InvalidArgumentException('The polygon must have at least 3 points'); + } + } + + /** + * Return object to be drawn. + */ + protected function drawable(): DrawableInterface + { + return $this->drawable; + } +} diff --git a/src/Modifiers/DrawRectangleModifier.php b/src/Modifiers/DrawRectangleModifier.php new file mode 100644 index 000000000..3ed307ddf --- /dev/null +++ b/src/Modifiers/DrawRectangleModifier.php @@ -0,0 +1,27 @@ +drawable; + } +} diff --git a/src/Modifiers/FillModifier.php b/src/Modifiers/FillModifier.php new file mode 100644 index 000000000..299ccf248 --- /dev/null +++ b/src/Modifiers/FillModifier.php @@ -0,0 +1,40 @@ +position instanceof PointInterface; + } + + /** + * Return filling color object: + * + * @throws ColorDecoderException + * @throws StateException + */ + protected function color(): ColorInterface + { + return $this->driver()->decodeColor($this->color); + } +} diff --git a/src/Modifiers/FillTransparentAreasModifier.php b/src/Modifiers/FillTransparentAreasModifier.php new file mode 100644 index 000000000..196d25eff --- /dev/null +++ b/src/Modifiers/FillTransparentAreasModifier.php @@ -0,0 +1,30 @@ +decodeColor( + $this->color !== null ? $this->color : $driver->config()->backgroundColor + ); + } +} diff --git a/src/Modifiers/FlipModifier.php b/src/Modifiers/FlipModifier.php new file mode 100644 index 000000000..2655ccddb --- /dev/null +++ b/src/Modifiers/FlipModifier.php @@ -0,0 +1,16 @@ +transparency < 0 || $this->transparency > 1) { + throw new InvalidArgumentException('Transparency must be in range 0 to 1'); + } + } + + /** + * Calculate position of the watermark to be inserted on the image. + */ + public function position(ImageInterface $image, ImageInterface $watermark): PointInterface + { + $imageSize = $image->size()->movePivot( + $this->alignment, + $this->x, + $this->y + ); + + $watermarkSize = $watermark->size()->movePivot( + $this->alignment + ); + + return $imageSize->offsetTo($watermarkSize); + } +} diff --git a/src/Modifiers/InvertModifier.php b/src/Modifiers/InvertModifier.php new file mode 100644 index 000000000..2c596906a --- /dev/null +++ b/src/Modifiers/InvertModifier.php @@ -0,0 +1,12 @@ +size < 1) { + throw new InvalidArgumentException('Invalid pixelation size. Only use int<1, max>'); + } + } +} diff --git a/src/Modifiers/ProfileModifier.php b/src/Modifiers/ProfileModifier.php new file mode 100644 index 000000000..f784c95ce --- /dev/null +++ b/src/Modifiers/ProfileModifier.php @@ -0,0 +1,16 @@ +limit < 1) { + throw new InvalidArgumentException('Invalid color limit. Only use int<1, max>'); + } + } + + /** + * Return color in colorspace of image to fill transparent areas. + * + * @throws StateException + */ + protected function backgroundColor(ImageInterface $image): ColorInterface + { + return $this->driver()->decodeColor( + $this->background ?? $this->driver()->config()->backgroundColor, + )->toColorspace($image->colorspace()); + } +} diff --git a/src/Modifiers/RemoveAnimationModifier.php b/src/Modifiers/RemoveAnimationModifier.php new file mode 100644 index 000000000..0769b00a1 --- /dev/null +++ b/src/Modifiers/RemoveAnimationModifier.php @@ -0,0 +1,59 @@ +position) && $this->position < 0) { + throw new InvalidArgumentException('Invalid position argument. Only use int<0, max>'); + } + } + + /** + * @throws InvalidArgumentException + */ + protected function selectedFrame(ImageInterface $image): FrameInterface + { + return $image->core()->frame($this->normalizePosition($image)); + } + + /** + * Return the position of the selected frame as integer. + * + * @throws InvalidArgumentException + */ + protected function normalizePosition(ImageInterface $image): int + { + if (is_int($this->position)) { + return $this->position; + } + + if (is_numeric($this->position)) { + return (int) $this->position; + } + + // calculate position from percentage value + if (preg_match("/^(?P[0-9]{1,3})%$/", $this->position, $matches) !== 1) { + throw new InvalidArgumentException( + 'Position must be either integer or a percent value as string' + ); + } + + $total = count($image); + $position = intval(round($total / 100 * intval($matches['percent']))); + + return $position === $total ? $position - 1 : $position; + } +} diff --git a/src/Modifiers/RemoveProfileModifier.php b/src/Modifiers/RemoveProfileModifier.php new file mode 100644 index 000000000..db81fab9b --- /dev/null +++ b/src/Modifiers/RemoveProfileModifier.php @@ -0,0 +1,12 @@ + new Size( + is_null($this->width) ? $image->width() : $image->width() + $this->width, + is_null($this->height) ? $image->height() : $image->height() + $this->height, + ), + default => new Size( + is_null($this->width) ? $image->width() : $this->width, + is_null($this->height) ? $image->height() : $this->height, + ), + }; + + return $size->alignPivotTo($image->size(), $this->alignment); + } + + /** + * Return color to fill the newly created areas after rotation. + * + * @throws StateException + */ + protected function backgroundColor(): ColorInterface + { + return $this->driver()->decodeColor( + $this->background ?? $this->driver()->config()->backgroundColor, + ); + } +} diff --git a/src/Modifiers/ResizeCanvasRelativeModifier.php b/src/Modifiers/ResizeCanvasRelativeModifier.php new file mode 100644 index 000000000..bcc5fa80b --- /dev/null +++ b/src/Modifiers/ResizeCanvasRelativeModifier.php @@ -0,0 +1,10 @@ +angle, 360); + } + + /** + * Return color to fill the newly created areas after rotation. + * + * @throws StateException + */ + protected function backgroundColor(): ColorInterface + { + return $this->driver()->decodeColor( + $this->background ?? $this->driver()->config()->backgroundColor, + ); + } +} diff --git a/src/Modifiers/ScaleDownModifier.php b/src/Modifiers/ScaleDownModifier.php new file mode 100644 index 000000000..8fdfa9bcd --- /dev/null +++ b/src/Modifiers/ScaleDownModifier.php @@ -0,0 +1,10 @@ +level < 0) { + throw new InvalidArgumentException('Invalid sharpening level. Only use int<0, max>'); + } + } +} diff --git a/src/Modifiers/SliceAnimationModifier.php b/src/Modifiers/SliceAnimationModifier.php new file mode 100644 index 000000000..02b1f2f59 --- /dev/null +++ b/src/Modifiers/SliceAnimationModifier.php @@ -0,0 +1,21 @@ +length !== null && $this->length <= 0) { + throw new InvalidArgumentException('Length must be greater than or equal to 1'); + } + } +} diff --git a/src/Modifiers/TextModifier.php b/src/Modifiers/TextModifier.php new file mode 100644 index 000000000..207b378c9 --- /dev/null +++ b/src/Modifiers/TextModifier.php @@ -0,0 +1,89 @@ +driver()->decodeColor($this->font->color()); + + if ($this->font->hasStrokeEffect() && $color->isTransparent()) { + throw new StateException( + 'The text color must be fully opaque when using the stroke effect' + ); + } + + return $color; + } + + /** + * Decode outline stroke color. + * + * @throws StateException + */ + protected function strokeColor(): ColorInterface + { + $color = $this->driver()->decodeColor($this->font->strokeColor()); + + if ($color->isTransparent()) { + throw new StateException( + 'The stroke color must be fully opaque' + ); + } + + return $color; + } + + /** + * Return array of offset points to draw text stroke effect below the actual text. + * + * @return array + */ + protected function strokeOffsets(FontInterface $font): array + { + $offsets = []; + + if ($font->strokeWidth() <= 0) { + return $offsets; + } + + for ($x = $font->strokeWidth() * -1; $x <= $font->strokeWidth(); $x++) { + for ($y = $font->strokeWidth() * -1; $y <= $font->strokeWidth(); $y++) { + $offsets[] = new Point($x, $y); + } + } + + return $offsets; + } +} diff --git a/src/Modifiers/TrimModifier.php b/src/Modifiers/TrimModifier.php new file mode 100644 index 000000000..c63a18971 --- /dev/null +++ b/src/Modifiers/TrimModifier.php @@ -0,0 +1,23 @@ +tolerance < 0) { + throw new InvalidArgumentException('Invalid trim tolerance. Only use int<0, max>'); + } + } +} diff --git a/src/Origin.php b/src/Origin.php new file mode 100644 index 000000000..6567c5cb7 --- /dev/null +++ b/src/Origin.php @@ -0,0 +1,113 @@ +mediaType; + } + + /** + * @see self::mediaType() + */ + public function mimetype(): string + { + return $this->mediaType(); + } + + /** + * {@inheritdoc} + * + * @see OriginInterface::setMediaType() + */ + public function setMediaType(string|MediaType $type): self + { + $this->mediaType = is_string($type) ? $type : $type->value; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see OriginInterface::filePath() + */ + public function filePath(): ?string + { + return $this->filePath; + } + + /** + * {@inheritdoc} + * + * @see OriginInterface::setFilePath() + */ + public function setFilePath(string $path): self + { + $this->filePath = $path; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see OriginInterface::fileExtension() + */ + public function fileExtension(): ?string + { + return pathinfo($this->filePath ?: '', PATHINFO_EXTENSION) ?: null; + } + + /** + * {@inheritdoc} + * + * @see OriginInterface::format() + * + * @throws NotSupportedException + */ + public function format(): Format + { + try { + return MediaType::create($this->mediaType())->format(); + } catch (InvalidArgumentException) { + throw new NotSupportedException('Media type "' . $this->mediaType() . '" is not supported'); + } + } + + /** + * Show debug info for the current image. + * + * @return array + */ + public function __debugInfo(): array + { + return [ + 'mediaType' => $this->mediaType(), + 'filePath' => $this->filePath(), + ]; + } +} diff --git a/src/Resolution.php b/src/Resolution.php new file mode 100644 index 000000000..67a27b2f0 --- /dev/null +++ b/src/Resolution.php @@ -0,0 +1,169 @@ + + */ +class Resolution implements ResolutionInterface, Stringable, IteratorAggregate +{ + /** + * Create new instance. + * + * @throws InvalidArgumentException + */ + public function __construct( + protected float $x, + protected float $y, + protected Length $length = Length::INCH, + ) { + if ($x < 0) { + throw new InvalidArgumentException( + 'The value of the X-axis for ' . $this::class . ' must be greater or equal to 0', + ); + } + + if ($y < 0) { + throw new InvalidArgumentException( + 'The value of the Y-axis for ' . $this::class . ' must be greater or equal to 0', + ); + } + } + + /** + * {@inheritdoc} + * + * @see ResolutionInterface::dpi() + * + * @throws InvalidArgumentException + */ + public static function dpi(float $x, float $y): ResolutionInterface + { + return new self($x, $y, Length::INCH); + } + + /** + * {@inheritdoc} + * + * @see ResolutionInterface::ppi() + * + * @throws InvalidArgumentException + */ + public static function ppi(float $x, float $y): ResolutionInterface + { + return new self($x, $y, Length::CM); + } + + /** + * {@inheritdoc} + * + * @see IteratorAggregate::getIterator() + */ + public function getIterator(): Traversable + { + return new ArrayIterator([$this->x, $this->y]); + } + + /** + * {@inheritdoc} + * + * @see ResolutionInterface::x() + */ + public function x(): float + { + return $this->x; + } + + /** + * {@inheritdoc} + * + * @see ResolutionInterface::y() + */ + public function y(): float + { + return $this->y; + } + + /** + * {@inheritdoc} + * + * @see ResolutionInterface::length() + */ + public function length(): Length + { + return $this->length; + } + + /** + * {@inheritdoc} + * + * @see ResolutionInterface::perInch() + */ + public function perInch(): self + { + return match ($this->length) { + // @phpstan-ignore missingType.checkedException + Length::CM => new self( + $this->x * 2.54, + $this->y * 2.54, + Length::INCH, + ), + default => $this + }; + } + + /** + * {@inheritdoc} + * + * @see ResolutionInterface::perCm() + */ + public function perCm(): self + { + return match ($this->length) { + // @phpstan-ignore missingType.checkedException + Length::INCH => new self( + $this->x / 2.54, + $this->y / 2.54, + Length::CM, + ), + default => $this, + }; + } + + /** + * {@inheritdoc} + * + * @see ResolutionInterface::toString() + */ + public function toString(): string + { + return sprintf( + "%1\$.2f x %2\$.2f %3\$s", + $this->x, + $this->y, + match ($this->length) { + Length::INCH => 'dpi', + Length::CM => 'dpcm', + }, + ); + } + + /** + * {@inheritdoc} + * + * @see ResolutionInterface::__toString() + */ + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/src/Size.php b/src/Size.php new file mode 100644 index 000000000..c032a1493 --- /dev/null +++ b/src/Size.php @@ -0,0 +1,436 @@ +pivot->x(), $this->pivot->y()), + new Point($this->pivot->x() + $width, $this->pivot->y()), + new Point($this->pivot->x() + $width, $this->pivot->y() - $height), + new Point($this->pivot->x(), $this->pivot->y() - $height), + ], $pivot); + } + + /** + * Create size statically. + * + * @throws InvalidArgumentException + */ + public static function create(int $width, int $height, PointInterface $pivot = new Point()): self + { + return new self($width, $height, $pivot); + } + + /** + * Set size of rectangle. + */ + public function setSize(int $width, int $height): self + { + return $this->setWidth($width)->setHeight($height); + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::setWidth() + */ + public function setWidth(int $width): self + { + $this[1]->setX($this[0]->x() + $width); + $this[2]->setX($this[3]->x() + $width); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::setHeight() + */ + public function setHeight(int $height): self + { + $this[2]->setY($this[1]->y() + $height); + $this[3]->setY($this[0]->y() + $height); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::pivot() + */ + public function pivot(): PointInterface + { + return $this->pivot; + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::setPivot() + */ + public function setPivot(PointInterface $pivot): self + { + $this->pivot = $pivot; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see DrawableInterface::setPosition() + */ + public function setPosition(PointInterface $position): self + { + parent::setPosition($position); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::movePivot() + * + * @throws InvalidArgumentException + */ + public function movePivot(string|Alignment $alignment, int $x = 0, int $y = 0): self + { + $alignment = Alignment::create($alignment); // normalize alignment + + $point = match ($alignment) { + Alignment::TOP => new Point( + intval(round($this->width() / 2)) + $x, + $y, + ), + Alignment::TOP_RIGHT => new Point( + $this->width() - $x, + $y, + ), + Alignment::LEFT => new Point( + $x, + intval(round($this->height() / 2)) + $y, + ), + Alignment::RIGHT => new Point( + $this->width() - $x, + intval(round($this->height() / 2)) + $y, + ), + Alignment::BOTTOM_LEFT => new Point( + $x, + $this->height() - $y, + ), + Alignment::BOTTOM => new Point( + intval(round($this->width() / 2)) + $x, + $this->height() - $y, + ), + Alignment::BOTTOM_RIGHT => new Point( + $this->width() - $x, + $this->height() - $y, + ), + Alignment::CENTER => new Point( + intval(round($this->width() / 2)) + $x, + intval(round($this->height() / 2)) + $y, + ), + Alignment::TOP_LEFT => new Point( + $x, + $y, + ), + }; + + $this->pivot->setPosition(...$point); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::alignPivotTo() + * + * @throws InvalidArgumentException + */ + public function alignPivotTo(SizeInterface $size, string|Alignment $alignment): self + { + $reference = new self($size->width(), $size->height()); + $reference->movePivot($alignment); + + $this->movePivot($alignment)->setPivot( + $reference->offsetTo($this) + ); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::offsetTo() + */ + public function offsetTo(SizeInterface $rectangle): PointInterface + { + return new Point( + $this->pivot()->x() - $rectangle->pivot()->x(), + $this->pivot()->y() - $rectangle->pivot()->y() + ); + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::aspectRatio() + * + * @throws RuntimeException + */ + public function aspectRatio(): float + { + try { + return $this->width() / $this->height(); + } catch (DivisionByZeroError) { + throw new RuntimeException('Division by zero'); + } + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::fitsWithin() + */ + public function fitsWithin(SizeInterface $size): bool + { + if ($this->width() > $size->width()) { + return false; + } + + if ($this->height() > $size->height()) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::isLandscape() + */ + public function isLandscape(): bool + { + return $this->width() > $this->height(); + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::isPortrait() + */ + public function isPortrait(): bool + { + return $this->width() < $this->height(); + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::resize() + * + * @throws InvalidArgumentException + */ + public function resize(?int $width = null, ?int $height = null): SizeInterface + { + try { + return $this->resizer($width, $height)->resize($this); + } catch (InvalidArgumentException $e) { + throw new InvalidArgumentException( + 'Invalid target size ' . $width . 'x' . $height, + previous: $e, + ); + } + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::resizeDown() + * + * @throws InvalidArgumentException + */ + public function resizeDown(?int $width = null, ?int $height = null): SizeInterface + { + try { + return $this->resizer($width, $height)->resizeDown($this); + } catch (InvalidArgumentException $e) { + throw new InvalidArgumentException( + 'Invalid target size ' . $width . 'x' . $height, + previous: $e, + ); + } + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::scale() + * + * @throws InvalidArgumentException + */ + public function scale(?int $width = null, ?int $height = null): SizeInterface + { + try { + return $this->resizer($width, $height)->scale($this); + } catch (InvalidArgumentException $e) { + throw new InvalidArgumentException( + 'Invalid target size ' . $width . 'x' . $height, + previous: $e, + ); + } + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::scaleDown() + * + * @throws InvalidArgumentException + */ + public function scaleDown(?int $width = null, ?int $height = null): SizeInterface + { + try { + return $this->resizer($width, $height)->scaleDown($this); + } catch (InvalidArgumentException $e) { + throw new InvalidArgumentException( + 'Invalid target size ' . $width . 'x' . $height, + previous: $e, + ); + } + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::cover() + * + * @throws InvalidArgumentException + */ + public function cover(int $width, int $height): SizeInterface + { + try { + return $this->resizer($width, $height)->cover($this); + } catch (InvalidArgumentException | StateException $e) { + throw new InvalidArgumentException( + 'Invalid target size ' . $width . 'x' . $height, + previous: $e, + ); + } + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::contain() + * + * @throws InvalidArgumentException + */ + public function contain(int $width, int $height): SizeInterface + { + try { + return $this->resizer($width, $height)->contain($this); + } catch (StateException $e) { + throw new InvalidArgumentException( + 'Invalid target size ' . $width . 'x' . $height, + previous: $e, + ); + } + } + + /** + * {@inheritdoc} + * + * @see SizeInterface::containDown() + * + * @throws InvalidArgumentException + */ + public function containDown(int $width, int $height): SizeInterface + { + try { + return $this->resizer($width, $height)->containDown($this); + } catch (StateException $e) { + throw new InvalidArgumentException( + 'Invalid target size ' . $width . 'x' . $height, + previous: $e, + ); + } + } + + /** + * Create resizer instance with given target size. + * + * @throws InvalidArgumentException + */ + protected function resizer(?int $width = null, ?int $height = null): Resizer + { + return new Resizer($width, $height); + } + + /** + * Implement iteration. + * + * @return Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator([$this->width(), $this->height()]); + } + + /** + * Show debug info for the current rectangle. + * + * @return array + */ + public function __debugInfo(): array + { + return [ + 'width' => $this->width(), + 'height' => $this->height(), + 'pivot' => $this->pivot, + ]; + } +} diff --git a/src/Traits/CanBeDriverSpecialized.php b/src/Traits/CanBeDriverSpecialized.php new file mode 100644 index 000000000..3cc283325 --- /dev/null +++ b/src/Traits/CanBeDriverSpecialized.php @@ -0,0 +1,104 @@ +> + */ + private static array $parameterCache = []; + + /** + * The driver with which the instance will be specialized. + */ + protected DriverInterface $driver; + + /** + * {@inheritdoc} + * + * @see SpecializableInterface::specializationArguments() + */ + public function specializationArguments(): array + { + $class = $this::class; + + if (!isset(self::$parameterCache[$class])) { + $names = []; + $reflectionClass = new ReflectionClass($class); + $constructor = $reflectionClass->getConstructor(); + if ($constructor !== null) { + foreach ($constructor->getParameters() as $parameter) { + $names[] = $parameter->getName(); + } + } + self::$parameterCache[$class] = $names; + } + + $specializable = []; + foreach (self::$parameterCache[$class] as $name) { + $specializable[$name] = $this->{$name}; + } + + return $specializable; + } + + /** + * {@inheritdoc} + * + * @see SpecializableInterface::driver() + * + * @throws StateException + */ + public function driver(): DriverInterface + { + if (!isset($this->driver)) { + throw new StateException( + 'Use setDriver() on ' . $this::class . ' to provide an applicable ' . DriverInterface::class, + ); + } + + return $this->driver; + } + + /** + * {@inheritdoc} + * + * @see SpecializableInterface::setDriver() + * + * @throws NotSupportedException + */ + public function setDriver(DriverInterface $driver): SpecializableInterface + { + if (!$this->belongsToDriver($driver)) { + throw new NotSupportedException( + "Class '" . $this::class . "' can not be used with " . $driver->id() . " driver" + ); + } + + $this->driver = $driver; + + return $this; + } + + /** + * Determine if the current object belongs to the given driver's namespace. + */ + protected function belongsToDriver(object $driver): bool + { + return str_starts_with( + $this::class, + substr($driver::class, 0, (int) strrpos($driver::class, '\\')), // driver namespace + ); + } +} diff --git a/src/Traits/CanBuildStream.php b/src/Traits/CanBuildStream.php new file mode 100644 index 000000000..df9b32386 --- /dev/null +++ b/src/Traits/CanBuildStream.php @@ -0,0 +1,54 @@ + fn(mixed $data) => fopen('php://temp', 'r+'), + is_resource($data) && get_resource_type($data) === 'stream' => fn(mixed $data) => $data, + is_string($data) => function (mixed $data) { + $stream = fopen('php://temp', 'r+'); + + if ($stream === false) { + throw new StreamException('Failed to build stream from string'); + } + + fwrite($stream, $data); + return $stream; + }, + default => throw new InvalidArgumentException( + 'Unable to create stream from ' . gettype($data) . '. Use only null, string or resource.', + ), + }; + + $stream = $buildStrategy($data); + + if ($stream === false) { + throw new StreamException('Failed to build stream'); + } + + $rewind = rewind($stream); + + if ($rewind === false) { + throw new StreamException('Failed to rewind stream'); + } + + return $stream; + } +} diff --git a/src/Traits/CanConvertRange.php b/src/Traits/CanConvertRange.php new file mode 100644 index 000000000..6c83ccb41 --- /dev/null +++ b/src/Traits/CanConvertRange.php @@ -0,0 +1,31 @@ + PHP_MAXPATHLEN) { + return false; + } + + if (str_starts_with($input, DIRECTORY_SEPARATOR)) { + return true; + } + + if (preg_match('/[^ -~]/', $input) === 1) { + return false; + } + + return true; + } +} diff --git a/src/Traits/CanParseFilePath.php b/src/Traits/CanParseFilePath.php new file mode 100644 index 000000000..7f17206aa --- /dev/null +++ b/src/Traits/CanParseFilePath.php @@ -0,0 +1,132 @@ + PHP_MAXPATHLEN) { + throw new InvalidArgumentException( + "Path is longer than the configured max. value of " . PHP_MAXPATHLEN + ); + } + + // get info on path + $dirname = pathinfo($path, PATHINFO_DIRNAME); + $basename = pathinfo($path, PATHINFO_BASENAME); + + if (!is_dir($dirname)) { + throw new DirectoryNotFoundException('Directory "' . $dirname . '" not found'); + } + + // file must exit + if (!file_exists($path)) { + throw new FileNotFoundException('File "' . $basename . '" not found in directory "' . $dirname . '"'); + } + + if (!is_file($path)) { + throw new FileNotFoundException('"' . $basename . '" is no file in directory "' . $dirname . '"'); + } + + if (!is_readable($dirname)) { + throw new FileNotReadableException('Directory "' . $dirname . '" is not readable'); + } + + if (!is_readable($path)) { + throw new FileNotReadableException('File "' . $path . '" is not readable'); + } + + $realpath = realpath($path); + + if ($realpath === false) { + throw new FileNotReadableException('File "' . $path . '" is not readable'); + } + + return $realpath; + } + + /** + * Read real path from given SplFileInfo object or throw exeption. + * + * @throws InvalidArgumentException + * @throws DirectoryNotFoundException + * @throws FileNotFoundException + * @throws FileNotReadableException + */ + protected static function filePathFromSplFileInfoOrFail(SplFileInfo $splFileInfo): string + { + $path = $splFileInfo->getPathname(); + $dirname = pathinfo($path, PATHINFO_DIRNAME); + $basename = pathinfo($path, PATHINFO_BASENAME); + + if ($path === '') { + throw new InvalidArgumentException('Path contained in SplFileInfo must not be an empty string'); + } + + if (!is_dir($dirname)) { + throw new DirectoryNotFoundException( + 'The directory "' . $dirname . '" contained in SplFileInfo does not exist', + ); + } + + if (!file_exists($path)) { + throw new FileNotFoundException( + 'File "' . $basename . '" contained in SplFileInfo was not found in directory "' . $dirname . '"', + ); + } + + if (!is_file($path)) { + throw new FileNotFoundException( + 'File "' . $basename . '" contained in SplFileInfo is no file in directory "' . $dirname . '"', + ); + } + + if (!is_readable($dirname)) { + throw new FileNotReadableException( + 'Directory "' . $dirname . '" contained in SplFileInfo is not readable', + ); + } + + if (!is_readable($path)) { + throw new FileNotReadableException( + 'File "' . $path . '" contained in SplFileInfo is not readable', + ); + } + + $realpath = $splFileInfo->getRealPath(); + + if ($realpath === false) { + throw new FileNotReadableException('Failed to read file "' . $path . '" from ' . SplFileInfo::class); + } + + return $realpath; + } +} diff --git a/src/Traits/CanResolveDriver.php b/src/Traits/CanResolveDriver.php new file mode 100644 index 000000000..cd6b5fce9 --- /dev/null +++ b/src/Traits/CanResolveDriver.php @@ -0,0 +1,36 @@ +config()->setOptions(...$options); + + return $driver; + } +} diff --git a/src/Typography/Font.php b/src/Typography/Font.php new file mode 100644 index 000000000..65f78e7eb --- /dev/null +++ b/src/Typography/Font.php @@ -0,0 +1,306 @@ +size = $size; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::size() + */ + public function size(): float + { + return $this->size; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::setAngle() + */ + public function setAngle(float $angle): FontInterface + { + $this->angle = $angle; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::angle() + */ + public function angle(): float + { + return $this->angle; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::setFilepath() + * + * @throws InvalidArgumentException + * @throws DirectoryNotFoundException + * @throws FileNotFoundException + * @throws FileNotReadableException + */ + public function setFilepath(string $path): FontInterface + { + $this->filepath = self::readableFilePathOrFail($path); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::filepath() + */ + public function filepath(): ?string + { + try { + return self::readableFilePathOrFail($this->filepath); + } catch (FilesystemException | InvalidArgumentException) { + return null; + } + } + + /** + * {@inheritdoc} + * + * @see FontInterface::hasFile() + */ + public function hasFile(): bool + { + return $this->filepath() !== null; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::setColor() + */ + public function setColor(string|ColorInterface $color): FontInterface + { + $this->color = $color; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::color() + */ + public function color(): null|string|ColorInterface + { + return $this->color; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::setStrokeColor() + */ + public function setStrokeColor(string|ColorInterface $color): FontInterface + { + $this->strokeColor = $color; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::strokeColor() + */ + public function strokeColor(): null|string|ColorInterface + { + return $this->strokeColor; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::setStrokeWidth() + * + * @throws InvalidArgumentException + */ + public function setStrokeWidth(int $width): FontInterface + { + if (!in_array($width, range(0, 10))) { + throw new InvalidArgumentException( + 'The stroke width must be in the range from 0 to 10' + ); + } + + $this->strokeWidth = $width; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::strokeWidth() + */ + public function strokeWidth(): int + { + return $this->strokeWidth; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::hasStrokeEffect() + */ + public function hasStrokeEffect(): bool + { + return $this->strokeWidth > 0; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::alignmentHorizontal() + */ + public function alignmentHorizontal(): Alignment + { + return $this->alignmentHorizontal; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::setAlignmentHorizontal() + * + * @throws InvalidArgumentException + */ + public function setAlignmentHorizontal(string|Alignment $alignment): FontInterface + { + try { + $this->alignmentHorizontal = is_string($alignment) ? Alignment::from($alignment) : $alignment; + } catch (ValueError | TypeError) { + throw new InvalidArgumentException('Invalid value for alignment'); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::alignmentVertical() + */ + public function alignmentVertical(): Alignment + { + return $this->alignmentVertical; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::setAlignmentVertical() + * + * @throws InvalidArgumentException + */ + public function setAlignmentVertical(string|Alignment $alignment): FontInterface + { + try { + $this->alignmentVertical = is_string($alignment) ? Alignment::from($alignment) : $alignment; + } catch (ValueError | TypeError) { + throw new InvalidArgumentException('Invalid value for alignment'); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::setLineHeight() + */ + public function setLineHeight(float $height): FontInterface + { + $this->lineHeight = $height; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::lineHeight() + */ + public function lineHeight(): float + { + return $this->lineHeight; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::setWrapWidth() + */ + public function setWrapWidth(?int $width): FontInterface + { + $this->wrapWidth = $width; + + return $this; + } + + /** + * {@inheritdoc} + * + * @see FontInterface::wrapWidth() + */ + public function wrapWidth(): ?int + { + return $this->wrapWidth; + } +} diff --git a/src/Typography/FontFactory.php b/src/Typography/FontFactory.php new file mode 100644 index 000000000..b2295886a --- /dev/null +++ b/src/Typography/FontFactory.php @@ -0,0 +1,149 @@ +font = $font instanceof FontInterface ? clone $font : new Font(); + + if (is_callable($font)) { + $font($this); + } + } + + /** + * Create the end product of the factory statically by calling given callable + */ + public static function build(null|callable|FontInterface $font = null): FontInterface + { + return (new self($font))->font(); + } + + /** + * Return the end product of the factory + */ + public function font(): FontInterface + { + return $this->font; + } + + /** + * Set the filename of the font to be built. + */ + public function filename(string $path): self + { + $this->font->setFilepath($path); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see self::filename() + */ + public function file(string $path): self + { + return $this->filename($path); + } + + /** + * {@inheritdoc} + * + * @see self::filename() + */ + public function filepath(string $path): self + { + return $this->filename($path); + } + + /** + * Set outline stroke effect for the font to be built. + */ + public function stroke(string|ColorInterface $color, int $width = 1): self + { + $this->font->setStrokeWidth($width); + $this->font->setStrokeColor($color); + + return $this; + } + + /** + * Set color for the font to be built. + */ + public function color(string|ColorInterface $color): self + { + $this->font->setColor($color); + + return $this; + } + + /** + * Set the size for the font to be built. + */ + public function size(float $size): self + { + $this->font->setSize($size); + + return $this; + } + + /** + * Set the horizontal and/or vertical alignment of the font. + */ + public function align(null|string|Alignment $horizontal = null, null|string|Alignment $vertical = null): self + { + if ($horizontal !== null) { + $this->font->setAlignmentHorizontal($horizontal); + } + + if ($vertical !== null) { + $this->font->setAlignmentVertical($vertical); + } + + return $this; + } + + /** + * Set the line height of the font to be built. + */ + public function lineHeight(float $height): self + { + $this->font->setLineHeight($height); + + return $this; + } + + /** + * Set the clockwise rotation angle of the font to be built. + */ + public function angle(float $angle): self + { + $this->font->setAngle($angle); + + return $this; + } + + /** + * Set the maximum width of the text block to be built. + */ + public function wrap(int $width): self + { + $this->font->setWrapWidth($width); + + return $this; + } +} diff --git a/src/Typography/Line.php b/src/Typography/Line.php new file mode 100644 index 000000000..2c5d6a784 --- /dev/null +++ b/src/Typography/Line.php @@ -0,0 +1,123 @@ + + */ +class Line implements IteratorAggregate, Countable, Stringable +{ + /** + * Segments (usually individual words including punctuation marks) of the line. + * + * @var array + */ + protected array $segments = []; + + /** + * Create new text line object with given text & position. + */ + public function __construct( + ?string $text = null, + protected PointInterface $position = new Point() + ) { + if (is_string($text)) { + $this->segments = $this->wordsSeparatedBySpaces($text) ? explode(" ", $text) : mb_str_split($text); + } + } + + /** + * Add word to current line. + */ + public function add(string $word): self + { + $this->segments[] = $word; + + return $this; + } + + /** + * Returns Iterator. + * + * @return Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->segments); + } + + /** + * Get Position of line. + */ + public function position(): PointInterface + { + return $this->position; + } + + /** + * Set position of current line. + */ + public function setPosition(PointInterface $point): self + { + $this->position = $point; + + return $this; + } + + /** + * Count segments (individual words including punctuation marks) of line. + */ + public function count(): int + { + return count($this->segments); + } + + /** + * Count characters of line. + */ + public function length(): int + { + return mb_strlen((string) $this); + } + + /** + * Determine if words are separated by spaces in the written language of the given text. + */ + private function wordsSeparatedBySpaces(string $text): bool + { + return 1 !== preg_match( + '/[' . + '\x{4E00}-\x{9FFF}' . // CJK Unified Ideographs (chinese) + '\x{3400}-\x{4DBF}' . // CJK Unified Ideographs Extension A (chinese) + '\x{3040}-\x{309F}' . // hiragana (japanese) + '\x{30A0}-\x{30FF}' . // katakana (japanese) + '\x{0E00}-\x{0E7F}' . // thai + ']/u', + $text + ); + } + + /** + * Cast line to string. + */ + public function __toString(): string + { + $string = implode("", $this->segments); + + if ($this->wordsSeparatedBySpaces($string)) { + return implode(" ", $this->segments); + } + + return $string; + } +} diff --git a/src/Typography/TextBlock.php b/src/Typography/TextBlock.php new file mode 100644 index 000000000..b52b54e15 --- /dev/null +++ b/src/Typography/TextBlock.php @@ -0,0 +1,71 @@ + new Line($line), + explode("\n", $text), + )); + } + + /** + * Return array of lines in text block. + * + * @return array + */ + public function lines(): array + { + return $this->items; + } + + /** + * Set lines of the text block. + * + * @param array $lines + */ + public function setLines(array $lines): self + { + $this->items = $lines; + + return $this; + } + + /** + * Get line by given key. + */ + public function line(string|int|float $key): ?Line + { + if (!array_key_exists((string) $key, $this->lines())) { + return null; + } + + return $this->lines()[(string) $key]; + } + + /** + * Return line with most characters of text block. + */ + public function longestLine(): Line + { + $lines = $this->lines(); + usort($lines, function (Line $a, Line $b): int { + if ($a->length() === $b->length()) { + return 0; + } + return $a->length() > $b->length() ? -1 : 1; + }); + + return $lines[0]; + } +} diff --git a/src/config/config.php b/src/config/config.php deleted file mode 100644 index b106809e2..000000000 --- a/src/config/config.php +++ /dev/null @@ -1,20 +0,0 @@ - 'gd' - -); diff --git a/tests/AbstractColorTest.php b/tests/AbstractColorTest.php deleted file mode 100644 index 3febab79a..000000000 --- a/tests/AbstractColorTest.php +++ /dev/null @@ -1,18 +0,0 @@ -getMockForAbstractClass('\Intervention\Image\AbstractColor'); - $color->format('xxxxxxxxxxxxxxxxxxxxxxx'); - } -} diff --git a/tests/AbstractCommandTest.php b/tests/AbstractCommandTest.php deleted file mode 100644 index cd6196008..000000000 --- a/tests/AbstractCommandTest.php +++ /dev/null @@ -1,46 +0,0 @@ -getTestCommand(); - $this->assertEquals('foo', $command->argument(0)->value()); - $this->assertEquals('bar', $command->argument(1)->value()); - } - - public function testGetOutput() - { - $command = $this->getTestCommand(); - $command->setOutput('foo'); - $this->assertEquals('foo', $command->getOutput()); - } - - public function testHasOutput() - { - $command = $this->getTestCommand(); - $this->assertEquals(false, $command->hasOutput()); - $command->setOutput('foo'); - $this->assertEquals(true, $command->hasOutput()); - } - - public function testSetOutput() - { - $command = $this->getTestCommand(); - $command->setOutput('foo'); - $this->assertEquals(true, $command->hasOutput()); - } - - public function getTestCommand() - { - $arguments = array('foo', 'bar'); - $command = $this->getMockForAbstractClass('\Intervention\Image\Commands\AbstractCommand', array($arguments)); - - return $command; - } -} diff --git a/tests/AbstractDecoderTest.php b/tests/AbstractDecoderTest.php deleted file mode 100644 index 4d633ce27..000000000 --- a/tests/AbstractDecoderTest.php +++ /dev/null @@ -1,148 +0,0 @@ -getTestDecoder(new \Imagick); - $this->assertTrue($source->isImagick()); - - $source = $this->getTestDecoder(new stdClass); - $this->assertFalse($source->isImagick()); - - $source = $this->getTestDecoder(null); - $this->assertFalse($source->isImagick()); - } - - public function testIsGdResource() - { - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $source = $this->getTestDecoder($resource); - $this->assertTrue($source->isGdResource()); - - $source = $this->getTestDecoder(tmpfile()); - $this->assertFalse($source->isGdResource()); - - $source = $this->getTestDecoder(null); - $this->assertFalse($source->isGdResource()); - } - - public function testIsFilepath() - { - $source = $this->getTestDecoder(__DIR__.'/AbstractDecoderTest.php'); - $this->assertTrue($source->isFilepath()); - - $source = $this->getTestDecoder(null); - $this->assertFalse($source->isFilepath()); - - $source = $this->getTestDecoder(array()); - $this->assertFalse($source->isFilepath()); - - $source = $this->getTestDecoder(new stdClass); - $this->assertFalse($source->isFilepath()); - } - - public function testIsUrl() - { - $source = $this->getTestDecoder('http://foo.bar'); - $this->assertTrue($source->isUrl()); - - $source = $this->getTestDecoder(null); - $this->assertFalse($source->isUrl()); - } - - public function testIsStream() - { - $source = $this->getTestDecoder(fopen(__DIR__ . '/images/test.jpg', 'r')); - $this->assertTrue($source->isStream()); - - $source = $this->getTestDecoder(null); - $this->assertFalse($source->isStream()); - } - - public function testIsBinary() - { - $source = $this->getTestDecoder(file_get_contents(__DIR__.'/images/test.jpg')); - $this->assertTrue($source->isBinary()); - - $source = $this->getTestDecoder(null); - $this->assertFalse($source->isBinary()); - - $source = $this->getTestDecoder(1); - $this->assertFalse($source->isBinary()); - - $source = $this->getTestDecoder(0); - $this->assertFalse($source->isBinary()); - - $source = $this->getTestDecoder(array(1,2,3)); - $this->assertFalse($source->isBinary()); - - $source = $this->getTestDecoder(new stdClass); - $this->assertFalse($source->isBinary()); - } - - public function testIsInterventionImage() - { - $source = $this->getTestDecoder(1); - $this->assertFalse($source->isInterventionImage()); - - $img = Mockery::mock('Intervention\Image\Image'); - $source = $this->getTestDecoder($img); - $this->assertTrue($source->isInterventionImage()); - } - - public function testIsSplFileInfo() - { - $source = $this->getTestDecoder(1); - $this->assertFalse($source->isSplFileInfo()); - - $img = Mockery::mock('SplFileInfo'); - $source = $this->getTestDecoder($img); - $this->assertTrue($source->isSplFileInfo()); - - $img = Mockery::mock('Symfony\Component\HttpFoundation\File\UploadedFile', 'SplFileInfo'); - $this->assertTrue($source->isSplFileInfo()); - } - - public function testIsSymfonyUpload() - { - $source = $this->getTestDecoder(1); - $this->assertFalse($source->isSymfonyUpload()); - - $img = Mockery::mock('Symfony\Component\HttpFoundation\File\UploadedFile'); - $source = $this->getTestDecoder($img); - $this->assertTrue($source->isSymfonyUpload()); - } - - public function testIsDataUrl() - { - $source = $this->getTestDecoder('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGElEQVQYlWM8c+bMfwYiABMxikYVUk8hAHWzA3cRvs4UAAAAAElFTkSuQmCC'); - $this->assertTrue($source->isDataUrl()); - - $source = $this->getTestDecoder(null); - $this->assertFalse($source->isDataUrl()); - } - - public function testIsBase64() - { - $decoder = $this->getTestDecoder(null); - $this->assertFalse($decoder->isBase64()); - - $decoder = $this->getTestDecoder('random'); - $this->assertFalse($decoder->isBase64()); - - $base64 = "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGElEQVQYlWM8c+bMfwYiABMxikYVUk8hAHWzA3cRvs4UAAAAAElFTkSuQmCC"; - $decoder = $this->getTestDecoder($base64); - $this->assertTrue($decoder->isBase64()); - } - - public function getTestDecoder($data) - { - return $this->getMockForAbstractClass('\Intervention\Image\AbstractDecoder', array($data)); - } -} diff --git a/tests/AbstractDriverTest.php b/tests/AbstractDriverTest.php deleted file mode 100644 index 7339e077c..000000000 --- a/tests/AbstractDriverTest.php +++ /dev/null @@ -1,19 +0,0 @@ -getMockForAbstractClass('\Intervention\Image\AbstractDriver'); - $command = $driver->executeCommand($image, 'xxxxxxxxxxxxxxxxxxxxxxx', array()); - } -} diff --git a/tests/AbstractFontTest.php b/tests/AbstractFontTest.php deleted file mode 100644 index cf1d03b5a..000000000 --- a/tests/AbstractFontTest.php +++ /dev/null @@ -1,77 +0,0 @@ -getMockForAbstractClass('\Intervention\Image\AbstractFont', array('test')); - $this->assertEquals('test', $font->text); - } - - public function testText() - { - $font = $this->getMockForAbstractClass('\Intervention\Image\AbstractFont'); - $font->text('test'); - $this->assertEquals('test', $font->text); - } - - public function testSize() - { - $font = $this->getMockForAbstractClass('\Intervention\Image\AbstractFont'); - $font->size(16); - $this->assertEquals(16, $font->size); - } - - public function testColor() - { - $font = $this->getMockForAbstractClass('\Intervention\Image\AbstractFont'); - $font->color('#ffffff'); - $this->assertEquals('#ffffff', $font->color); - } - - public function testAngle() - { - $font = $this->getMockForAbstractClass('\Intervention\Image\AbstractFont'); - $font->angle(30); - $this->assertEquals(30, $font->angle); - } - - public function testAlign() - { - $font = $this->getMockForAbstractClass('\Intervention\Image\AbstractFont'); - $font->align('right'); - $this->assertEquals('right', $font->align); - } - - public function testValign() - { - $font = $this->getMockForAbstractClass('\Intervention\Image\AbstractFont'); - $font->valign('top'); - $this->assertEquals('top', $font->valign); - } - - public function testFile() - { - $font = $this->getMockForAbstractClass('\Intervention\Image\AbstractFont'); - $font->file('test.ttf'); - $this->assertEquals('test.ttf', $font->file); - } - - public function testCountLines() - { - $font = $this->getMockForAbstractClass('\Intervention\Image\AbstractFont'); - $font->text('foo'.PHP_EOL.'bar'.PHP_EOL.'baz'); - $this->assertEquals(3, $font->countLines()); - $font->text("foo\nbar\nbaz"); - $this->assertEquals(3, $font->countLines()); - $font->text('foo - bar - baz'); - $this->assertEquals(3, $font->countLines()); - } -} diff --git a/tests/AbstractShapeTest.php b/tests/AbstractShapeTest.php deleted file mode 100644 index e27ca82fc..000000000 --- a/tests/AbstractShapeTest.php +++ /dev/null @@ -1,41 +0,0 @@ -getMockForAbstractClass('\Intervention\Image\AbstractShape'); - $shape->background('foo'); - $this->assertEquals('foo', $shape->background); - $this->assertEquals(0, $shape->border_width); - } - - public function testBorder() - { - $shape = $this->getMockForAbstractClass('\Intervention\Image\AbstractShape'); - $shape->border(4); - $this->assertEquals(4, $shape->border_width); - $this->assertEquals('#000000', $shape->border_color); - } - - public function testBorderWithColor() - { - $shape = $this->getMockForAbstractClass('\Intervention\Image\AbstractShape'); - $shape->border(3, '#ff00ff'); - $this->assertEquals(3, $shape->border_width); - $this->assertEquals('#ff00ff', $shape->border_color); - } - - public function testHasBorder() - { - $shape = $this->getMockForAbstractClass('\Intervention\Image\AbstractShape'); - $this->assertFalse($shape->hasBorder()); - $shape->border(1); - $this->assertTrue($shape->hasBorder()); - } -} diff --git a/tests/ArgumentTest.php b/tests/ArgumentTest.php deleted file mode 100644 index 54e4d5d81..000000000 --- a/tests/ArgumentTest.php +++ /dev/null @@ -1,421 +0,0 @@ -getMockedCommand(array('foo'))); - $this->validateArgument($arg, 'foo'); - - $arg = new Argument($this->getMockedCommand(array('foo', 'bar')), 1); - $this->validateArgument($arg, 'bar'); - - $arg = new Argument($this->getMockedCommand(), 0); - $this->validateArgument($arg, null); - } - - public function testRequiredPass() - { - $arg = new Argument($this->getMockedCommand(array('foo'))); - $arg->required(); - $this->validateArgument($arg, 'foo'); - - $arg = new Argument($this->getMockedCommand(array(null))); - $arg->required(); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(0))); - $arg->required(); - $this->validateArgument($arg, 0); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testRequiredFail() - { - $arg = new Argument($this->getMockedCommand(array())); - $arg->required(); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testRequiredFailSecondParameter() - { - $arg = new Argument($this->getMockedCommand(array('foo')), 1); - $arg->required(); - } - - public function testTypeIntegerPass() - { - $arg = new Argument($this->getMockedCommand(array())); - $arg->type('integer'); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(123))); - $arg->type('integer'); - $this->validateArgument($arg, 123); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testTypeIntegerFail() - { - $arg = new Argument($this->getMockedCommand(array('foo'))); - $arg->type('integer'); - } - - public function testTypeArrayPass() - { - $arg = new Argument($this->getMockedCommand(array())); - $arg->type('array'); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(array(1,2,3)))); - $arg->type('array'); - $this->validateArgument($arg, array(1,2,3)); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testTypeArrayFail() - { - $arg = new Argument($this->getMockedCommand(array('foo'))); - $arg->type('array'); - } - - public function testTypeDigitPass() - { - $arg = new Argument($this->getMockedCommand(array())); - $arg->type('digit'); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(0))); - $arg->type('digit'); - $this->validateArgument($arg, 0); - - $arg = new Argument($this->getMockedCommand(array(123))); - $arg->type('digit'); - $this->validateArgument($arg, 123); - - $arg = new Argument($this->getMockedCommand(array(5.0))); - $arg->type('digit'); - $this->validateArgument($arg, 5.0); - - $arg = new Argument($this->getMockedCommand(array(-10))); - $arg->type('digit'); - $this->validateArgument($arg, -10); - - $arg = new Argument($this->getMockedCommand(array(-10.0))); - $arg->type('digit'); - $this->validateArgument($arg, -10.0); - - $arg = new Argument($this->getMockedCommand(array('12'))); - $arg->type('digit'); - $this->validateArgument($arg, '12'); - - $arg = new Argument($this->getMockedCommand(array('12.0'))); - $arg->type('digit'); - $this->validateArgument($arg, '12.0'); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testTypeDigitFailString() - { - $arg = new Argument($this->getMockedCommand(array('foo'))); - $arg->type('digit'); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testTypeDigitFailFloat() - { - $arg = new Argument($this->getMockedCommand(array(12.5))); - $arg->type('digit'); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testTypeDigitFailBool() - { - $arg = new Argument($this->getMockedCommand(array(true))); - $arg->type('digit'); - } - - public function testTypeNumericPass() - { - $arg = new Argument($this->getMockedCommand(array())); - $arg->type('numeric'); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(12.3))); - $arg->type('numeric'); - $this->validateArgument($arg, 12.3); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testTypeNumericFail() - { - $arg = new Argument($this->getMockedCommand(array('foo'))); - $arg->type('numeric'); - } - - public function testTypeBooleanPass() - { - $arg = new Argument($this->getMockedCommand(array())); - $arg->type('boolean'); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(true))); - $arg->type('boolean'); - $this->validateArgument($arg, true); - - $arg = new Argument($this->getMockedCommand(array(false))); - $arg->type('boolean'); - $this->validateArgument($arg, false); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testTypeBooleanFail() - { - $arg = new Argument($this->getMockedCommand(array('foo'))); - $arg->type('boolean'); - } - - public function testTypeStringPass() - { - $arg = new Argument($this->getMockedCommand(array())); - $arg->type('string'); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array('foo'))); - $arg->type('string'); - $this->validateArgument($arg, 'foo'); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testTypeStringFail() - { - $arg = new Argument($this->getMockedCommand(array(12))); - $arg->type('string'); - } - - public function testTypeClosurePass() - { - $arg = new Argument($this->getMockedCommand(array())); - $arg->type('closure'); - $this->validateArgument($arg, null); - - $c = function ($foo) {}; - $arg = new Argument($this->getMockedCommand(array($c))); - $arg->type('closure'); - $this->validateArgument($arg, $c); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testTypeClosureFail() - { - $arg = new Argument($this->getMockedCommand(array('foo'))); - $arg->type('closure'); - } - - public function testBetweenPass() - { - $arg = new Argument($this->getMockedCommand(array())); - $arg->between(0, 10); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(null))); - $arg->between(0, 10); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(4.5))); - $arg->between(0, 10); - $this->validateArgument($arg, 4.5); - - $arg = new Argument($this->getMockedCommand(array(4.5))); - $arg->between(10, 1); - $this->validateArgument($arg, 4.5); - - $arg = new Argument($this->getMockedCommand(array(0))); - $arg->between(0, 10); - $this->validateArgument($arg, 0); - - $arg = new Argument($this->getMockedCommand(array(10))); - $arg->between(0, 10); - $this->validateArgument($arg, 10); - - $arg = new Argument($this->getMockedCommand(array(0))); - $arg->between(-100, 100); - $this->validateArgument($arg, 0); - - $arg = new Argument($this->getMockedCommand(array(-100))); - $arg->between(-100, 100); - $this->validateArgument($arg, -100); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testBetweenFailString() - { - $arg = new Argument($this->getMockedCommand(array('foo'))); - $arg->between(1, 10); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testBetweenFailAbove() - { - $arg = new Argument($this->getMockedCommand(array(10.9))); - $arg->between(0, 10); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testBetweenFailBelow() - { - $arg = new Argument($this->getMockedCommand(array(-1))); - $arg->between(0, 10); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testBetweenFail() - { - $arg = new Argument($this->getMockedCommand(array(-1000))); - $arg->between(-100, 100); - } - - public function testMinPass() - { - $arg = new Argument($this->getMockedCommand(array())); - $arg->min(10); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(null))); - $arg->min(10); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(50))); - $arg->min(10); - $this->validateArgument($arg, 50); - - $arg = new Argument($this->getMockedCommand(array(50))); - $arg->min(50); - $this->validateArgument($arg, 50); - - $arg = new Argument($this->getMockedCommand(array(50))); - $arg->min(-10); - $this->validateArgument($arg, 50); - - $arg = new Argument($this->getMockedCommand(array(-10))); - $arg->min(-10); - $this->validateArgument($arg, -10); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testMinFailString() - { - $arg = new Argument($this->getMockedCommand(array('foo'))); - $arg->min(10); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testMinFail() - { - $arg = new Argument($this->getMockedCommand(array(10.9))); - $arg->min(11); - } - - public function testMaxPass() - { - $arg = new Argument($this->getMockedCommand(array())); - $arg->max(100); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(null))); - $arg->max(100); - $this->validateArgument($arg, null); - - $arg = new Argument($this->getMockedCommand(array(50))); - $arg->max(100); - $this->validateArgument($arg, 50); - - $arg = new Argument($this->getMockedCommand(array(100))); - $arg->max(100); - $this->validateArgument($arg, 100); - - $arg = new Argument($this->getMockedCommand(array(-100))); - $arg->max(-10); - $this->validateArgument($arg, -100); - - $arg = new Argument($this->getMockedCommand(array(-10))); - $arg->max(-10); - $this->validateArgument($arg, -10); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testMaxFailString() - { - $arg = new Argument($this->getMockedCommand(array('foo'))); - $arg->max(10); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testMaxFail() - { - $arg = new Argument($this->getMockedCommand(array(25))); - $arg->max(10); - } - - public function testValueDefault() - { - $arg = new Argument($this->getMockedCommand()); - $value = $arg->value('foo'); - $this->assertEquals('foo', $value); - - $arg = new Argument($this->getMockedCommand(array(null))); - $value = $arg->value('foo'); - $this->assertEquals('foo', $value); - } - - private function validateArgument($argument, $value) - { - $this->assertInstanceOf('\Intervention\Image\Commands\Argument', $argument); - $this->assertEquals($value, $argument->value()); - } - - private function getMockedCommand($arguments = array()) - { - return $this->getMockForAbstractClass('\Intervention\Image\Commands\AbstractCommand', array($arguments)); - } -} diff --git a/tests/BackupCommandTest.php b/tests/BackupCommandTest.php deleted file mode 100644 index 3f7967dda..000000000 --- a/tests/BackupCommandTest.php +++ /dev/null @@ -1,60 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('getSize')->once()->andReturn($size); - $image->shouldReceive('setBackup')->once(); - $command = new BackupGd(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testGdWithName() - { - $size = Mockery::mock('Intervention\Image\Size', array(800, 600)); - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('getSize')->once()->andReturn($size); - $image->shouldReceive('setBackup')->once(); - $command = new BackupGd(array('name' => 'fooBackup')); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagickWithoutName() - { - $imagick = Mockery::mock('Imagick'); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $image->shouldReceive('setBackup')->once(); - $command = new BackupImagick(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagickWithName() - { - $imagick = Mockery::mock('Imagick'); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $image->shouldReceive('setBackup')->once(); - $command = new BackupImagick(array('name' => 'fooBackup')); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php new file mode 100644 index 000000000..db66285d0 --- /dev/null +++ b/tests/BaseTestCase.php @@ -0,0 +1,106 @@ +channel(Red::class)->value(), + $color->channel(Green::class)->value(), + $color->channel(Blue::class)->value(), + $color->channel(Alpha::class)->value(), + ]) . ')'; + + return implode(' ', [ + 'Failed asserting that color', + $color, + 'equals', + 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')' + ]); + }; + + foreach ([Red::class => $r, Green::class => $g, Blue::class => $b, Alpha::class => $a] as $channel => $value) { + $this->assertThat( + $color->channel($channel)->value(), + $this->logicalAnd( + $this->greaterThanOrEqual(max($channel::min(), $value - $tolerance)), + $this->lessThanOrEqual(min($channel::max(), $value + $tolerance)) + ), + message: $errorMessage($r, $g, $b, $a, $color) + ); + } + } + + protected function assertBetween(int|float $min, int|float $max, int|float $value): void + { + if ($value < $min || $value > $max) { + throw new ExpectationFailedException( + 'Failed asserting that value ' . $value . ' is between ' . $min . ' and ' . $max, + ); + } + } + + protected function assertTransparency(ColorInterface $color): void + { + $this->assertInstanceOf(RgbColor::class, $color); + $channel = $color->channel(Alpha::class); + $this->assertEquals(0, $channel->value(), 'Detected color ' . $color . ' is not completely transparent.'); + } + + protected function assertMediaType(string|array $allowed, string|EncodedImage $input): void + { + $stream = fopen('php://temp', 'rw'); + fwrite($stream, (string) $input); + rewind($stream); + $detected = mime_content_type($stream); + fclose($stream); + + $allowed = is_string($allowed) ? [$allowed] : $allowed; + $this->assertTrue( + in_array($detected, $allowed), + 'Detected media type "' . $detected . '" is not: ' . implode(', ', $allowed), + ); + } + + protected function assertMediaTypeBitmap(string|EncodedImage $input): void + { + $this->assertMediaType([ + 'image/x-ms-bmp', + 'image/bmp', + 'bmp', + 'ms-bmp', + 'x-bitmap', + 'x-bmp', + 'x-ms-bmp', + 'x-win-bitmap', + 'x-windows-bmp', + 'x-xbitmap', + 'image/ms-bmp', + 'image/x-bitmap', + 'image/x-bmp', + 'image/x-ms-bmp', + 'image/x-win-bitmap', + 'image/x-windows-bmp', + 'image/x-xbitmap', + ], $input); + } +} diff --git a/tests/BlurCommandTest.php b/tests/BlurCommandTest.php deleted file mode 100644 index 2380c3fce..000000000 --- a/tests/BlurCommandTest.php +++ /dev/null @@ -1,33 +0,0 @@ -shouldReceive('getCore')->times(2)->andReturn($resource); - $command = new BlurGd(array(2)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('blurimage')->with(2, 1)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new BlurImagick(array(2)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/BrightnessCommandTest.php b/tests/BrightnessCommandTest.php deleted file mode 100644 index 999a966c8..000000000 --- a/tests/BrightnessCommandTest.php +++ /dev/null @@ -1,33 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $command = new BrightnessGd(array(12)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('modulateimage')->with(112, 100, 100)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new BrightnessImagick(array(12)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/ChecksumCommandTest.php b/tests/ChecksumCommandTest.php deleted file mode 100644 index cf42f7ddf..000000000 --- a/tests/ChecksumCommandTest.php +++ /dev/null @@ -1,26 +0,0 @@ -shouldReceive('getSize')->once()->andReturn($size); - $image->shouldReceive('pickColor')->times(9)->andReturn($color); - $command = new ChecksumCommand(array()); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertEquals('ec9cbdb71be04e26b4a89333f20c273b', $command->getOutput()); - } -} diff --git a/tests/CircleCommandTest.php b/tests/CircleCommandTest.php deleted file mode 100644 index 241440ec4..000000000 --- a/tests/CircleCommandTest.php +++ /dev/null @@ -1,42 +0,0 @@ -shouldReceive('getDriverName')->once()->andReturn('Gd'); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $command = new CircleCommand(array(250, 10, 20)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertFalse($command->hasOutput()); - } - - public function testImagick() - { - $imagick = Mockery::mock('\Imagick'); - $imagick->shouldReceive('drawimage'); - $driver = Mockery::mock('\Intervention\Image\Imagick\Driver'); - $driver->shouldReceive('getDriverName')->once()->andReturn('Imagick'); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - - $command = new CircleCommand(array(25, 10, 20)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertFalse($command->hasOutput()); - } - -} diff --git a/tests/CircleShapeTest.php b/tests/CircleShapeTest.php deleted file mode 100644 index 71b40d23a..000000000 --- a/tests/CircleShapeTest.php +++ /dev/null @@ -1,51 +0,0 @@ -assertInstanceOf('Intervention\Image\Gd\Shapes\CircleShape', $circle); - $this->assertEquals(250, $circle->diameter); - - } - - public function testGdApplyToImage() - { - $core = imagecreatetruecolor(300, 200); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $circle = new CircleGd(250); - $result = $circle->applyToImage($image, 10, 20); - $this->assertInstanceOf('Intervention\Image\Gd\Shapes\CircleShape', $circle); - $this->assertTrue($result); - } - - public function testImagickConstructor() - { - $circle = new CircleImagick(250); - $this->assertInstanceOf('Intervention\Image\Imagick\Shapes\CircleShape', $circle); - $this->assertEquals(250, $circle->width); - } - - public function testImagickApplyToImage() - { - $core = Mockery::mock('\Imagick'); - $core->shouldReceive('drawimage')->once(); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $circle = new CircleImagick(250); - $result = $circle->applyToImage($image, 10, 20); - $this->assertInstanceOf('Intervention\Image\Imagick\Shapes\CircleShape', $circle); - $this->assertTrue($result); - } - -} diff --git a/tests/ColorizeCommandTest.php b/tests/ColorizeCommandTest.php deleted file mode 100644 index c89d0d4a9..000000000 --- a/tests/ColorizeCommandTest.php +++ /dev/null @@ -1,36 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $command = new ColorizeGd(array(20, 0, -40)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('getquantumrange')->with()->once()->andReturn(array('quantumRangeLong' => 42)); - $imagick->shouldReceive('levelimage')->with(0, 4, 42, \Imagick::CHANNEL_RED)->once()->andReturn(true); - $imagick->shouldReceive('levelimage')->with(0, 1, 42, \Imagick::CHANNEL_GREEN)->once()->andReturn(true); - $imagick->shouldReceive('levelimage')->with(0, 0.6, 42, \Imagick::CHANNEL_BLUE)->once()->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->times(4)->andReturn($imagick); - $command = new ColorizeImagick(array(20, 0, -40)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/ConstraintTest.php b/tests/ConstraintTest.php deleted file mode 100644 index 6aec91ea0..000000000 --- a/tests/ConstraintTest.php +++ /dev/null @@ -1,57 +0,0 @@ -getMockedSize(800, 600); - $constraint = new Constraint($size); - $this->assertInstanceOf('Intervention\Image\Constraint', $constraint); - $this->assertFalse($constraint->isFixed(Constraint::ASPECTRATIO)); - $this->assertFalse($constraint->isFixed(Constraint::UPSIZE)); - } - - public function testSetOnlyAspectRatio() - { - $size = $this->getMockedSize(800, 600); - $constraint = new Constraint($size); - $constraint->aspectRatio(); - $this->assertTrue($constraint->isFixed(Constraint::ASPECTRATIO)); - $this->assertFalse($constraint->isFixed(Constraint::UPSIZE)); - } - - public function testSetOnlyUpsize() - { - $size = $this->getMockedSize(800, 600); - $constraint = new Constraint($size); - $constraint->upsize(); - $this->assertFalse($constraint->isFixed(Constraint::ASPECTRATIO)); - $this->assertTrue($constraint->isFixed(Constraint::UPSIZE)); - } - - public function testSetAspectratioAndUpsize() - { - $size = $this->getMockedSize(800, 600); - $constraint = new Constraint($size); - $constraint->aspectRatio(); - $constraint->upsize(); - $this->assertTrue($constraint->isFixed(Constraint::ASPECTRATIO)); - $this->assertTrue($constraint->isFixed(Constraint::UPSIZE)); - } - - private function getMockedSize($width, $height) - { - $size = Mockery::mock('\Intervention\Image\Size', array($width, $height)); - $size->shouldReceive('getWidth')->andReturn($width); - $size->shouldReceive('getHeight')->andReturn($height); - $size->shouldReceive('getRatio')->andReturn($width/$height); - return $size; - } -} diff --git a/tests/ContrastCommandTest.php b/tests/ContrastCommandTest.php deleted file mode 100644 index 2c18f0866..000000000 --- a/tests/ContrastCommandTest.php +++ /dev/null @@ -1,33 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $command = new ContrastGd(array(20)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('sigmoidalcontrastimage')->with(true, 5, 0)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new ContrastImagick(array(20)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/CropCommandTest.php b/tests/CropCommandTest.php deleted file mode 100644 index 94cc5c04f..000000000 --- a/tests/CropCommandTest.php +++ /dev/null @@ -1,35 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('setCore')->once(); - $command = new CropGd(array(100, 150, 10, 20)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('cropimage')->with(100, 150, 10, 20)->andReturn(true); - $imagick->shouldReceive('setimagepage')->with(0, 0, 0, 0)->once(); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->times(2)->andReturn($imagick); - $command = new CropImagick(array(100, 150, 10, 20)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/DestroyCommandTest.php b/tests/DestroyCommandTest.php deleted file mode 100644 index f25719644..000000000 --- a/tests/DestroyCommandTest.php +++ /dev/null @@ -1,45 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('getBackups')->once()->andReturn($backups); - $command = new DestroyGd(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('clear')->with()->andReturn(true); - - $backup = Mockery::mock('Imagick'); - $backup->shouldReceive('clear')->with()->andReturn(true); - $backups = array($backup); - - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $image->shouldReceive('getBackups')->once()->andReturn($backups); - $command = new DestroyImagick(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/DriverTest.php b/tests/DriverTest.php deleted file mode 100644 index 0d8184ae0..000000000 --- a/tests/DriverTest.php +++ /dev/null @@ -1,60 +0,0 @@ -newImage(300, 200, '00ff00'); - $this->assertInstanceOf('\Intervention\Image\Image', $image); - $this->assertInstanceOf('\Intervention\Image\Gd\Driver', $image->getDriver()); - $this->assertInternalType('resource', $image->getCore()); - } - - public function testNewImageImagick() - { - $driver = new ImagickDriver( - Mockery::mock('\Intervention\Image\Imagick\Decoder'), - Mockery::mock('\Intervention\Image\Imagick\Encoder') - ); - - $image = $driver->newImage(300, 200, '00ff00'); - $this->assertInstanceOf('\Intervention\Image\Image', $image); - $this->assertInstanceOf('\Intervention\Image\Imagick\Driver', $image->getDriver()); - $this->assertInstanceOf('\Imagick', $image->getCore()); - } - - public function testParseColorGd() - { - $driver = new GdDriver( - Mockery::mock('\Intervention\Image\Gd\Decoder'), - Mockery::mock('\Intervention\Image\Gd\Encoder') - ); - - $color = $driver->parseColor('00ff00'); - $this->assertInstanceOf('\Intervention\Image\Gd\Color', $color); - } - - public function testParseColorImagick() - { - $driver = new ImagickDriver( - Mockery::mock('\Intervention\Image\Imagick\Decoder'), - Mockery::mock('\Intervention\Image\Imagick\Encoder') - ); - - $color = $driver->parseColor('00ff00'); - $this->assertInstanceOf('\Intervention\Image\Imagick\Color', $color); - } -} diff --git a/tests/EllipseCommandTest.php b/tests/EllipseCommandTest.php deleted file mode 100644 index 8ac42e007..000000000 --- a/tests/EllipseCommandTest.php +++ /dev/null @@ -1,42 +0,0 @@ -shouldReceive('getDriverName')->once()->andReturn('Gd'); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $command = new EllipseCommand(array(250, 150, 10, 20)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertFalse($command->hasOutput()); - } - - public function testImagick() - { - $imagick = Mockery::mock('\Imagick'); - $imagick->shouldReceive('drawimage'); - $driver = Mockery::mock('\Intervention\Image\Imagick\Driver'); - $driver->shouldReceive('getDriverName')->once()->andReturn('Imagick'); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - - $command = new EllipseCommand(array(250, 150, 10, 20)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertFalse($command->hasOutput()); - } - -} diff --git a/tests/EllipseShapeTest.php b/tests/EllipseShapeTest.php deleted file mode 100644 index 773373f69..000000000 --- a/tests/EllipseShapeTest.php +++ /dev/null @@ -1,54 +0,0 @@ -assertInstanceOf('Intervention\Image\Gd\Shapes\EllipseShape', $ellipse); - $this->assertEquals(250, $ellipse->width); - $this->assertEquals(150, $ellipse->height); - - } - - public function testGdApplyToImage() - { - $core = imagecreatetruecolor(300, 200); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $ellipse = new EllipseGd(250, 150); - $result = $ellipse->applyToImage($image, 10, 20); - $this->assertInstanceOf('Intervention\Image\Gd\Shapes\EllipseShape', $ellipse); - $this->assertTrue($result); - } - - public function testImagickConstructor() - { - $ellipse = new EllipseImagick(250, 150); - $this->assertInstanceOf('Intervention\Image\Imagick\Shapes\EllipseShape', $ellipse); - $this->assertEquals(250, $ellipse->width); - $this->assertEquals(150, $ellipse->height); - - } - - public function testImagickApplyToImage() - { - $core = Mockery::mock('\Imagick'); - $core->shouldReceive('drawimage')->once(); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $ellipse = new EllipseImagick(250, 150); - $result = $ellipse->applyToImage($image, 10, 20); - $this->assertInstanceOf('Intervention\Image\Imagick\Shapes\EllipseShape', $ellipse); - $this->assertTrue($result); - } - -} diff --git a/tests/EncoderTest.php b/tests/EncoderTest.php deleted file mode 100644 index 0b7eba8b3..000000000 --- a/tests/EncoderTest.php +++ /dev/null @@ -1,252 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, 'jpg', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('image/jpeg; charset=binary', $this->getMime($encoder->result)); - } - - public function testProcessPngGd() - { - $core = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $encoder = new GdEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, 'png', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('image/png; charset=binary', $this->getMime($encoder->result)); - } - - public function testProcessGifGd() - { - $core = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $encoder = new GdEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, 'gif', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('image/gif; charset=binary', $this->getMime($encoder->result)); - } - - /** - * @expectedException \Intervention\Image\Exception\NotSupportedException - */ - public function testProcessTiffGd() - { - $core = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $encoder = new GdEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $img = $encoder->process($image, 'tif', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - } - - /** - * @expectedException \Intervention\Image\Exception\NotSupportedException - */ - public function testProcessBmpGd() - { - $core = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $encoder = new GdEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $img = $encoder->process($image, 'bmp', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - } - - /** - * @expectedException \Intervention\Image\Exception\NotSupportedException - */ - public function testProcessIcoGd() - { - $core = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $encoder = new GdEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $img = $encoder->process($image, 'ico', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - } - - /** - * @expectedException \Intervention\Image\Exception\NotSupportedException - */ - public function testProcessPsdGd() - { - $core = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $encoder = new GdEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $img = $encoder->process($image, 'psd', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - } - - public function testProcessUnknownWithMimeGd() - { - $core = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $encoder = new GdEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->mime = 'image/jpeg'; - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, null); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('image/jpeg; charset=binary', $this->getMime($encoder->result)); - } - - public function testProcessUnknownGd() - { - $core = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $encoder = new GdEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, null); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('image/jpeg; charset=binary', $this->getMime($encoder->result)); - } - - public function testProcessJpegImagick() - { - $core = $this->getImagickMock('jpeg'); - $encoder = new ImagickEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, 'jpg', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('mock-jpeg', $encoder->result); - } - - public function testProcessPngImagick() - { - $core = $this->getImagickMock('png'); - $encoder = new ImagickEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, 'png', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('mock-png', $encoder->result); - } - - public function testProcessGifImagick() - { - $core = $this->getImagickMock('gif'); - $encoder = new ImagickEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, 'gif', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('mock-gif', $encoder->result); - } - - public function testProcessTiffImagick() - { - $core = $this->getImagickMock('tiff'); - $encoder = new ImagickEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, 'tiff', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('mock-tiff', $encoder->result); - } - - public function testProcessBmpImagick() - { - $core = $this->getImagickMock('bmp'); - $encoder = new ImagickEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, 'bmp', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('mock-bmp', $encoder->result); - } - - public function testProcessIcoImagick() - { - $core = $this->getImagickMock('ico'); - $encoder = new ImagickEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, 'ico', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('mock-ico', $encoder->result); - } - - public function testProcessPsdImagick() - { - $core = $this->getImagickMock('psd'); - $encoder = new ImagickEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, 'psd', 90); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('mock-psd', $encoder->result); - } - - public function testProcessUnknownWithMimeImagick() - { - $core = $this->getImagickMock('jpeg'); - $encoder = new ImagickEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->mime = 'image/jpeg'; - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, null); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('mock-jpeg', $encoder->result); - } - - public function testProcessUnknownImagick() - { - $core = $this->getImagickMock('jpeg'); - $encoder = new ImagickEncoder; - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $image->shouldReceive('setEncoded')->once()->andReturn($image); - $img = $encoder->process($image, null); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('mock-jpeg', $encoder->result); - } - - public function getImagickMock($type) - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('setformat')->with($type)->once(); - $imagick->shouldReceive('setimageformat')->once(); - $imagick->shouldReceive('setcompression')->once(); - $imagick->shouldReceive('setimagecompression')->once(); - $imagick->shouldReceive('setcompressionquality'); - $imagick->shouldReceive('setimagecompressionquality'); - $imagick->shouldReceive('setimagebackgroundcolor'); - $imagick->shouldReceive('setbackgroundcolor'); - $imagick->shouldReceive('mergeimagelayers')->andReturn($imagick); - $imagick->shouldReceive('getimagesblob')->once()->andReturn(sprintf('mock-%s', $type)); - return $imagick; - } - - public function getMime($data) - { - $finfo = new finfo(FILEINFO_MIME); - return $finfo->buffer($data); - } -} diff --git a/tests/ExifCommandTest.php b/tests/ExifCommandTest.php deleted file mode 100644 index 7e7cd31df..000000000 --- a/tests/ExifCommandTest.php +++ /dev/null @@ -1,111 +0,0 @@ -dirname = __DIR__.'/images'; - $image->basename = 'exif.jpg'; - $command = new ExifCommand(array()); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertInternalType('array', $command->getOutput()); - } - - public function testFetchDefined() - { - $image = new Image; - $image->dirname = __DIR__.'/images'; - $image->basename = 'exif.jpg'; - $command = new ExifCommand(array('Artist')); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertEquals('Oliver Vogel', $command->getOutput()); - } - - public function testFetchNonExisting() - { - $image = new Image; - $image->dirname = __DIR__.'/images'; - $image->basename = 'exif.jpg'; - $command = new ExifCommand(array('xxx')); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertEquals(null, $command->getOutput()); - } - - public function testFetchFromPng() - { - $image = new Image; - $image->dirname = __DIR__.'/images'; - $image->basename = 'star.png'; - $command = new ExifCommand(array('Orientation')); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertEquals(null, $command->getOutput()); - } - - public function testImagickFetchAll() - { - $image = $this->imagick()->make(__DIR__.'/images/exif.jpg'); - $command = new \Intervention\Image\Imagick\Commands\ExifCommand(array()); - $command->dontPreferExtension(); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertInternalType('array', $command->getOutput()); - $this->assertEquals('Oliver Vogel', $command->getOutput()['Artist']); - } - - public function testImagickFetchDefined() - { - $image = $this->imagick()->make(__DIR__.'/images/exif.jpg'); - $command = new \Intervention\Image\Imagick\Commands\ExifCommand(array('Artist')); - $command->dontPreferExtension(); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertEquals('Oliver Vogel', $command->getOutput()); - } - - public function testImagickNonExisting() - { - $image = $this->imagick()->make(__DIR__.'/images/exif.jpg'); - $command = new \Intervention\Image\Imagick\Commands\ExifCommand(array('xx')); - $command->dontPreferExtension(); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertEquals(null, $command->getOutput()); - } - - public function testImagickFallbackToExifExtenstion() - { - $image = $this->imagick()->make(__DIR__.'/images/exif.jpg'); - $command = new \Intervention\Image\Imagick\Commands\ExifCommand(array('Artist')); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertEquals('Oliver Vogel', $command->getOutput()); - } - - private function imagick() - { - return new \Intervention\Image\ImageManager(array( - 'driver' => 'imagick' - )); - } -} diff --git a/tests/Feature/AutoOrientationTest.php b/tests/Feature/AutoOrientationTest.php new file mode 100644 index 000000000..419849b92 --- /dev/null +++ b/tests/Feature/AutoOrientationTest.php @@ -0,0 +1,261 @@ +, 'colors': array}> $colors + */ + #[DataProvider('autoOrientationDisabledProvider')] + public function testAutoOrientationDisabled( + DriverInterface $driver, + string $path, + int $width, + int $height, + array $colors, + ): void { + $image = ImageManager::usingDriver($driver, autoOrientation: false)->decodePath($path); + $this->assertImageMatchesSpecs($width, $height, $colors, $image); + } + + /** + * @param array, 'colors': array}> $colors + */ + #[DataProvider('autoOrientationEnabledProvider')] + public function testAutoOrientationEnabled( + DriverInterface $driver, + string $path, + int $width, + int $height, + array $colors, + ): void { + $image = ImageManager::usingDriver($driver, autoOrientation: true)->decodePath($path); + $this->assertImageMatchesSpecs($width, $height, $colors, $image); + } + + /** + * @param array, 'colors': array}> $colors + */ + #[DataProvider('autoOrientationEnabledProvider')] + public function testAutoOrientationDisabledFollowUp( + DriverInterface $driver, + string $path, + int $width, + int $height, + array $colors, + ): void { + $image = ImageManager::usingDriver($driver, autoOrientation: false)->decodePath($path)->orient(); + $this->assertImageMatchesSpecs($width, $height, $colors, $image); + } + + /** + * @param array, 'colors': array}> $colors + */ + #[DataProvider('autoOrientationEnabledProvider')] + public function testAutoOrientationEnabledFollowUp( + DriverInterface $driver, + string $path, + int $width, + int $height, + array $colors, + ): void { + $image = ImageManager::usingDriver($driver, autoOrientation: true)->decodePath($path)->orient(); + $this->assertImageMatchesSpecs($width, $height, $colors, $image); + } + + /** + * @param array, 'colors': array}> $colors + */ + private function assertImageMatchesSpecs(int $width, int $height, array $colors, ImageInterface $image): void + { + $this->assertEquals($width, $image->width()); + $this->assertEquals($height, $image->height()); + foreach ($colors as $color) { + $this->assertColor( + $color['color'][0], + $color['color'][1], + $color['color'][2], + $color['color'][3], + $image->colorAt($color['position'][0], $color['position'][1]), + 3 + ); + } + } + + /** + * Provide image test information for pending orientation adjustment. + */ + public static function autoOrientationDisabledProvider(): Generator + { + $drivers = [GdDriver::class, ImagickDriver::class]; + + foreach ($drivers as $driver) { + yield [ + new $driver(), + Resource::create('orientation/landscape_0.jpg')->path(), + 60, + 30, + [ + ['position' => self::LANDSCAPE_TOP_LEFT, 'color' => [255, 0, 0, 255]], + ['position' => self::LANDSCAPE_TOP_RIGHT, 'color' => [0, 255, 0, 255]], + ['position' => self::LANDSCAPE_BOTTOM_LEFT, 'color' => [0, 0, 255, 255]], + ['position' => self::LANDSCAPE_BOTTOM_RIGHT, 'color' => [51, 51, 51, 255]], + ] + ]; + + yield [ + new $driver(), + Resource::create('orientation/landscape_1.jpg')->path(), + 60, + 30, + [ + ['position' => self::LANDSCAPE_TOP_LEFT, 'color' => [255, 0, 0, 255]], + ['position' => self::LANDSCAPE_TOP_RIGHT, 'color' => [0, 255, 0, 255]], + ['position' => self::LANDSCAPE_BOTTOM_LEFT, 'color' => [0, 0, 255, 255]], + ['position' => self::LANDSCAPE_BOTTOM_RIGHT, 'color' => [51, 51, 51, 255]], + ] + ]; + + yield [ + new $driver(), + Resource::create('orientation/landscape_2.jpg')->path(), + 60, + 30, + [ + ['position' => self::LANDSCAPE_TOP_LEFT, 'color' => [0, 255, 0, 255]], + ['position' => self::LANDSCAPE_TOP_RIGHT, 'color' => [255, 0, 0, 255]], + ['position' => self::LANDSCAPE_BOTTOM_LEFT, 'color' => [51, 51, 51, 255]], + ['position' => self::LANDSCAPE_BOTTOM_RIGHT, 'color' => [0, 0, 255, 255]], + ] + ]; + + yield [ + new $driver(), + Resource::create('orientation/landscape_3.jpg')->path(), + 60, + 30, + [ + ['position' => self::LANDSCAPE_TOP_LEFT, 'color' => [51, 51, 51, 255]], + ['position' => self::LANDSCAPE_TOP_RIGHT, 'color' => [0, 0, 255, 255]], + ['position' => self::LANDSCAPE_BOTTOM_LEFT, 'color' => [0, 255, 0, 255]], + ['position' => self::LANDSCAPE_BOTTOM_RIGHT, 'color' => [255, 0, 0, 255]], + ] + ]; + + yield [ + new $driver(), + Resource::create('orientation/landscape_4.jpg')->path(), + 60, + 30, + [ + ['position' => self::LANDSCAPE_TOP_LEFT, 'color' => [0, 0, 255, 255]], + ['position' => self::LANDSCAPE_TOP_RIGHT, 'color' => [51, 51, 51, 255]], + ['position' => self::LANDSCAPE_BOTTOM_LEFT, 'color' => [255, 0, 0, 255]], + ['position' => self::LANDSCAPE_BOTTOM_RIGHT, 'color' => [0, 255, 0, 255]], + ] + ]; + + yield [ + new $driver(), + Resource::create('orientation/landscape_5.jpg')->path(), + 30, + 60, + [ + ['position' => self::PORTRAIT_TOP_LEFT, 'color' => [255, 0, 0, 255]], + ['position' => self::PORTRAIT_TOP_RIGHT, 'color' => [0, 0, 255, 255]], + ['position' => self::PORTRAIT_BOTTOM_LEFT, 'color' => [0, 255, 0, 255]], + ['position' => self::PORTRAIT_BOTTOM_RIGHT, 'color' => [51, 51, 51, 255]], + ] + ]; + + yield [ + new $driver(), + Resource::create('orientation/landscape_6.jpg')->path(), + 30, + 60, + [ + ['position' => self::PORTRAIT_TOP_LEFT, 'color' => [0, 255, 0, 255]], + ['position' => self::PORTRAIT_TOP_RIGHT, 'color' => [51, 51, 51, 255]], + ['position' => self::PORTRAIT_BOTTOM_LEFT, 'color' => [255, 0, 0, 255]], + ['position' => self::PORTRAIT_BOTTOM_RIGHT, 'color' => [0, 0, 255, 255]], + ] + ]; + + yield [ + new $driver(), + Resource::create('orientation/landscape_7.jpg')->path(), + 30, + 60, + [ + ['position' => self::PORTRAIT_TOP_LEFT, 'color' => [51, 51, 51, 255]], + ['position' => self::PORTRAIT_TOP_RIGHT, 'color' => [0, 255, 0, 255]], + ['position' => self::PORTRAIT_BOTTOM_LEFT, 'color' => [0, 0, 255, 255]], + ['position' => self::PORTRAIT_BOTTOM_RIGHT, 'color' => [255, 0, 0, 255]], + ] + ]; + + yield [ + new $driver(), + Resource::create('orientation/landscape_8.jpg')->path(), + 30, + 60, + [ + ['position' => self::PORTRAIT_TOP_LEFT, 'color' => [0, 0, 255, 255]], + ['position' => self::PORTRAIT_TOP_RIGHT, 'color' => [255, 0, 0, 255]], + ['position' => self::PORTRAIT_BOTTOM_LEFT, 'color' => [51, 51, 51, 255]], + ['position' => self::PORTRAIT_BOTTOM_RIGHT, 'color' => [0, 255, 0, 255]], + ] + ]; + } + } + + /** + * Provide image test information for correct orientation. + */ + public static function autoOrientationEnabledProvider(): Generator + { + $drivers = [GdDriver::class, ImagickDriver::class]; + $width = 60; + $height = 30; + $colors = [ + ['position' => self::LANDSCAPE_TOP_LEFT, 'color' => [255, 0, 0, 255]], + ['position' => self::LANDSCAPE_TOP_RIGHT, 'color' => [0, 255, 0, 255]], + ['position' => self::LANDSCAPE_BOTTOM_LEFT, 'color' => [0, 0, 255, 255]], + ['position' => self::LANDSCAPE_BOTTOM_RIGHT, 'color' => [51, 51, 51, 255]], + ]; + + foreach ($drivers as $driver) { + yield [new $driver(), Resource::create('orientation/landscape_0.jpg')->path(), $width, $height, $colors]; + yield [new $driver(), Resource::create('orientation/landscape_1.jpg')->path(), $width, $height, $colors]; + yield [new $driver(), Resource::create('orientation/landscape_2.jpg')->path(), $width, $height, $colors]; + yield [new $driver(), Resource::create('orientation/landscape_3.jpg')->path(), $width, $height, $colors]; + yield [new $driver(), Resource::create('orientation/landscape_4.jpg')->path(), $width, $height, $colors]; + yield [new $driver(), Resource::create('orientation/landscape_5.jpg')->path(), $width, $height, $colors]; + yield [new $driver(), Resource::create('orientation/landscape_6.jpg')->path(), $width, $height, $colors]; + yield [new $driver(), Resource::create('orientation/landscape_7.jpg')->path(), $width, $height, $colors]; + yield [new $driver(), Resource::create('orientation/landscape_8.jpg')->path(), $width, $height, $colors]; + } + } +} diff --git a/tests/Feature/Gd/ConvertPngGif.php b/tests/Feature/Gd/ConvertPngGif.php new file mode 100644 index 000000000..37c2f8620 --- /dev/null +++ b/tests/Feature/Gd/ConvertPngGif.php @@ -0,0 +1,25 @@ +decodeBinary( + $this->readTestImage('circle.png')->encodeUsingFormat(Format::GIF) + ); + + $this->assertTransparency($converted->colorAt(0, 0)); + $this->assertColor(4, 2, 4, 255, $converted->colorAt(25, 25), 4); + } +} diff --git a/tests/Feature/Imagick/ConvertPngGif.php b/tests/Feature/Imagick/ConvertPngGif.php new file mode 100644 index 000000000..895eb621d --- /dev/null +++ b/tests/Feature/Imagick/ConvertPngGif.php @@ -0,0 +1,25 @@ +decodeBinary( + $this->readTestImage('circle.png')->encodeUsingFormat(Format::GIF) + ); + + $this->assertTransparency($converted->colorAt(0, 0)); + $this->assertColor(4, 2, 4, 255, $converted->colorAt(25, 25), 4); + } +} diff --git a/tests/Feature/Imagick/CropResizePngTest.php b/tests/Feature/Imagick/CropResizePngTest.php new file mode 100644 index 000000000..a2d2e95f2 --- /dev/null +++ b/tests/Feature/Imagick/CropResizePngTest.php @@ -0,0 +1,19 @@ +readTestImage('tile.png'); + $image->crop(100, 100); + $image->resize(200, 200); + $this->assertTransparency($image->colorAt(7, 22)); + $this->assertTransparency($image->colorAt(22, 7)); + } +} diff --git a/tests/FileTest.php b/tests/FileTest.php deleted file mode 100644 index d79d6497e..000000000 --- a/tests/FileTest.php +++ /dev/null @@ -1,27 +0,0 @@ -setFileInfoFromPath('tests/images/test.jpg'); - $this->assertEquals('tests/images', $file->dirname); - $this->assertEquals('test.jpg', $file->basename); - $this->assertEquals('jpg', $file->extension); - $this->assertEquals('test', $file->filename); - $this->assertEquals('image/jpeg', $file->mime); - } - - public function testBasePath() - { - $file = new File; - $this->assertNull(null, $file->basePath()); - - $file->dirname = 'foo'; - $file->basename = 'bar'; - $this->assertEquals('foo/bar', $file->basePath()); - } -} diff --git a/tests/FillCommandTest.php b/tests/FillCommandTest.php deleted file mode 100644 index b4d92c9fa..000000000 --- a/tests/FillCommandTest.php +++ /dev/null @@ -1,92 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $command = new FillGd(array('666666')); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testGdFillArray() - { - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $command = new FillGd(array(array(50, 50, 50))); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testGdFillArrayWithAlpha() - { - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $command = new FillGd(array(array(50, 50, 50, .50))); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testGdFillWithCoordinates() - { - $driver = Mockery::mock('\Intervention\Image\Gd\Driver'); - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->times(2)->andReturn($resource); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('setCore')->once(); - $driver->shouldReceive('newImage')->with(800, 600)->once()->andReturn($image); - $command = new FillGd(array('#666666', 0, 0)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagickFill() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('drawimage')->once()->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('getCore')->andReturn($imagick); - $command = new FillImagick(array('666666')); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagickFillWithCoordinates() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('getimagepixelcolor')->once()->andReturn('#000000'); - $imagick->shouldReceive('transparentpaintimage')->once()->andReturn(true); - $imagick->shouldReceive('compositeimage')->times(3)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->andReturn($imagick); - $image->shouldReceive('getWidth')->andReturn(800); - $image->shouldReceive('getHeight')->andReturn(600); - $command = new FillImagick(array('666666', 0, 0)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/FitCommandTest.php b/tests/FitCommandTest.php deleted file mode 100644 index bdc4c8081..000000000 --- a/tests/FitCommandTest.php +++ /dev/null @@ -1,92 +0,0 @@ -shouldReceive('getWidth')->times(2)->andReturn(800); - $cropped_size->shouldReceive('getHeight')->times(2)->andReturn(400); - $cropped_size->shouldReceive('resize')->with(200, 100, null)->once()->andReturn($cropped_size); - $cropped_size->pivot = Mockery::mock('\Intervention\Image\Point', array(0, 100)); - $original_size = Mockery::mock('\Intervention\Image\Size', array(800, 600)); - $original_size->shouldReceive('fit')->with(Mockery::any(), 'center')->once()->andReturn($cropped_size); - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getSize')->once()->andReturn($original_size); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('setCore')->once(); - $command = new FitGd(array(200, 100)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testGdFitWithPosition() - { - $cropped_size = Mockery::mock('\Intervention\Image\Size', array(800, 400)); - $cropped_size->shouldReceive('getWidth')->times(2)->andReturn(800); - $cropped_size->shouldReceive('getHeight')->times(2)->andReturn(400); - $cropped_size->shouldReceive('resize')->with(200, 100, null)->once()->andReturn($cropped_size); - $cropped_size->pivot = Mockery::mock('\Intervention\Image\Point', array(0, 100)); - $original_size = Mockery::mock('\Intervention\Image\Size', array(800, 600)); - $original_size->shouldReceive('fit')->with(Mockery::any(), 'top-left')->once()->andReturn($cropped_size); - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getSize')->once()->andReturn($original_size); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('setCore')->once(); - $command = new FitGd(array(200, 100, null, 'top-left')); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagickFit() - { - $cropped_size = Mockery::mock('\Intervention\Image\Size', array(800, 400)); - $cropped_size->shouldReceive('getWidth')->once()->andReturn(200); - $cropped_size->shouldReceive('getHeight')->once()->andReturn(100); - $cropped_size->shouldReceive('resize')->with(200, 100, null)->once()->andReturn($cropped_size); - $cropped_size->pivot = Mockery::mock('\Intervention\Image\Point', array(0, 100)); - $original_size = Mockery::mock('\Intervention\Image\Size', array(800, 600)); - $original_size->shouldReceive('fit')->with(Mockery::any(), 'center')->once()->andReturn($cropped_size); - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('cropimage')->with(800, 400, 0, 100)->andReturn(true); - $imagick->shouldReceive('scaleimage')->with(200, 100)->once()->andReturn(true); - $imagick->shouldReceive('setimagepage')->with(0, 0, 0, 0)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getSize')->once()->andReturn($original_size); - $image->shouldReceive('getCore')->times(3)->andReturn($imagick); - $command = new FitImagick(array(200, 100)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagickFitWithPosition() - { - $cropped_size = Mockery::mock('\Intervention\Image\Size', array(800, 400)); - $cropped_size->shouldReceive('getWidth')->once()->andReturn(200); - $cropped_size->shouldReceive('getHeight')->once()->andReturn(100); - $cropped_size->shouldReceive('resize')->with(200, 100, null)->once()->andReturn($cropped_size); - $cropped_size->pivot = Mockery::mock('\Intervention\Image\Point', array(0, 100)); - $original_size = Mockery::mock('\Intervention\Image\Size', array(800, 600)); - $original_size->shouldReceive('fit')->with(Mockery::any(), 'top-left')->once()->andReturn($cropped_size); - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('cropimage')->with(800, 400, 0, 100)->andReturn(true); - $imagick->shouldReceive('scaleimage')->with(200, 100)->once()->andReturn(true); - $imagick->shouldReceive('setimagepage')->with(0, 0, 0, 0)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getSize')->once()->andReturn($original_size); - $image->shouldReceive('getCore')->times(3)->andReturn($imagick); - $command = new FitImagick(array(200, 100, null, 'top-left')); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/FlipCommandTest.php b/tests/FlipCommandTest.php deleted file mode 100644 index a38070da5..000000000 --- a/tests/FlipCommandTest.php +++ /dev/null @@ -1,44 +0,0 @@ -shouldReceive('getSize')->once()->andReturn($size); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('setCore')->once(); - $command = new FlipGd(array('h')); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('flopimage')->with()->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new FlipImagick(array('h')); - $result = $command->execute($image); - $this->assertTrue($result); - - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('flipimage')->with()->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new FlipImagick(array('v')); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/GammaCommandTest.php b/tests/GammaCommandTest.php deleted file mode 100644 index 48d6bb9f6..000000000 --- a/tests/GammaCommandTest.php +++ /dev/null @@ -1,33 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $command = new GammaGd(array(1.4)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('gammaimage')->with(1.4)->once()->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new GammaImagick(array(1.4)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/GdColorTest.php b/tests/GdColorTest.php deleted file mode 100644 index 51d96c851..000000000 --- a/tests/GdColorTest.php +++ /dev/null @@ -1,295 +0,0 @@ -validateColor($c, 255, 255, 255, 127); - } - - public function testParseNull() - { - $c = new Color; - $c->parse(null); - $this->validateColor($c, 255, 255, 255, 127); - } - - public function testParseInteger() - { - $c = new Color; - $c->parse(850736919); - $this->validateColor($c, 181, 55, 23, 50); - } - - public function testParseArray() - { - $c = new Color; - $c->parse(array(181, 55, 23, 0.5)); - $this->validateColor($c, 181, 55, 23, 64); - } - - public function testParseHexString() - { - $c = new Color; - $c->parse('#b53717'); - $this->validateColor($c, 181, 55, 23, 0); - } - - public function testParseRgbaString() - { - $c = new Color; - $c->parse('rgba(181, 55, 23, 1)'); - $this->validateColor($c, 181, 55, 23, 0); - } - - public function testInitFromInteger() - { - $c = new Color; - $c->initFromInteger(0); - $this->validateColor($c, 0, 0, 0, 0); - $c->initFromInteger(2147483647); - $this->validateColor($c, 255, 255, 255, 127); - $c->initFromInteger(16777215); - $this->validateColor($c, 255, 255, 255, 0); - $c->initFromInteger(2130706432); - $this->validateColor($c, 0, 0, 0, 127); - $c->initFromInteger(850736919); - $this->validateColor($c, 181, 55, 23, 50); - } - - public function testInitFromArray() - { - $c = new Color; - $c->initFromArray(array(0, 0, 0, 0)); - $this->validateColor($c, 0, 0, 0, 127); - $c->initFromArray(array(0, 0, 0, 1)); - $this->validateColor($c, 0, 0, 0, 0); - $c->initFromArray(array(255, 255, 255, 1)); - $this->validateColor($c, 255, 255, 255, 0); - $c->initFromArray(array(255, 255, 255, 0)); - $this->validateColor($c, 255, 255, 255, 127); - $c->initFromArray(array(255, 255, 255, 0.5)); - $this->validateColor($c, 255, 255, 255, 64); - $c->initFromArray(array(0, 0, 0)); - $this->validateColor($c, 0, 0, 0, 0); - $c->initFromArray(array(255, 255, 255)); - $this->validateColor($c, 255, 255, 255, 0); - $c->initFromArray(array(181, 55, 23)); - $this->validateColor($c, 181, 55, 23, 0); - $c->initFromArray(array(181, 55, 23, 0.5)); - $this->validateColor($c, 181, 55, 23, 64); - } - - public function testInitFromHexString() - { - $c = new Color; - $c->initFromString('#cccccc'); - $this->validateColor($c, 204, 204, 204, 0); - $c->initFromString('#b53717'); - $this->validateColor($c, 181, 55, 23, 0); - $c->initFromString('ffffff'); - $this->validateColor($c, 255, 255, 255, 0); - $c->initFromString('ff00ff'); - $this->validateColor($c, 255, 0, 255, 0); - $c->initFromString('#000'); - $this->validateColor($c, 0, 0, 0, 0); - $c->initFromString('000'); - $this->validateColor($c, 0, 0, 0, 0); - } - - public function testInitFromRgbString() - { - $c = new Color; - $c->initFromString('rgb(1, 14, 144)'); - $this->validateColor($c, 1, 14, 144, 0); - $c->initFromString('rgb (255, 255, 255)'); - $this->validateColor($c, 255, 255, 255, 0); - $c->initFromString('rgb(0,0,0)'); - $this->validateColor($c, 0, 0, 0, 0); - $c->initFromString('rgba(0,0,0,0)'); - $this->validateColor($c, 0, 0, 0, 127); - $c->initFromString('rgba(0,0,0,0.5)'); - $this->validateColor($c, 0, 0, 0, 64); - $c->initFromString('rgba(255, 0, 0, 0.5)'); - $this->validateColor($c, 255, 0, 0, 64); - $c->initFromString('rgba(204, 204, 204, 0.9)'); - $this->validateColor($c, 204, 204, 204, 13); - } - - public function testInitFromRgb() - { - $c = new Color; - $c->initFromRgb(0, 0, 0); - $this->validateColor($c, 0, 0, 0, 0); - $c->initFromRgb(255, 255, 255); - $this->validateColor($c, 255, 255, 255, 0); - $c->initFromRgb(181, 55, 23); - $this->validateColor($c, 181, 55, 23, 0); - } - - public function testInitFromRgba() - { - $c = new Color; - $c->initFromRgba(0, 0, 0, 1); - $this->validateColor($c, 0, 0, 0, 0); - $c->initFromRgba(255, 255, 255, 1); - $this->validateColor($c, 255, 255, 255, 0); - $c->initFromRgba(181, 55, 23, 1); - $this->validateColor($c, 181, 55, 23, 0); - $c->initFromRgba(181, 55, 23, 0); - $this->validateColor($c, 181, 55, 23, 127); - $c->initFromRgba(181, 55, 23, 0.5); - $this->validateColor($c, 181, 55, 23, 64); - } - - public function testGetInt() - { - $c = new Color; - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals(2147483647, $i); - - $c = new Color(array(255, 255, 255)); - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 16777215); - - $c = new Color(array(255, 255, 255, 1)); - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 16777215); - - $c = new Color(array(181, 55, 23, 0.5)); - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 1085617943); - - $c = new Color(array(181, 55, 23, 1)); - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 11876119); - - $c = new Color(array(0, 0, 0, 0)); - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 2130706432); - } - - public function testGetHex() - { - $c = new Color; - $i = $c->getHex(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'ffffff'); - - $c = new Color(array(255, 255, 255, 1)); - $i = $c->getHex(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'ffffff'); - - $c = new Color(array(181, 55, 23, 0.5)); - $i = $c->getHex(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'b53717'); - - $c = new Color(array(0, 0, 0, 0)); - $i = $c->getHex('#'); - $this->assertInternalType('string', $i); - $this->assertEquals($i, '#000000'); - } - - public function testGetArray() - { - $c = new Color; - $i = $c->getArray(); - $this->assertInternalType('array', $i); - $this->assertEquals($i, array(255, 255, 255, 0)); - - $c = new Color(array(255, 255, 255, 1)); - $i = $c->getArray(); - $this->assertInternalType('array', $i); - $this->assertEquals($i, array(255, 255, 255, 1)); - - $c = new Color(array(181, 55, 23, 0.5)); - $i = $c->getArray(); - $this->assertInternalType('array', $i); - $this->assertEquals($i, array(181, 55, 23, 0.5)); - - $c = new Color(array(0, 0, 0, 1)); - $i = $c->getArray(); - $this->assertInternalType('array', $i); - $this->assertEquals($i, array(0, 0, 0, 1)); - } - - public function testGetRgba() - { - $c = new Color; - $i = $c->getRgba(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'rgba(255, 255, 255, 0.00)'); - - $c = new Color(array(255, 255, 255, 1)); - $i = $c->getRgba(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'rgba(255, 255, 255, 1.00)'); - - $c = new Color(array(181, 55, 23, 0.5)); - $i = $c->getRgba(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'rgba(181, 55, 23, 0.50)'); - - $c = new Color(array(0, 0, 0, 1)); - $i = $c->getRgba(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'rgba(0, 0, 0, 1.00)'); - } - - public function testDiffers() - { - $c1 = new Color(array(0, 0, 0)); - $c2 = new Color(array(0, 0, 0)); - $this->assertEquals(false, $c1->differs($c2)); - - $c1 = new Color(array(1, 0, 0)); - $c2 = new Color(array(0, 0, 0)); - $this->assertEquals(true, $c1->differs($c2)); - - $c1 = new Color(array(1, 0, 0)); - $c2 = new Color(array(0, 0, 0)); - $this->assertEquals(false, $c1->differs($c2, 10)); - - $c1 = new Color(array(127, 127, 127)); - $c2 = new Color(array(0, 0, 0)); - $this->assertEquals(true, $c1->differs($c2, 49)); - - $c1 = new Color(array(127, 127, 127)); - $c2 = new Color(array(0, 0, 0)); - $this->assertEquals(false, $c1->differs($c2, 50)); - } - - /** - * @expectedException \Intervention\Image\Exception\NotReadableException - */ - public function testParseUnknown() - { - $c = new Color('xxxxxxxxxxxxxxxxxxxx'); - } - - private function validateColor($obj, $r, $g, $b, $a) - { - $this->assertInstanceOf('Intervention\Image\Gd\Color', $obj); - $this->assertInternalType('int', $r); - $this->assertInternalType('int', $g); - $this->assertInternalType('int', $b); - $this->assertInternalType('int', $a); - $this->assertEquals($obj->r, $r); - $this->assertEquals($obj->g, $g); - $this->assertEquals($obj->b, $b); - $this->assertEquals($obj->a, $a); - } -} diff --git a/tests/GdSystemTest.php b/tests/GdSystemTest.php deleted file mode 100644 index 3b107411e..000000000 --- a/tests/GdSystemTest.php +++ /dev/null @@ -1,1646 +0,0 @@ -manager()->make('tests/images/circle.png'); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(50, $img->getHeight()); - $this->assertEquals('image/png', $img->mime); - $this->assertEquals('tests/images', $img->dirname); - $this->assertEquals('circle.png', $img->basename); - $this->assertEquals('png', $img->extension); - $this->assertEquals('circle', $img->filename); - $this->assertEquals('image/png', $img->mime); - } - - /** - * @expectedException \Intervention\Image\Exception\NotReadableException - */ - public function testMakeFromPathBroken() - { - $this->manager()->make('tests/images/broken.png'); - } - - public function testMakeFromString() - { - $str = file_get_contents('tests/images/circle.png'); - $img = $this->manager()->make($str); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(50, $img->getHeight()); - $this->assertEquals('image/png', $img->mime); - } - - public function testMakeFromResource() - { - $resource = imagecreatefrompng('tests/images/circle.png'); - $img = $this->manager()->make($resource); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(50, $img->getHeight()); - } - - public function testMakeFromDataUrl() - { - $img = $this->manager()->make('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGElEQVQYlWM8c+bMfwYiABMxikYVUk8hAHWzA3cRvs4UAAAAAElFTkSuQmCC'); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - } - - public function testMakeFromBase64() - { - $img = $this->manager()->make('iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGElEQVQYlWM8c+bMfwYiABMxikYVUk8hAHWzA3cRvs4UAAAAAElFTkSuQmCC'); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - } - - public function testCanvas() - { - $img = $this->manager()->canvas(30, 20); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(30, $img->getWidth()); - $this->assertEquals(20, $img->getHeight()); - $this->assertTransparentPosition($img, 0, 0); - } - - public function testCanvasWithSolidBackground() - { - $img = $this->manager()->canvas(30, 20, 'b53717'); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(30, $img->getWidth()); - $this->assertEquals(20, $img->getHeight()); - $this->assertEquals('#b53717', $img->pickColor(15, 15, 'hex')); - } - - public function testGetSize() - { - $img = $this->manager()->make('tests/images/tile.png'); - $size = $img->getSize(); - $this->assertInstanceOf('Intervention\Image\Size', $size); - $this->assertInternalType('int', $size->width); - $this->assertInternalType('int', $size->height); - $this->assertEquals(16, $size->width); - $this->assertEquals(16, $size->height); - } - - public function testResizeImage() - { - $img = $this->manager()->make('tests/images/circle.png'); - $img->resize(120, 150); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(120, $img->getWidth()); - $this->assertEquals(150, $img->getHeight()); - $this->assertTransparentPosition($img, 0, 0); - } - - public function testResizeImageOnlyWidth() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(120, null); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(120, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertTransparentPosition($img, 0, 15); - } - - public function testResizeImageOnlyHeight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(null, 150); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(150, $img->getHeight()); - $this->assertTransparentPosition($img, 15, 0); - } - - public function testResizeImageAutoHeight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(50, null, function ($constraint) { $constraint->aspectRatio(); }); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(50, $img->getHeight()); - $this->assertTransparentPosition($img, 30, 0); - } - - public function testResizeImageAutoWidth() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(null, 50, function ($constraint) { $constraint->aspectRatio(); }); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(50, $img->getHeight()); - $this->assertTransparentPosition($img, 30, 0); - } - - public function testResizeDominantWidth() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(100, 120, function ($constraint) { $constraint->aspectRatio(); }); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(100, $img->getWidth()); - $this->assertEquals(100, $img->getHeight()); - $this->assertTransparentPosition($img, 60, 0); - } - - public function testResizeImagePreserveSimpleUpsizing() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(100, 100, function ($constraint) { $constraint->aspectRatio(); $constraint->upsize(); }); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertTransparentPosition($img, 15, 0); - } - - public function testWidenImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->widen(100); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(100, $img->getWidth()); - $this->assertEquals(100, $img->getHeight()); - $this->assertTransparentPosition($img, 60, 0); - } - - public function testWidenImageWithConstraint() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->widen(100, function ($constraint) {$constraint->upsize();}); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertTransparentPosition($img, 8, 0); - } - - public function testHeightenImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->heighten(100); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(100, $img->getWidth()); - $this->assertEquals(100, $img->getHeight()); - $this->assertTransparentPosition($img, 60, 0); - } - - public function testHeightenImageWithConstraint() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->heighten(100, function ($constraint) {$constraint->upsize();}); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('resource', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertTransparentPosition($img, 8, 0); - } - - public function testResizeCanvasCenter() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 4); - $this->assertColorAtPosition('#445160', $img, 5, 5); - $this->assertTransparentPosition($img, 0, 5); - $this->assertTransparentPosition($img, 5, 4); - } - - public function testResizeCanvasTopLeft() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'top-left'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertTransparentPosition($img, 0, 8); - $this->assertTransparentPosition($img, 8, 7); - } - - public function testResizeCanvasTop() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'top'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#445160', $img, 5, 8); - $this->assertTransparentPosition($img, 0, 8); - $this->assertTransparentPosition($img, 5, 7); - } - - public function testResizeCanvasTopRight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'top-right'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#445160', $img, 2, 8); - $this->assertTransparentPosition($img, 0, 8); - $this->assertTransparentPosition($img, 2, 7); - } - - public function testResizeCanvasLeft() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'left'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 4); - $this->assertColorAtPosition('#445160', $img, 8, 5); - $this->assertTransparentPosition($img, 0, 5); - $this->assertTransparentPosition($img, 8, 4); - } - - public function testResizeCanvasRight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'right'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 4); - $this->assertColorAtPosition('#445160', $img, 2, 5); - $this->assertTransparentPosition($img, 0, 5); - $this->assertTransparentPosition($img, 2, 4); - } - - public function testResizeCanvasBottomLeft() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'bottom-left'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 1); - $this->assertColorAtPosition('#445160', $img, 8, 2); - $this->assertTransparentPosition($img, 0, 2); - $this->assertTransparentPosition($img, 8, 1); - } - - public function testResizeCanvasBottomRight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'bottom-right'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 1); - $this->assertColorAtPosition('#445160', $img, 2, 2); - $this->assertTransparentPosition($img, 0, 2); - $this->assertTransparentPosition($img, 2, 1); - } - - public function testResizeCanvasBottom() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'bottom'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 1); - $this->assertColorAtPosition('#445160', $img, 5, 2); - $this->assertTransparentPosition($img, 0, 2); - $this->assertTransparentPosition($img, 5, 1); - } - - public function testResizeCanvasRelativeWithBackground() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(4, 4, 'center', true, '#ff00ff'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(20, $img->getWidth()); - $this->assertEquals(20, $img->getHeight()); - $this->assertColorAtPosition('#ff00ff', $img, 0, 0); - $this->assertColorAtPosition('#ff00ff', $img, 19, 19); - $this->assertColorAtPosition('#b4e000', $img, 2, 9); - $this->assertColorAtPosition('#445160', $img, 10, 10); - $this->assertTransparentPosition($img, 2, 10); - $this->assertTransparentPosition($img, 10, 9); - } - - public function testResizeCanvasJustWidth() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, null); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#445160', $img, 5, 8); - $this->assertTransparentPosition($img, 0, 8); - $this->assertTransparentPosition($img, 5, 7); - } - - public function testResizeCanvasJustHeight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(null, 10); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 4); - $this->assertColorAtPosition('#445160', $img, 8, 5); - $this->assertTransparentPosition($img, 0, 5); - $this->assertTransparentPosition($img, 8, 4); - } - - public function testResizeCanvasSmallerWidthLargerHeight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 20); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(20, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 9); - $this->assertColorAtPosition('#445160', $img, 5, 10); - $this->assertTransparentPosition($img, 0, 10); - $this->assertTransparentPosition($img, 5, 9); - } - - public function testResizeCanvasLargerWidthSmallerHeight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(20, 10); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(20, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 2, 4); - $this->assertColorAtPosition('#445160', $img, 10, 5); - $this->assertTransparentPosition($img, 0, 0); - $this->assertTransparentPosition($img, 2, 5); - $this->assertTransparentPosition($img, 10, 4); - } - - public function testResizeCanvasNegative() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(-4, -4); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(12, $img->getWidth()); - $this->assertEquals(12, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 5); - $this->assertColorAtPosition('#445160', $img, 6, 6); - $this->assertTransparentPosition($img, 0, 6); - $this->assertTransparentPosition($img, 6, 5); - } - - public function testResizeCanvasLargerHeightAutoWidth() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(null, 20, 'bottom-left', false, '#ff00ff'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(20, $img->getHeight()); - $this->assertColorAtPosition('#ff00ff', $img, 0, 0); - $this->assertColorAtPosition('#b4e000', $img, 0, 4); - $this->assertColorAtPosition('#b4e000', $img, 0, 11); - $this->assertColorAtPosition('#445160', $img, 8, 12); - $this->assertTransparentPosition($img, 0, 12); - $this->assertTransparentPosition($img, 8, 11); - } - - public function testResizeCanvasBorderNonRelative() - { - $img = $this->manager()->canvas(1, 1, 'ff0000'); - $img->resizeCanvas(17, 17, 'center', false, '333333'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(17, $img->getWidth()); - $this->assertEquals(17, $img->getHeight()); - $this->assertColorAtPosition('#333333', $img, 0, 0); - $this->assertColorAtPosition('#333333', $img, 5, 5); - $this->assertColorAtPosition('#333333', $img, 7, 7); - $this->assertColorAtPosition('#ff0000', $img, 8, 8); - } - - public function testCropImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->crop(6, 6); // should be centered without pos. - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(6, $img->getWidth()); - $this->assertEquals(6, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 2); - $this->assertColorAtPosition('#445160', $img, 3, 3); - $this->assertTransparentPosition($img, 0, 3); - $this->assertTransparentPosition($img, 3, 2); - } - - public function testCropImageWithPosition() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->crop(4, 4, 7, 7); // should be centered without pos. - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(4, $img->getWidth()); - $this->assertEquals(4, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 1, 1); - $this->assertTransparentPosition($img, 0, 1); - $this->assertTransparentPosition($img, 1, 0); - } - - public function testFitImageSquare() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->fit(6); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(6, $img->getWidth()); - $this->assertEquals(6, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 2); - $this->assertColorAtPosition('#445060', $img, 3, 3); - $this->assertTransparentPosition($img, 0, 3); - $this->assertTransparentPosition($img, 3, 2); - } - - public function testFitImageRectangle() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->fit(12, 6); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(12, $img->getWidth()); - $this->assertEquals(6, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 2); - $this->assertColorAtPosition('#445160', $img, 6, 3); - $this->assertTransparentPosition($img, 0, 3); - $this->assertTransparentPosition($img, 6, 2); - } - - public function testFitImageWithConstraintUpsize() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->fit(300, 150, function ($constraint) {$constraint->upsize();}); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(25, $img->getHeight()); - $this->assertColorAtPosition('#00aef0', $img, 0, 0); - $this->assertColorAtPosition('#afa94c', $img, 17, 0); - $this->assertColorAtPosition('#ffa601', $img, 24, 0); - } - - public function testFlipImageHorizontal() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->flip('h'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 8, 7); - $this->assertColorAtPosition('#445160', $img, 0, 8); - $this->assertTransparentPosition($img, 0, 7); - $this->assertTransparentPosition($img, 8, 8); - } - - public function testFlipImageVertical() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->flip('v'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 7); - $this->assertTransparentPosition($img, 0, 7); - $this->assertTransparentPosition($img, 8, 8); - } - - public function testRotateImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->rotate(90); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 7); - $this->assertTransparentPosition($img, 0, 7); - $this->assertTransparentPosition($img, 8, 8); - } - - public function testInsertImage() - { - $watermark = $this->manager()->canvas(16, 16, '#0000ff'); // create watermark - - // top-left anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top-left', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(0, 0, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(16, 16, 'hex')); - - // top-left anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top-left', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(9, 9, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(10, 10, 'hex')); - - // top anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(0, 0, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(23, 15, 'hex')); - - // top anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(18, 10, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(31, 26, 'hex')); - - // top-right anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top-right', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(15, 0, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(31, 0, 'hex')); - - // top-right anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top-right', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(6, 9, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(21, 25, 'hex')); - - // left anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'left', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(15, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(0, 7, 'hex')); - - // left anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'left', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(8, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(10, 7, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(25, 23, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(25, 8, 'hex')); - - // right anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'right', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(31, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(15, 15, 'hex')); - - // right anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'right', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(5, 8, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(22, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(21, 7, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(6, 8, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(21, 23, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(6, 23, 'hex')); - - // bottom-left anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom-left', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(15, 31, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(0, 15, 'hex')); - - // bottom-left anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom-left', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(10, 21, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(9, 20, 'hex')); - - // bottom anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(8, 16, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(8, 15, 'hex')); - - // bottom anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(5, 8, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(23, 22, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(24, 21, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(7, 6, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(8, 6, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(23, 21, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(23, 6, 'hex')); - - // bottom-right anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom-right', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(16, 16, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(15, 16, 'hex')); - - // bottom-right anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom-right', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(21, 21, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(22, 22, 'hex')); - - // center anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'center', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(23, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(8, 7, 'hex')); - - // center anchor coordinates / coordinates will be ignored for center - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'center', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(23, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(8, 7, 'hex')); - } - - public function testInsertWithAlphaChannel() - { - $img = $this->manager()->canvas(50, 50, 'ff0000'); - $img->insert('tests/images/circle.png'); - $this->assertColorAtPosition('#ff0000', $img, 0, 0); - $this->assertColorAtPosition('#320000', $img, 30, 30); - } - - public function testInsertAfterResize() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->resize(16, 16)->insert('tests/images/tile.png'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#00aef0', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#ffa601', $img, 8, 7); - } - - public function testInsertResource() - { - $resource = imagecreatefrompng('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->insert($resource); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#00aef0', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 24, 24); - } - - public function testInsertBinary() - { - $data = file_get_contents('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->insert($data); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#00aef0', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 24, 24); - } - - public function testInsertInterventionImage() - { - $obj = $this->manager()->make('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->insert($obj); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#00aef0', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 24, 24); - } - - public function testOpacity() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->opacity(50); - $checkColor = $img->pickColor(7, 7, 'array'); - $this->assertEquals($checkColor[0], 180); - $this->assertEquals($checkColor[1], 224); - $this->assertEquals($checkColor[2], 0); - $this->assertEquals($checkColor[3], 0.5); - $checkColor = $img->pickColor(8, 8, 'array'); - $this->assertEquals($checkColor[0], 68); - $this->assertEquals($checkColor[1], 81); - $this->assertEquals($checkColor[2], 96); - $this->assertEquals($checkColor[3], 0.5); - $this->assertTransparentPosition($img, 0, 11); - } - - public function testMaskImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->mask('tests/images/gradient.png'); - $this->assertTransparentPosition($img, 0, 0); - $this->assertTransparentPosition($img, 23, 23); - $checkColor = $img->pickColor(23, 24, 'array'); - $this->assertEquals($checkColor[0], 255); - $this->assertEquals($checkColor[1], 166); - $this->assertEquals($checkColor[2], 1); - $this->assertEquals($checkColor[3], 0.97); - $checkColor = $img->pickColor(39, 25, 'array'); - $this->assertEquals($checkColor[0], 0); - $this->assertEquals($checkColor[1], 174); - $this->assertEquals($checkColor[2], 240); - $this->assertEquals($checkColor[3], 0.32); - } - - public function testMaskImageWithAlpha() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->mask('tests/images/star.png', true); - $this->assertTransparentPosition($img, 0, 0); - $this->assertTransparentPosition($img, 16, 16); - $checkColor = $img->pickColor(18, 18, 'array'); - $this->assertEquals($checkColor[0], 255); - $this->assertEquals($checkColor[1], 166); - $this->assertEquals($checkColor[2], 1); - $this->assertEquals($checkColor[3], 0.65); - $checkColor = $img->pickColor(24, 10, 'array'); - $this->assertEquals($checkColor[0], 0); - $this->assertEquals($checkColor[1], 174); - $this->assertEquals($checkColor[2], 240); - $this->assertEquals($checkColor[3], 0.80); - } - - public function testPixelateImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->pixelate(20); - $this->assertInstanceOf('Intervention\Image\Image', $img); - } - - public function testGreyscaleImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->greyscale(); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertTransparentPosition($img, 8, 0); - $this->assertColorAtPosition('#b9b9b9', $img, 0, 0); - } - - public function testInvertImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->invert(); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertTransparentPosition($img, 8, 0); - $this->assertColorAtPosition('#4b1fff', $img, 0, 0); - } - - public function testBlurImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->blur(1); - $this->assertInstanceOf('Intervention\Image\Image', $img); - } - - public function testFillImageWithColor() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->fill('b53717'); - $this->assertColorAtPosition('#b53717', $img, 0, 0); - $this->assertColorAtPosition('#b53717', $img, 15, 15); - } - - public function testFillImageWithColorAtPosition() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->fill('b53717', 0, 0); - $this->assertTransparentPosition($img, 0, 8); - $this->assertColorAtPosition('#b53717', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 15, 15); - } - - public function testFillImageWithResource() - { - $resource = imagecreatefrompng('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->fill($resource, 0, 0); - $this->assertColorAtPosition('#b4e000', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 20, 20); - } - - public function testFillImageWithBinary() - { - $data = file_get_contents('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->fill($data, 0, 0); - $this->assertColorAtPosition('#b4e000', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 20, 20); - } - - public function testFillImageWithInterventionImage() - { - $obj = $this->manager()->make('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->fill($obj, 0, 0); - $this->assertColorAtPosition('#b4e000', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 20, 20); - } - - public function testPixelImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $coords = array(array(5, 5), array(12, 12)); - $img = $img->pixel('fdf5e4', $coords[0][0], $coords[0][1]); - $img = $img->pixel(array(255, 255, 255), $coords[1][0], $coords[1][1]); - $this->assertEquals('#fdf5e4', $img->pickColor($coords[0][0], $coords[0][1], 'hex')); - $this->assertEquals('#ffffff', $img->pickColor($coords[1][0], $coords[1][1], 'hex')); - } - - public function testTextImage() - { - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $img = $img->text('0', 3, 11); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('a9e2b15452b2a4637b65625188d206f6', $img->checksum()); - - - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $img = $img->text('0', 8, 2, function($font) { - $font->align('center'); - $font->valign('top'); - $font->color('000000'); - }); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('649f3f529d3931c56601155fd2680959', $img->checksum()); - - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $img = $img->text('0', 8, 8, function($font) { - $font->align('right'); - $font->valign('middle'); - $font->file(2); - $font->color('000000'); - }); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertEquals('c0dda67589c46a90d78a97b891a811ee', $img->checksum()); - } - - public function testRectangleImage() - { - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $img->rectangle(5, 5, 11, 11, function ($draw) { $draw->background('#ff0000'); $draw->border(1, '#0000ff'); }); - $this->assertEquals('e95487dcc29daf371a0e9190bff8dbfe', $img->checksum()); - } - - public function testLineImage() - { - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $img->line(0, 0, 15, 15, function ($draw) { $draw->color('#ff0000'); }); - $this->assertEquals('a6237d34f6e95f30d2fc91a46bd058e6', $img->checksum()); - } - - public function testEllipseImage() - { - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $img->ellipse(12, 8, 8, 8, function ($draw) { $draw->background('#ff0000'); $draw->border(1, '#0000ff'); }); - $this->assertEquals('080d9dd92ebe22f976c3c703cba33510', $img->checksum()); - } - - public function testCircleImage() - { - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $img->circle(12, 8, 8, function ($draw) { $draw->background('#ff0000'); $draw->border(1, '#0000ff'); }); - $this->assertEquals('c3bff06c20244ba14e898e39ea0efd76', $img->checksum()); - } - - public function testPolygonImage() - { - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $points = array(3, 3, 11, 11, 7, 13); - $img->polygon($points, function ($draw) { $draw->background('#ff0000'); $draw->border(1, '#0000ff'); }); - $this->assertEquals('e534ff90c8026f9317b99071fda01ed4', $img->checksum()); - } - - public function testResetImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->backup(); - $img->resize(30, 20); - $img->reset(); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - } - - public function testResetEmptyImage() - { - $img = $this->manager()->canvas(16, 16, '#0000ff'); - $img->backup(); - $img->resize(30, 20); - $img->fill('#ff0000'); - $img->reset(); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#0000ff', $img, 0, 0); - } - - public function testResetKeepTransparency() - { - $img = $this->manager()->make('tests/images/circle.png'); - $img->backup(); - $img->reset(); - $this->assertTransparentPosition($img, 0, 0); - } - - public function testResetToNamed() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->backup('original'); - $img->resize(30, 20); - $img->backup('30x20'); - - // reset to original - $img->reset('original'); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - - // reset to 30x20 - // $img->reset('30x20'); - // $this->assertEquals(30, $img->getWidth()); - // $this->assertEquals(20, $img->getHeight()); - - // reset to original again - $img->reset('original'); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - } - - public function testLimitColors() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->limitColors(4); - $this->assertLessThanOrEqual(5, imagecolorstotal($img->getCore())); - } - - public function testLimitColorsKeepTransparency() - { - $img = $this->manager()->make('tests/images/star.png'); - $img->limitColors(16); - $this->assertLessThanOrEqual(17, imagecolorstotal($img->getCore())); - $this->assertTransparentPosition($img, 0, 0); - $this->assertColorAtPosition('#0c02b4', $img, 6, 12); - $this->assertColorAtPosition('#fcbe04', $img, 22, 24); - } - - public function testLimitColorsKeepTransparencyWithMatte() - { - $img = $this->manager()->make('tests/images/star.png'); - $img->limitColors(64, '#00ff00'); - $this->assertLessThanOrEqual(65, imagecolorstotal($img->getCore())); - $this->assertTransparentPosition($img, 0, 0); - $this->assertColorAtPosition('#04f204', $img, 12, 10); - $this->assertColorAtPosition('#e40214', $img, 16, 21); - } - - public function testLimitColorsNullWithMatte() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->limitColors(null, '#ff00ff'); - $this->assertColorAtPosition('#b4e000', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#ff00ff', $img, 0, 8); - $this->assertColorAtPosition('#ff00ff', $img, 15, 0); - } - - public function testPickColorFromTrueColor() - { - $img = $this->manager()->make('tests/images/star.png'); - $c = $img->pickColor(0, 0); - $this->assertEquals(255, $c[0]); - $this->assertEquals(255, $c[1]); - $this->assertEquals(255, $c[2]); - $this->assertEquals(0, $c[3]); - - $c = $img->pickColor(11, 11); - $this->assertEquals(34, $c[0]); - $this->assertEquals(0, $c[1]); - $this->assertEquals(160, $c[2]); - $this->assertEquals(0.46, $c[3]); - - $c = $img->pickColor(16, 16); - $this->assertEquals(231, $c[0]); - $this->assertEquals(0, $c[1]); - $this->assertEquals(18, $c[2]); - $this->assertEquals(1, $c[3]); - } - - public function testPickColorFromIndexed() - { - $img = $this->manager()->make('tests/images/tile.png'); - $c = $img->pickColor(0, 0); - $this->assertEquals(180, $c[0]); - $this->assertEquals(224, $c[1]); - $this->assertEquals(0, $c[2]); - $this->assertEquals(1, $c[3]); - - $c = $img->pickColor(8, 8); - $this->assertEquals(68, $c[0]); - $this->assertEquals(81, $c[1]); - $this->assertEquals(96, $c[2]); - $this->assertEquals(1, $c[3]); - - $c = $img->pickColor(0, 15); - $this->assertEquals(255, $c[0]); - $this->assertEquals(255, $c[1]); - $this->assertEquals(255, $c[2]); - $this->assertEquals(0, $c[3]); - } - - public function testPickColorFromPalette() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->limitColors(200); - - $c = $img->pickColor(0, 0); - $this->assertEquals(180, $c[0]); - $this->assertEquals(226, $c[1]); - $this->assertEquals(4, $c[2]); - $this->assertEquals(1, $c[3]); - - $c = $img->pickColor(8, 8); - $this->assertEquals(68, $c[0]); - $this->assertEquals(82, $c[1]); - $this->assertEquals(100, $c[2]); - $this->assertEquals(1, $c[3]); - - $c = $img->pickColor(0, 15); - $this->assertEquals(255, $c[0]); - $this->assertEquals(255, $c[1]); - $this->assertEquals(255, $c[2]); - $this->assertEquals(0, $c[3]); - } - - public function testInterlaceImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->interlace(); - $img->encode('png'); - $this->assertTrue((ord($img->encoded[28]) != '0')); - $img->interlace(false); - $img->encode('png'); - $this->assertFalse((ord($img->encoded[28]) != '0')); - } - - public function testGammaImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->gamma(1.6); - $this->assertColorAtPosition('#00c9f6', $img, 0, 0); - $this->assertColorAtPosition('#ffc308', $img, 24, 24); - } - - public function testBrightnessImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->brightness(35); - $this->assertColorAtPosition('#59ffff', $img, 0, 0); - $this->assertColorAtPosition('#ffff5a', $img, 24, 24); - } - - public function testContrastImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->contrast(35); - $this->assertColorAtPosition('#00d4ff', $img, 0, 0); - $this->assertColorAtPosition('#ffc500', $img, 24, 24); - } - - public function testColorizeImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->colorize(40, 25, -50); - $this->assertColorAtPosition('#66ee70', $img, 0, 0); - $this->assertColorAtPosition('#ffe600', $img, 24, 24); - } - - public function testTrimGradient() - { - $canvas = $this->manager()->make('tests/images/gradient.png'); - - $img = clone $canvas; - $img->trim(); - $this->assertEquals($img->getWidth(), 46); - $this->assertEquals($img->getHeight(), 46); - - $img = clone $canvas; - $img->trim(null, null, 10); - $this->assertEquals($img->getWidth(), 38); - $this->assertEquals($img->getHeight(), 38); - - $img = clone $canvas; - $img->trim(null, null, 20); - $this->assertEquals($img->getWidth(), 34); - $this->assertEquals($img->getHeight(), 34); - - $img = clone $canvas; - $img->trim(null, null, 30); - $this->assertEquals($img->getWidth(), 30); - $this->assertEquals($img->getHeight(), 30); - - $img = clone $canvas; - $img->trim(null, null, 40); - $this->assertEquals($img->getWidth(), 26); - $this->assertEquals($img->getHeight(), 26); - - $img = clone $canvas; - $img->trim(null, null, 50); - $this->assertEquals($img->getWidth(), 22); - $this->assertEquals($img->getHeight(), 22); - - $img = clone $canvas; - $img->trim(null, null, 60); - $this->assertEquals($img->getWidth(), 20); - $this->assertEquals($img->getHeight(), 20); - - $img = clone $canvas; - $img->trim(null, null, 70); - $this->assertEquals($img->getWidth(), 16); - $this->assertEquals($img->getHeight(), 16); - - $img = clone $canvas; - $img->trim(null, null, 80); - $this->assertEquals($img->getWidth(), 12); - $this->assertEquals($img->getHeight(), 12); - - $img = clone $canvas; - $img->trim(null, null, 90); - $this->assertEquals($img->getWidth(), 8); - $this->assertEquals($img->getHeight(), 8); - } - - public function testTrimOnlyLeftAndRight() - { - $img = $this->manager()->make('tests/images/gradient.png'); - $img->trim(null, array('left', 'right'), 60); - $this->assertEquals($img->getWidth(), 20); - $this->assertEquals($img->getHeight(), 50); - } - - public function testTrimOnlyTopAndBottom() - { - $img = $this->manager()->make('tests/images/gradient.png'); - $img->trim(null, array('top', 'bottom'), 60); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 20); - } - - public function testTrimOnlyTop() - { - $img = $this->manager()->make('tests/images/gradient.png'); - $img->trim(null, 'top', 60); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 35); - } - - public function testTrimOnlyBottom() - { - $img = $this->manager()->make('tests/images/gradient.png'); - $img->trim(null, 'top', 60); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 35); - } - - public function testTrimWithFeather() - { - $canvas = $this->manager()->make('tests/images/trim.png'); - - $img = clone $canvas; - $feather = 5; - $img->trim(null, null, null, $feather); - $this->assertEquals($img->getWidth(), 28 + $feather * 2); - $this->assertEquals($img->getHeight(), 28 + $feather * 2); - - $img = clone $canvas; - $feather = 10; - $img->trim(null, null, null, $feather); - $this->assertEquals($img->getWidth(), 28 + $feather * 2); - $this->assertEquals($img->getHeight(), 28 + $feather * 2); - - $img = clone $canvas; - $feather = 20; // must respect original dimensions of image - $img->trim(null, null, null, $feather); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 50); - - $img = clone $canvas; - $feather = -5; - $img->trim(null, null, null, $feather); - $this->assertEquals($img->getWidth(), 28 + $feather * 2); - $this->assertEquals($img->getHeight(), 28 + $feather * 2); - - $img = clone $canvas; - $feather = -10; - $img->trim(null, null, null, $feather); - $this->assertEquals($img->getWidth(), 28 + $feather * 2); - $this->assertEquals($img->getHeight(), 28 + $feather * 2); - - // trim only left and right with feather - $img = clone $canvas; - $feather = 10; - $img->trim(null, array('left', 'right'), null, $feather); - $this->assertEquals($img->getWidth(), 28 + $feather * 2); - $this->assertEquals($img->getHeight(), 50); - - // trim only top and bottom with feather - $img = clone $canvas; - $feather = 10; - $img->trim(null, array('top', 'bottom'), null, $feather); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 28 + $feather * 2); - - // trim with tolerance and feather - $canvas = $this->manager()->make('tests/images/gradient.png'); - - $img = clone $canvas; - $feather = 2; - $img->trim(null, null, 10, $feather); - $this->assertEquals($img->getWidth(), 38 + $feather * 2); - $this->assertEquals($img->getHeight(), 38 + $feather * 2); - - $img = clone $canvas; - $feather = 5; - $img->trim(null, null, 10, $feather); - $this->assertEquals($img->getWidth(), 38 + $feather * 2); - $this->assertEquals($img->getHeight(), 38 + $feather * 2); - - $img = clone $canvas; - $feather = 10; // should respect original dimensions - $img->trim(null, null, 20, $feather); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 50); - } - - public function testEncodeDefault() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->encode(); - $this->assertInternalType('resource', imagecreatefromstring($img->encoded)); - } - - public function testEncodeJpeg() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->encode('jpg'); - $this->assertInternalType('resource', imagecreatefromstring($img->encoded)); - } - - public function testEncodeGif() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->encode('gif'); - $this->assertInternalType('resource', imagecreatefromstring($img->encoded)); - } - - public function testEncodeDataUrl() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->encode('data-url'); - $this->assertEquals('data:image/png;base64', substr($img->encoded, 0, 21)); - } - - public function testExifReadAll() - { - $img = $this->manager()->make('tests/images/exif.jpg'); - $data = $img->exif(); - $this->assertInternalType('array', $data); - $this->assertEquals(19, count($data)); - } - - public function testExifReadKey() - { - $img = $this->manager()->make('tests/images/exif.jpg'); - $data = $img->exif('Artist'); - $this->assertInternalType('string', $data); - $this->assertEquals('Oliver Vogel', $data); - } - - public function testExifReadNotExistingKey() - { - $img = $this->manager()->make('tests/images/exif.jpg'); - $data = $img->exif('xxx'); - $this->assertEquals(null, $data); - } - - public function testSaveImage() - { - $save_as = 'tests/tmp/foo.jpg'; - $img = $this->manager()->make('tests/images/trim.png'); - $img->save($save_as, 80); - $this->assertFileExists($save_as); - $this->assertEquals($img->dirname, 'tests/tmp'); - $this->assertEquals($img->basename, 'foo.jpg'); - $this->assertEquals($img->extension, 'jpg'); - $this->assertEquals($img->filename, 'foo'); - $this->assertEquals($img->mime, 'image/jpeg'); - @unlink($save_as); - - $save_as = 'tests/tmp/foo.png'; - $img = $this->manager()->make('tests/images/trim.png'); - $img->save($save_as); - $this->assertEquals($img->dirname, 'tests/tmp'); - $this->assertEquals($img->basename, 'foo.png'); - $this->assertEquals($img->extension, 'png'); - $this->assertEquals($img->filename, 'foo'); - $this->assertEquals($img->mime, 'image/png'); - $this->assertFileExists($save_as); - @unlink($save_as); - - $save_as = 'tests/tmp/foo.jpg'; - $img = $this->manager()->make('tests/images/trim.png'); - $img->save($save_as, 0); - $this->assertEquals($img->dirname, 'tests/tmp'); - $this->assertEquals($img->basename, 'foo.jpg'); - $this->assertEquals($img->extension, 'jpg'); - $this->assertEquals($img->filename, 'foo'); - $this->assertEquals($img->mime, 'image/jpeg'); - $this->assertFileExists($save_as); - @unlink($save_as); - } - - public function testSaveImageWithoutParameter() - { - $path = 'tests/tmp/bar.png'; - - // create temp. test image (red) - $img = $this->manager()->canvas(16, 16, '#ff0000'); - $img->save($path); - $img->destroy(); - - // open test image again - $img = $this->manager()->make($path); - $this->assertColorAtPosition('#ff0000', $img, 0, 0); - - // fill with green and save wthout paramater - $img->fill('#00ff00'); - $img->save(); - $img->destroy(); - - // re-open test image (should be green) - $img = $this->manager()->make($path); - $this->assertColorAtPosition('#00ff00', $img, 0, 0); - $img->destroy(); - - @unlink($path); - } - - public function testDestroy() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->backup(); - $img->destroy(); - // destroy should affect core - $this->assertEquals(get_resource_type($img->getCore()), 'Unknown'); - // destroy should affect backup - $this->assertEquals(get_resource_type($img->getBackup()), 'Unknown'); - } - - public function testStringConversion() - { - $img = $this->manager()->make('tests/images/trim.png'); - $value = strval($img); - $this->assertInternalType('string', $value); - } - - public function testFilter() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->filter(new \Intervention\Image\Filters\DemoFilter(10)); - $this->assertColorAtPosition('#818181', $img, 0, 0); - $this->assertColorAtPosition('#939393', $img, 18, 18); - $this->assertColorAtPosition('#939393', $img, 18, 18); - $this->assertColorAtPosition('#adadad', $img, 25, 25); - $this->assertColorAtPosition('#939393', $img, 35, 35); - } - - public function testCloneImageObject() - { - $img = $this->manager()->make('tests/images/trim.png'); - $cln = clone $img; - - // destroy original - $img->destroy(); - unset($img); - - // clone should be still intact - $this->assertInstanceOf('Intervention\Image\Image', $cln); - $this->assertInternalType('resource', $cln->getCore()); - } - - public function testGifConversionKeepsTransparency() - { - $save_as = 'tests/tmp/foo.gif'; - - // create gif image from transparent png - $img = $this->manager()->make('tests/images/star.png'); - $img->save($save_as); - - // new gif image should be transparent - $img = $this->manager()->make($save_as); - $this->assertTransparentPosition($img, 0, 0); - @unlink($save_as); - } - - private function assertColorAtPosition($color, $img, $x, $y) - { - $pick = $img->pickColor($x, $y, 'hex'); - $this->assertEquals($color, $pick); - $this->assertInstanceOf('Intervention\Image\Image', $img); - } - - private function assertTransparentPosition($img, $x, $y, $transparent = 0) - { - // background should be transparent - $color = $img->pickColor($x, $y, 'array'); - $this->assertEquals($transparent, $color[3]); // alpha channel - } - - private function manager() - { - return new \Intervention\Image\ImageManager(array( - 'driver' => 'gd' - )); - } -} diff --git a/tests/GdTestCase.php b/tests/GdTestCase.php new file mode 100644 index 000000000..b9dc5b9f6 --- /dev/null +++ b/tests/GdTestCase.php @@ -0,0 +1,53 @@ +specializeDecoder(new FilePathImageDecoder())->decode( + Resource::create($filename)->path() + ); + } + + public static function createTestImage(int $width, int $height): Image + { + $gd = imagecreatetruecolor($width, $height); + imagefill($gd, 0, 0, imagecolorallocate($gd, 255, 0, 0)); + + return new Image( + new Driver(), + new Core([ + new Frame($gd) + ]) + ); + } + + public static function createTestAnimation(): Image + { + $gd1 = imagecreatetruecolor(3, 2); + imagefill($gd1, 0, 0, imagecolorallocate($gd1, 255, 0, 0)); + $gd2 = imagecreatetruecolor(3, 2); + imagefill($gd2, 0, 0, imagecolorallocate($gd1, 0, 255, 0)); + $gd3 = imagecreatetruecolor(3, 2); + imagefill($gd3, 0, 0, imagecolorallocate($gd1, 0, 0, 255)); + + return new Image( + new Driver(), + new Core([ + new Frame($gd1), + new Frame($gd2), + new Frame($gd3), + ]) + ); + } +} diff --git a/tests/GetsizeCommandTest.php b/tests/GetsizeCommandTest.php deleted file mode 100644 index 76072b487..000000000 --- a/tests/GetsizeCommandTest.php +++ /dev/null @@ -1,38 +0,0 @@ -shouldReceive('getCore')->times(2)->andReturn($resource); - $command = new GetSizeGd(array()); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertInstanceOf('Intervention\Image\Size', $command->getOutput()); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('getimagewidth')->with(); - $imagick->shouldReceive('getimageheight')->with(); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new GetSizeImagick(array()); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertInstanceOf('Intervention\Image\Size', $command->getOutput()); - } -} diff --git a/tests/GreyscaleCommandTest.php b/tests/GreyscaleCommandTest.php deleted file mode 100644 index 1fa8d3a10..000000000 --- a/tests/GreyscaleCommandTest.php +++ /dev/null @@ -1,33 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $command = new GreyscaleGd(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('modulateimage')->with(100, 0, 100)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new GreyscaleImagick(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/HeightenCommandTest.php b/tests/HeightenCommandTest.php deleted file mode 100644 index 7290f4274..000000000 --- a/tests/HeightenCommandTest.php +++ /dev/null @@ -1,48 +0,0 @@ -aspectRatio(); }; - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $image = Mockery::mock('Intervention\Image\Image'); - $size = Mockery::mock('Intervention\Image\Size', array(800, 600)); - $size->shouldReceive('resize')->once()->andReturn($size); - $size->shouldReceive('getWidth')->once()->andReturn(800); - $size->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('getSize')->once()->andReturn($size); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('setCore')->once(); - $command = new HeightenGd(array(200)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $callback = function ($constraint) { $constraint->upsize(); }; - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('scaleimage')->with(300, 200)->once()->andReturn(true); - $size = Mockery::mock('Intervention\Image\Size', array(800, 600)); - $size->shouldReceive('resize')->once()->andReturn($size); - $size->shouldReceive('getWidth')->once()->andReturn(300); - $size->shouldReceive('getHeight')->once()->andReturn(200); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $image->shouldReceive('getSize')->once()->andReturn($size); - $command = new HeightenImagick(array(200)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/ImageManagerStaticTest.php b/tests/ImageManagerStaticTest.php deleted file mode 100644 index 66bc59596..000000000 --- a/tests/ImageManagerStaticTest.php +++ /dev/null @@ -1,35 +0,0 @@ -getManager(); - $this->assertInstanceOf('Intervention\Image\ImageManager', $m); - } - - public function testMake() - { - $manager = Mockery::mock('Intervention\Image\ImageManager'); - $manager->shouldReceive('make')->with('foo')->once(); - $managerStatic = new ImageManagerStatic($manager); - $managerStatic->make('foo'); - } - - public function testCanvas() - { - $manager = Mockery::mock('Intervention\Image\ImageManager'); - $manager->shouldReceive('canvas')->with(100, 100, null)->once(); - $managerStatic = new ImageManagerStatic($manager); - $managerStatic->canvas(100, 100); - } -} diff --git a/tests/ImageManagerTest.php b/tests/ImageManagerTest.php deleted file mode 100644 index e23d91278..000000000 --- a/tests/ImageManagerTest.php +++ /dev/null @@ -1,39 +0,0 @@ - 'foo', 'bar' => 'baz'); - $manager = new ImageManager($config); - $this->assertEquals('foo', $manager->config['driver']); - $this->assertEquals('baz', $manager->config['bar']); - } - - public function testConfigure() - { - $overwrite = array('driver' => 'none', 'bar' => 'none'); - $config = array('driver' => 'foo', 'bar' => 'baz'); - $manager = new ImageManager($overwrite); - $manager->configure($config); - $this->assertEquals('foo', $manager->config['driver']); - $this->assertEquals('baz', $manager->config['bar']); - } - - public function testConfigureObject() - { - $config = array('driver' => new Intervention\Image\Imagick\Driver()); - $manager = new ImageManager($config); - - $image = $manager->make('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'); - $this->assertInstanceOf('Intervention\Image\Image', $image); - $this->assertInstanceOf('Imagick', $image->getCore()); - } -} diff --git a/tests/ImageTest.php b/tests/ImageTest.php deleted file mode 100644 index 245d4df9a..000000000 --- a/tests/ImageTest.php +++ /dev/null @@ -1,122 +0,0 @@ -getTestImage(); - $this->assertEquals('mock', $image->getCore()); - } - - public function testCommandCall() - { - $image = $this->getTestImage(); - $result = $image->test(1, 2, 3); - $this->assertEquals('mock', $result); - } - - public function testEncode() - { - $image = $this->getTestImage(); - $image->getDriver()->shouldReceive('encode')->with($image, 'png', 90)->once(); - $image->encode('png', 90); - } - - public function testSave() - { - $save_as = __DIR__.'/tmp/test.jpg'; - $image = $this->getTestImage(); - $image->getDriver()->shouldReceive('encode')->with($image, 'jpg', 85)->once()->andReturn('mock'); - $image = $image->save($save_as, 85); - $this->assertInstanceOf('\Intervention\Image\Image', $image); - $this->assertFileExists($save_as); - $this->assertEquals($image->basename, 'test.jpg'); - $this->assertEquals($image->extension, 'jpg'); - $this->assertEquals($image->filename, 'test'); - @unlink($save_as); - } - - public function testIsEncoded() - { - $image = $this->getTestImage(); - $this->assertFalse($image->isEncoded()); - - $image->setEncoded('foo'); - $this->assertTrue($image->isEncoded()); - } - - public function testFilter() - { - $demoFilter = Mockery::mock('\Intervention\Image\Filters\DemoFilter', array(15)); - $image = $this->getTestImage(); - $demoFilter->shouldReceive('applyFilter')->with($image)->once()->andReturn($image); - $image->filter($demoFilter); - } - - public function testMime() - { - $image = $this->getTestImage(); - $this->assertEquals('image/png', $image->mime()); - } - - /** - * @expectedException \Intervention\Image\Exception\RuntimeException - */ - public function testGetBackupWithoutBackuo() - { - $image = $this->getTestImage(); - $image->getBackup(); - } - - public function testSetGetBackup() - { - $image = $this->getTestImage(); - $image->setBackup('foo'); - $backup = $image->getBackup(); - $this->assertEquals('foo', $backup); - } - - public function testGetBackups() - { - $image = $this->getTestImage(); - $backups = $image->getBackups(); - $this->assertEquals(array(), $backups); - - $image = $this->getTestImage(); - $image->setBackup('foo'); - $image->setBackup('bar'); - $image->setBackup('baz'); - $backups = $image->getBackups(); - $this->assertEquals(array('default' => 'baz'), $backups); - - $image = $this->getTestImage(); - $image->setBackup('foo', 'a'); - $image->setBackup('bar', 'b'); - $image->setBackup('baz', 'c'); - $backups = $image->getBackups(); - $this->assertEquals(array('a' => 'foo', 'b' => 'bar', 'c' => 'baz'), $backups); - } - - private function getTestImage() - { - $size = Mockery::mock('\Intervention\Image\Size', array(800, 600)); - $driver = Mockery::mock('\Intervention\Image\AbstractDriver'); - $command = Mockery::mock('\Intervention\Image\Commands\AbstractCommand'); - $command->shouldReceive('hasOutput')->andReturn(true); - $command->shouldReceive('getOutput')->andReturn('mock'); - $driver->shouldReceive('executeCommand')->andReturn($command); - $image = new Image($driver, 'mock'); - $image->mime = 'image/png'; - $image->dirname = './tmp'; - $image->basename = 'foo.png'; - - return $image; - } -} diff --git a/tests/ImagickColorTest.php b/tests/ImagickColorTest.php deleted file mode 100644 index 6a9df3e68..000000000 --- a/tests/ImagickColorTest.php +++ /dev/null @@ -1,337 +0,0 @@ -pixel = Mockery::mock('ImagickPixel'); - $c->pixel->shouldReceive('getcolorvalue')->with(Imagick::COLOR_RED)->andReturn(0.956862745098); - $this->assertEquals(244, $c->getRedValue()); - - $c = new Color; - $c->pixel = Mockery::mock('ImagickPixel'); - $c->pixel->shouldReceive('getcolorvalue')->with(Imagick::COLOR_GREEN)->andReturn(0.0470588235294); - $this->assertEquals(12, $c->getGreenValue()); - - $c = new Color; - $c->pixel = Mockery::mock('ImagickPixel'); - $c->pixel->shouldReceive('getcolorvalue')->with(Imagick::COLOR_BLUE)->andReturn(0.0862745098039); - $this->assertEquals(22, $c->getBlueValue()); - - $c = new Color; - $c->pixel = Mockery::mock('ImagickPixel'); - $c->pixel->shouldReceive('getcolorvalue')->with(Imagick::COLOR_ALPHA)->andReturn(1); - $this->assertEquals(1, $c->getAlphaValue()); - - $c = new Color; - $c->pixel = Mockery::mock('ImagickPixel'); - $c->pixel->shouldReceive('getcolorvalue')->with(Imagick::COLOR_ALPHA)->andReturn(1); - $this->assertEquals(1, $c->getAlphaValue()); - } - - public function testConstructor() - { - $c = new Color; - $this->validateColor($c, 255, 255, 255, 0); - } - - public function testParseNull() - { - $c = new Color; - $c->parse(null); - $this->validateColor($c, 255, 255, 255, 0); - } - - public function testParseInteger() - { - $c = new Color; - $c->parse(16777215); - $this->validateColor($c, 255, 255, 255, 0); - - $c = new Color; - $c->parse(4294967295); - $this->validateColor($c, 255, 255, 255, 1); - } - - public function testParseArray() - { - $c = new Color; - $c->parse(array(181, 55, 23, 0.5)); - $this->validateColor($c, 181, 55, 23, 0.5); - } - - public function testParseHexString() - { - $c = new Color; - $c->parse('#b53717'); - $this->validateColor($c, 181, 55, 23, 1); - } - - public function testParseRgbaString() - { - $c = new Color; - $c->parse('rgba(181, 55, 23, 1)'); - $this->validateColor($c, 181, 55, 23, 1); - } - - public function testInitFromInteger() - { - $c = new Color; - $c->initFromInteger(0); - $this->validateColor($c, 0, 0, 0, 0); - $c->initFromInteger(2147483647); - $this->validateColor($c, 255, 255, 255, 0.5); - $c->initFromInteger(16777215); - $this->validateColor($c, 255, 255, 255, 0); - $c->initFromInteger(2130706432); - $this->validateColor($c, 0, 0, 0, 0.5); - $c->initFromInteger(867514135); - $this->validateColor($c, 181, 55, 23, 0.2); - } - - public function testInitFromArray() - { - $c = new Color; - $c->initFromArray(array(0, 0, 0, 0)); - $this->validateColor($c, 0, 0, 0, 0); - $c->initFromArray(array(0, 0, 0, 1)); - $this->validateColor($c, 0, 0, 0, 1); - $c->initFromArray(array(255, 255, 255, 1)); - $this->validateColor($c, 255, 255, 255, 1); - $c->initFromArray(array(255, 255, 255, 0)); - $this->validateColor($c, 255, 255, 255, 0); - $c->initFromArray(array(255, 255, 255, 0.5)); - $this->validateColor($c, 255, 255, 255, 0.5); - $c->initFromArray(array(0, 0, 0)); - $this->validateColor($c, 0, 0, 0, 1); - $c->initFromArray(array(255, 255, 255)); - $this->validateColor($c, 255, 255, 255, 1); - $c->initFromArray(array(181, 55, 23)); - $this->validateColor($c, 181, 55, 23, 1); - $c->initFromArray(array(181, 55, 23, 0.5)); - $this->validateColor($c, 181, 55, 23, 0.5); - } - - public function testInitFromHexString() - { - $c = new Color; - $c->initFromString('#cccccc'); - $this->validateColor($c, 204, 204, 204, 1); - $c->initFromString('#b53717'); - $this->validateColor($c, 181, 55, 23, 1); - $c->initFromString('ffffff'); - $this->validateColor($c, 255, 255, 255, 1); - $c->initFromString('ff00ff'); - $this->validateColor($c, 255, 0, 255, 1); - $c->initFromString('#000'); - $this->validateColor($c, 0, 0, 0, 1); - $c->initFromString('000'); - $this->validateColor($c, 0, 0, 0, 1); - } - - public function testInitFromRgbString() - { - $c = new Color; - $c->initFromString('rgb(1, 14, 144)'); - $this->validateColor($c, 1, 14, 144, 1); - $c->initFromString('rgb (255, 255, 255)'); - $this->validateColor($c, 255, 255, 255, 1); - $c->initFromString('rgb(0,0,0)'); - $this->validateColor($c, 0, 0, 0, 1); - $c->initFromString('rgba(0,0,0,0)'); - $this->validateColor($c, 0, 0, 0, 0); - $c->initFromString('rgba(0,0,0,0.5)'); - $this->validateColor($c, 0, 0, 0, 0.5); - $c->initFromString('rgba(255, 0, 0, 0.5)'); - $this->validateColor($c, 255, 0, 0, 0.5); - $c->initFromString('rgba(204, 204, 204, 0.9)'); - $this->validateColor($c, 204, 204, 204, 0.9); - } - - public function testInitFromRgb() - { - $c = new Color; - $c->initFromRgb(0, 0, 0); - $this->validateColor($c, 0, 0, 0, 1); - $c->initFromRgb(255, 255, 255); - $this->validateColor($c, 255, 255, 255, 1); - $c->initFromRgb(181, 55, 23); - $this->validateColor($c, 181, 55, 23, 1); - } - - public function testInitFromRgba() - { - $c = new Color; - $c->initFromRgba(0, 0, 0, 1); - $this->validateColor($c, 0, 0, 0, 1); - $c->initFromRgba(255, 255, 255, 1); - $this->validateColor($c, 255, 255, 255, 1); - $c->initFromRgba(181, 55, 23, 1); - $this->validateColor($c, 181, 55, 23, 1); - $c->initFromRgba(181, 55, 23, 0); - $this->validateColor($c, 181, 55, 23, 0); - $c->initFromRgba(181, 55, 23, 0.5); - $this->validateColor($c, 181, 55, 23, 0.5); - } - - public function testGetInt() - { - $c = new Color; - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 16777215); - - $c = new Color(array(255, 255, 255)); - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 4294967295); - - $c = new Color(array(255, 255, 255, 1)); - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 4294967295); - - $c = new Color(array(181, 55, 23, 0.2)); - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 867514135); - - $c = new Color(array(255, 255, 255, 0.5)); - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 2164260863); - - $c = new Color(array(181, 55, 23, 1)); - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 4290066199); - - $c = new Color(array(0, 0, 0, 0)); - $i = $c->getInt(); - $this->assertInternalType('int', $i); - $this->assertEquals($i, 0); - } - - public function testGetHex() - { - $c = new Color; - $i = $c->getHex(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'ffffff'); - - $c = new Color(array(255, 255, 255, 1)); - $i = $c->getHex(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'ffffff'); - - $c = new Color(array(181, 55, 23, 0.5)); - $i = $c->getHex(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'b53717'); - - $c = new Color(array(0, 0, 0, 0)); - $i = $c->getHex('#'); - $this->assertInternalType('string', $i); - $this->assertEquals($i, '#000000'); - } - - public function testGetArray() - { - $c = new Color; - $i = $c->getArray(); - $this->assertInternalType('array', $i); - $this->assertEquals($i, array(255, 255, 255, 0)); - - $c = new Color(array(255, 255, 255, 1)); - $i = $c->getArray(); - $this->assertInternalType('array', $i); - $this->assertEquals($i, array(255, 255, 255, 1)); - - $c = new Color(array(181, 55, 23, 0.5)); - $i = $c->getArray(); - $this->assertInternalType('array', $i); - $this->assertEquals($i, array(181, 55, 23, 0.5)); - - $c = new Color(array(0, 0, 0, 1)); - $i = $c->getArray(); - $this->assertInternalType('array', $i); - $this->assertEquals($i, array(0, 0, 0, 1)); - } - - public function testGetRgba() - { - $c = new Color; - $i = $c->getRgba(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'rgba(255, 255, 255, 0.00)'); - - $c = new Color(array(255, 255, 255, 1)); - $i = $c->getRgba(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'rgba(255, 255, 255, 1.00)'); - - $c = new Color(array(181, 55, 23, 0.5)); - $i = $c->getRgba(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'rgba(181, 55, 23, 0.50)'); - - $c = new Color(array(0, 0, 0, 1)); - $i = $c->getRgba(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'rgba(0, 0, 0, 1.00)'); - - $c = new Color(array(255, 255, 255, 0.5)); - $i = $c->getRgba(); - $this->assertInternalType('string', $i); - $this->assertEquals($i, 'rgba(255, 255, 255, 0.50)'); - } - - public function testDiffers() - { - $c1 = new Color(array(0, 0, 0)); - $c2 = new Color(array(0, 0, 0)); - $this->assertEquals(false, $c1->differs($c2)); - - $c1 = new Color(array(1, 0, 0)); - $c2 = new Color(array(0, 0, 0)); - $this->assertEquals(true, $c1->differs($c2)); - - $c1 = new Color(array(1, 0, 0)); - $c2 = new Color(array(0, 0, 0)); - $this->assertEquals(false, $c1->differs($c2, 10)); - - $c1 = new Color(array(127, 127, 127)); - $c2 = new Color(array(0, 0, 0)); - $this->assertEquals(true, $c1->differs($c2, 49)); - - $c1 = new Color(array(127, 127, 127)); - $c2 = new Color(array(0, 0, 0)); - $this->assertEquals(false, $c1->differs($c2, 50)); - } - - /** - * @expectedException \Intervention\Image\Exception\NotReadableException - */ - public function testParseUnknown() - { - $c = new Color('xxxxxxxxxxxxxxxxxxxx'); - } - - private function validateColor($obj, $r, $g, $b, $a) - { - $this->assertInstanceOf('Intervention\Image\Imagick\Color', $obj); - $this->assertInstanceOf('ImagickPixel', $obj->pixel); - $this->assertEquals($r, round($obj->getRedValue(), 2)); - $this->assertEquals($g, round($obj->getGreenValue(), 2)); - $this->assertEquals($b, round($obj->getBlueValue(), 2)); - $this->assertEquals($a, round($obj->getAlphaValue(), 2)); - } -} diff --git a/tests/ImagickSystemTest.php b/tests/ImagickSystemTest.php deleted file mode 100644 index 7c4419ac3..000000000 --- a/tests/ImagickSystemTest.php +++ /dev/null @@ -1,1631 +0,0 @@ -manager()->make('tests/images/circle.png'); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(50, $img->getHeight()); - $this->assertEquals('image/png', $img->mime); - $this->assertEquals('tests/images', $img->dirname); - $this->assertEquals('circle.png', $img->basename); - $this->assertEquals('png', $img->extension); - $this->assertEquals('circle', $img->filename); - $this->assertEquals('image/png', $img->mime); - } - - public function testMakeFromString() - { - $str = file_get_contents('tests/images/circle.png'); - $img = $this->manager()->make($str); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(50, $img->getHeight()); - $this->assertEquals('image/png', $img->mime); - } - - public function testMakeFromImagick() - { - $imagick = new \Imagick; - $imagick->readImage('tests/images/circle.png'); - $img = $this->manager()->make($imagick); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(50, $img->getHeight()); - } - - public function testMakeFromDataUrl() - { - $str = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGElEQVQYlWM8c+bMfwYiABMxikYVUk8hAHWzA3cRvs4UAAAAAElFTkSuQmCC'; - $img = $this->manager()->make($str); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertEquals('image/png', $img->mime); - } - - public function testMakeFromBase64() - { - $str = 'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGElEQVQYlWM8c+bMfwYiABMxikYVUk8hAHWzA3cRvs4UAAAAAElFTkSuQmCC'; - $img = $this->manager()->make($str); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertEquals('image/png', $img->mime); - } - - public function testCanvas() - { - $img = $this->manager()->canvas(30, 20); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(30, $img->getWidth()); - $this->assertEquals(20, $img->getHeight()); - $this->assertTransparentPosition($img, 0, 0); - } - - public function testCanvasWithSolidBackground() - { - $img = $this->manager()->canvas(30, 20, 'b53717'); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(30, $img->getWidth()); - $this->assertEquals(20, $img->getHeight()); - $this->assertEquals('#b53717', $img->pickColor(15, 15, 'hex')); - } - - public function testGetSize() - { - $img = $this->manager()->make('tests/images/tile.png'); - $size = $img->getSize(); - $this->assertInstanceOf('Intervention\Image\Size', $size); - $this->assertInternalType('int', $size->width); - $this->assertInternalType('int', $size->height); - $this->assertEquals(16, $size->width); - $this->assertEquals(16, $size->height); - } - - public function testResizeImage() - { - $img = $this->manager()->make('tests/images/circle.png'); - $img->resize(120, 150); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(120, $img->getWidth()); - $this->assertEquals(150, $img->getHeight()); - $this->assertTransparentPosition($img, 0, 0); - } - - public function testResizeImageOnlyWidth() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(120, null); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(120, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertTransparentPosition($img, 0, 15); - } - - public function testResizeImageOnlyHeight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(null, 150); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(150, $img->getHeight()); - $this->assertTransparentPosition($img, 15, 0); - } - - public function testResizeImageAutoHeight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(50, null, function ($constraint) { $constraint->aspectRatio(); }); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(50, $img->getHeight()); - $this->assertTransparentPosition($img, 30, 0); - } - - public function testResizeImageAutoWidth() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(null, 50, function ($constraint) { $constraint->aspectRatio(); }); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(50, $img->getHeight()); - $this->assertTransparentPosition($img, 30, 0); - } - - public function testResizeDominantWidth() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(100, 120, function ($constraint) { $constraint->aspectRatio(); }); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(100, $img->getWidth()); - $this->assertEquals(100, $img->getHeight()); - $this->assertTransparentPosition($img, 60, 0); - } - - public function testResizeImagePreserveSimpleUpsizing() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resize(100, 100, function ($constraint) { $constraint->aspectRatio(); $constraint->upsize(); }); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertTransparentPosition($img, 15, 0); - } - - public function testWidenImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->widen(100); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(100, $img->getWidth()); - $this->assertEquals(100, $img->getHeight()); - $this->assertTransparentPosition($img, 60, 0); - } - - public function testWidenImageWithConstraint() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->widen(100, function ($constraint) {$constraint->upsize();}); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertTransparentPosition($img, 8, 0); - } - - public function testHeightenImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->heighten(100); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(100, $img->getWidth()); - $this->assertEquals(100, $img->getHeight()); - $this->assertTransparentPosition($img, 60, 0); - } - - public function testHeightenImageWithConstraint() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->heighten(100, function ($constraint) {$constraint->upsize();}); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInstanceOf('Imagick', $img->getCore()); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertTransparentPosition($img, 8, 0); - } - - public function testResizeCanvasCenter() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 4); - $this->assertColorAtPosition('#445160', $img, 5, 5); - $this->assertTransparentPosition($img, 0, 5); - $this->assertTransparentPosition($img, 5, 4); - } - - public function testResizeCanvasTopLeft() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'top-left'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertTransparentPosition($img, 0, 8); - $this->assertTransparentPosition($img, 8, 7); - } - - public function testResizeCanvasTop() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'top'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#445160', $img, 5, 8); - $this->assertTransparentPosition($img, 0, 8); - $this->assertTransparentPosition($img, 5, 7); - } - - public function testResizeCanvasTopRight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'top-right'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#445160', $img, 2, 8); - $this->assertTransparentPosition($img, 0, 8); - $this->assertTransparentPosition($img, 2, 7); - } - - public function testResizeCanvasLeft() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'left'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 4); - $this->assertColorAtPosition('#445160', $img, 8, 5); - $this->assertTransparentPosition($img, 0, 5); - $this->assertTransparentPosition($img, 8, 4); - } - - public function testResizeCanvasRight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'right'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 4); - $this->assertColorAtPosition('#445160', $img, 2, 5); - $this->assertTransparentPosition($img, 0, 5); - $this->assertTransparentPosition($img, 2, 4); - } - - public function testResizeCanvasBottomLeft() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'bottom-left'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 1); - $this->assertColorAtPosition('#445160', $img, 8, 2); - $this->assertTransparentPosition($img, 0, 2); - $this->assertTransparentPosition($img, 8, 1); - } - - public function testResizeCanvasBottomRight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'bottom-right'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 1); - $this->assertColorAtPosition('#445160', $img, 2, 2); - $this->assertTransparentPosition($img, 0, 2); - $this->assertTransparentPosition($img, 2, 1); - } - - public function testResizeCanvasBottom() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 10, 'bottom'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 1); - $this->assertColorAtPosition('#445160', $img, 5, 2); - $this->assertTransparentPosition($img, 0, 2); - $this->assertTransparentPosition($img, 5, 1); - } - - public function testResizeCanvasRelativeWithBackground() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(4, 4, 'center', true, '#ff00ff'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(20, $img->getWidth()); - $this->assertEquals(20, $img->getHeight()); - $this->assertColorAtPosition('#ff00ff', $img, 0, 0); - $this->assertColorAtPosition('#ff00ff', $img, 19, 19); - $this->assertColorAtPosition('#b4e000', $img, 2, 9); - $this->assertColorAtPosition('#445160', $img, 10, 10); - $this->assertTransparentPosition($img, 2, 10); - $this->assertTransparentPosition($img, 10, 9); - } - - public function testResizeCanvasJustWidth() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, null); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#445160', $img, 5, 8); - $this->assertTransparentPosition($img, 0, 8); - $this->assertTransparentPosition($img, 5, 7); - } - - public function testResizeCanvasJustHeight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(null, 10); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 4); - $this->assertColorAtPosition('#445160', $img, 8, 5); - $this->assertTransparentPosition($img, 0, 5); - $this->assertTransparentPosition($img, 8, 4); - } - - public function testResizeCanvasSmallerWidthLargerHeight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(10, 20); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(10, $img->getWidth()); - $this->assertEquals(20, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 9); - $this->assertColorAtPosition('#445160', $img, 5, 10); - $this->assertTransparentPosition($img, 0, 10); - $this->assertTransparentPosition($img, 5, 9); - } - - public function testResizeCanvasLargerWidthSmallerHeight() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(20, 10); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(20, $img->getWidth()); - $this->assertEquals(10, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 2, 4); - $this->assertColorAtPosition('#445160', $img, 10, 5); - $this->assertTransparentPosition($img, 0, 0); - $this->assertTransparentPosition($img, 2, 5); - $this->assertTransparentPosition($img, 10, 4); - } - - public function testResizeCanvasNegative() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(-4, -4); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(12, $img->getWidth()); - $this->assertEquals(12, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 5); - $this->assertColorAtPosition('#445160', $img, 6, 6); - $this->assertTransparentPosition($img, 0, 6); - $this->assertTransparentPosition($img, 6, 5); - } - - public function testResizeCanvasLargerHeightAutoWidth() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->resizeCanvas(null, 20, 'bottom-left', false, '#ff00ff'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(20, $img->getHeight()); - $this->assertColorAtPosition('#ff00ff', $img, 0, 0); - $this->assertColorAtPosition('#b4e000', $img, 0, 4); - $this->assertColorAtPosition('#b4e000', $img, 0, 11); - $this->assertColorAtPosition('#445160', $img, 8, 12); - $this->assertTransparentPosition($img, 0, 12); - $this->assertTransparentPosition($img, 8, 11); - } - - public function testResizeCanvasBorderNonRelative() - { - $img = $this->manager()->canvas(1, 1, 'ff0000'); - $img->resizeCanvas(17, 17, 'center', false, '333333'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(17, $img->getWidth()); - $this->assertEquals(17, $img->getHeight()); - $this->assertColorAtPosition('#333333', $img, 0, 0); - $this->assertColorAtPosition('#333333', $img, 5, 5); - $this->assertColorAtPosition('#333333', $img, 7, 7); - $this->assertColorAtPosition('#ff0000', $img, 8, 8); - } - - public function testCropImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->crop(6, 6); // should be centered without pos. - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(6, $img->getWidth()); - $this->assertEquals(6, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 2); - $this->assertColorAtPosition('#445160', $img, 3, 3); - $this->assertTransparentPosition($img, 0, 3); - $this->assertTransparentPosition($img, 3, 2); - } - - public function testCropImageWithPosition() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->crop(4, 4, 7, 7); // should be centered without pos. - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(4, $img->getWidth()); - $this->assertEquals(4, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 1, 1); - $this->assertTransparentPosition($img, 0, 1); - $this->assertTransparentPosition($img, 1, 0); - } - - public function testFitImageSquare() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->fit(6); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(6, $img->getWidth()); - $this->assertEquals(6, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 2); - $this->assertColorAtPosition('#445160', $img, 3, 3); - $this->assertTransparentPosition($img, 0, 3); - $this->assertTransparentPosition($img, 3, 2); - } - - public function testFitImageRectangle() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->fit(12, 6); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(12, $img->getWidth()); - $this->assertEquals(6, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 2); - $this->assertColorAtPosition('#445160', $img, 6, 3); - $this->assertTransparentPosition($img, 0, 3); - $this->assertTransparentPosition($img, 6, 2); - } - - public function testFitImageWithConstraintUpsize() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->fit(300, 150, function ($constraint) {$constraint->upsize();}); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(50, $img->getWidth()); - $this->assertEquals(25, $img->getHeight()); - $this->assertColorAtPosition('#00aef0', $img, 0, 0); - $this->assertColorAtPosition('#afa94c', $img, 17, 0); - $this->assertColorAtPosition('#ffa601', $img, 24, 0); - } - - public function testFlipImageHorizontal() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->flip('h'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 8, 7); - $this->assertColorAtPosition('#445160', $img, 0, 8); - $this->assertTransparentPosition($img, 0, 7); - $this->assertTransparentPosition($img, 8, 8); - } - - public function testFlipImageVertical() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->flip('v'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 7); - $this->assertTransparentPosition($img, 0, 7); - $this->assertTransparentPosition($img, 8, 8); - } - - public function testRotateImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->rotate(90); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 7); - $this->assertTransparentPosition($img, 0, 7); - $this->assertTransparentPosition($img, 8, 8); - } - - public function testInsertImage() - { - $watermark = $this->manager()->canvas(16, 16, '#0000ff'); // create watermark - - // top-left anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top-left', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(0, 0, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(16, 16, 'hex')); - - // top-left anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top-left', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(9, 9, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(10, 10, 'hex')); - - // top anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(0, 0, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(23, 15, 'hex')); - - // top anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(18, 10, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(31, 26, 'hex')); - - // top-right anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top-right', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(15, 0, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(31, 0, 'hex')); - - // top-right anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'top-right', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(6, 9, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(21, 25, 'hex')); - - // left anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'left', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(15, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(0, 7, 'hex')); - - // left anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'left', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(8, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(10, 7, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(25, 23, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(25, 8, 'hex')); - - // right anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'right', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(31, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(15, 15, 'hex')); - - // right anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'right', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(5, 8, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(22, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(21, 7, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(6, 8, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(21, 23, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(6, 23, 'hex')); - - // bottom-left anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom-left', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(15, 31, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(0, 15, 'hex')); - - // bottom-left anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom-left', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(10, 21, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(9, 20, 'hex')); - - // bottom anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(8, 16, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(8, 15, 'hex')); - - // bottom anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#ff0000', $img->pickColor(5, 8, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(23, 22, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(24, 21, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(7, 6, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(8, 6, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(23, 21, 'hex')); - $this->assertEquals('#0000ff', $img->pickColor(23, 6, 'hex')); - - // bottom-right anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom-right', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(16, 16, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(15, 16, 'hex')); - - // bottom-right anchor coordinates - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'bottom-right', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(21, 21, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(22, 22, 'hex')); - - // center anchor - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'center', 0, 0); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(23, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(8, 7, 'hex')); - - // center anchor coordinates / coordinates will be ignored for center - $img = $this->manager()->canvas(32, 32, '#ff0000'); // create canvas - $img->insert($watermark, 'center', 10, 10); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals($img->getWidth(), 32); - $this->assertEquals($img->getHeight(), 32); - $this->assertEquals('#0000ff', $img->pickColor(23, 23, 'hex')); - $this->assertEquals('#ff0000', $img->pickColor(8, 7, 'hex')); - } - - public function testInsertWithAlphaChannel() - { - $img = $this->manager()->canvas(50, 50, 'ff0000'); - $img->insert('tests/images/circle.png'); - $this->assertColorAtPosition('#ff0000', $img, 0, 0); - $this->assertColorAtPosition('#330000', $img, 30, 30); - } - - public function testInsertAfterResize() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->resize(16, 16)->insert('tests/images/tile.png'); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#00aef0', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#ffa601', $img, 8, 7); - } - - public function testInsertImagick() - { - $imagick = new \Imagick; - $imagick->readImage('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->insert($imagick); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#00aef0', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 24, 24); - } - - public function testInsertBinary() - { - $data = file_get_contents('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->insert($data); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#00aef0', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 24, 24); - } - - public function testInsertInterventionImage() - { - $obj = $this->manager()->make('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->insert($obj); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertColorAtPosition('#b4e000', $img, 0, 7); - $this->assertColorAtPosition('#00aef0', $img, 0, 8); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 24, 24); - } - - public function testOpacity() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->opacity(50); - $checkColor = $img->pickColor(7, 7, 'array'); - $this->assertEquals($checkColor[0], 180); - $this->assertEquals($checkColor[1], 224); - $this->assertEquals($checkColor[2], 0); - $this->assertEquals($checkColor[3], 0.5); - $checkColor = $img->pickColor(8, 8, 'array'); - $this->assertEquals($checkColor[0], 68); - $this->assertEquals($checkColor[1], 81); - $this->assertEquals($checkColor[2], 96); - $this->assertEquals($checkColor[3], 0.5); - $this->assertTransparentPosition($img, 0, 11); - } - - public function testMaskImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->mask('tests/images/gradient.png'); - $this->assertTransparentPosition($img, 0, 0); - $this->assertTransparentPosition($img, 18, 18); - $this->assertTransparentPosition($img, 23, 23); - $this->assertTransparentPosition($img, 30, 30); - $alpha = $img->pickColor(23, 24, 'array'); - $this->assertLessThan(1, $alpha[3]); - $this->assertGreaterThanOrEqual(0, $alpha[3]); - $alpha = $img->pickColor(35, 25, 'array'); - $this->assertLessThan(1, $alpha[3]); - $this->assertGreaterThanOrEqual(0, $alpha[3]); - $alpha = $img->pickColor(25, 42, 'array'); - $this->assertLessThan(1, $alpha[3]); - $this->assertGreaterThanOrEqual(0, $alpha[3]); - } - - public function testMaskImageWithAlpha() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->mask('tests/images/star.png', true); - $this->assertTransparentPosition($img, 0, 0); - $this->assertTransparentPosition($img, 16, 16); - $this->assertTransparentPosition($img, 36, 36); - $this->assertTransparentPosition($img, 47, 47); - $alpha = $img->pickColor(18, 18, 'array'); - $this->assertLessThan(1, $alpha[3]); - $this->assertGreaterThanOrEqual(0, $alpha[3]); - $alpha = $img->pickColor(22, 35, 'array'); - $this->assertLessThan(1, $alpha[3]); - $this->assertGreaterThanOrEqual(0, $alpha[3]); - } - - public function testPixelateImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->pixelate(20); - $this->assertInstanceOf('Intervention\Image\Image', $img); - } - - public function testGreyscaleImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->greyscale(); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertTransparentPosition($img, 8, 0); - $this->assertColorAtPosition('#707070', $img, 0, 0); - } - - public function testInvertImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->invert(); - $this->assertInstanceOf('Intervention\Image\Image', $img); - $this->assertTransparentPosition($img, 8, 0); - $this->assertColorAtPosition('#4b1fff', $img, 0, 0); - } - - public function testBlurImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->blur(1); - $this->assertInstanceOf('Intervention\Image\Image', $img); - } - - public function testFillImageWithColor() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->fill('b53717'); - $this->assertColorAtPosition('#b53717', $img, 0, 0); - $this->assertColorAtPosition('#b53717', $img, 15, 15); - } - - public function testFillImageWithColorAtPosition() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->fill('b53717', 0, 0); - $this->assertTransparentPosition($img, 0, 8); - $this->assertColorAtPosition('#b53717', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 15, 15); - } - - public function testFillImageWithImagick() - { - $imagick = new \Imagick; - $imagick->readImage('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->fill($imagick, 0, 0); - $this->assertColorAtPosition('#b4e000', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 20, 20); - } - - public function testFillImageWithBinary() - { - $data = file_get_contents('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->fill($data, 0, 0); - $this->assertColorAtPosition('#b4e000', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 20, 20); - } - - public function testFillImageWithInterventionImage() - { - $obj = $this->manager()->make('tests/images/tile.png'); - $img = $this->manager()->make('tests/images/trim.png'); - $img->fill($obj, 0, 0); - $this->assertColorAtPosition('#b4e000', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#00aef0', $img, 8, 7); - $this->assertColorAtPosition('#ffa601', $img, 20, 20); - } - - public function testPixelImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $coords = array(array(5, 5), array(12, 12)); - $img = $img->pixel('fdf5e4', $coords[0][0], $coords[0][1]); - $img = $img->pixel(array(255, 255, 255), $coords[1][0], $coords[1][1]); - $this->assertEquals('#fdf5e4', $img->pickColor($coords[0][0], $coords[0][1], 'hex')); - $this->assertEquals('#ffffff', $img->pickColor($coords[1][0], $coords[1][1], 'hex')); - } - - public function testRectangleImage() - { - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $img->rectangle(5, 5, 11, 11, function ($draw) { $draw->background('#ff0000'); $draw->border(1, '#0000ff'); }); - $this->assertEquals('32ceca9759d1973dd461b39664df604d', $img->checksum()); - } - - public function testLineImage() - { - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $img->line(0, 0, 15, 15, function ($draw) { $draw->color('#ff0000'); }); - $this->assertEquals('f5c585019bff361d91e2928b2ac2286b', $img->checksum()); - } - - public function testEllipseImage() - { - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $img->ellipse(12, 8, 8, 8, function ($draw) { $draw->background('#ff0000'); $draw->border(1, '#0000ff'); }); - $this->assertEquals('9dc5bbec6d45868610c082a1d67640b5', $img->checksum()); - } - - public function testCircleImage() - { - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $img->circle(12, 8, 8, function ($draw) { $draw->background('#ff0000'); $draw->border(1, '#0000ff'); }); - $this->assertEquals('a433c7c1a842ef83e1cb45875371358c', $img->checksum()); - } - - public function testPolygonImage() - { - $img = $this->manager()->canvas(16, 16, 'ffffff'); - $points = array(3, 3, 11, 11, 7, 13); - $img->polygon($points, function ($draw) { $draw->background('#ff0000'); $draw->border(1, '#0000ff'); }); - $this->assertEquals('e301afe179da858d441ad8fc0eb5490a', $img->checksum()); - } - - public function testResetImage() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->backup(); - $img->resize(30, 20); - $img->reset(); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - } - - public function testResetEmptyImage() - { - $img = $this->manager()->canvas(16, 16, '#0000ff'); - $img->backup(); - $img->resize(30, 20); - $img->fill('#ff0000'); - $img->reset(); - $this->assertInternalType('int', $img->getWidth()); - $this->assertInternalType('int', $img->getHeight()); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - $this->assertColorAtPosition('#0000ff', $img, 0, 0); - } - - public function testResetKeepTransparency() - { - $img = $this->manager()->make('tests/images/circle.png'); - $img->backup(); - $img->reset(); - $this->assertTransparentPosition($img, 0, 0); - } - - public function testResetToNamed() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->backup('original'); - $img->resize(30, 20); - $img->backup('30x20'); - - // reset to original - $img->reset('original'); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - - // reset to 30x20 - $img->reset('30x20'); - $this->assertEquals(30, $img->getWidth()); - $this->assertEquals(20, $img->getHeight()); - - // reset to original again - $img->reset('original'); - $this->assertEquals(16, $img->getWidth()); - $this->assertEquals(16, $img->getHeight()); - } - - public function testLimitColors() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->limitColors(4); - $this->assertLessThanOrEqual(5, $img->getCore()->getImageColors()); - } - - public function testLimitColorsKeepTransparency() - { - $img = $this->manager()->make('tests/images/star.png'); - $img->limitColors(16); - $this->assertLessThanOrEqual(17, $img->getCore()->getImageColors()); - $this->assertTransparentPosition($img, 0, 0); - $this->assertColorAtPosition('#680098', $img, 6, 12); - $this->assertColorAtPosition('#c2596a', $img, 22, 24); - } - - public function testLimitColorsKeepTransparencyWithMatte() - { - $img = $this->manager()->make('tests/images/star.png'); - $img->limitColors(32, '#00ff00'); - $this->assertLessThanOrEqual(33, $img->getCore()->getImageColors()); - $this->assertTransparentPosition($img, 0, 0); - $this->assertColorAtPosition('#00ff00', $img, 12, 10); - $this->assertColorAtPosition('#00ff00', $img, 22, 17); - $this->assertColorAtPosition('#e70012', $img, 16, 21); - } - - public function testLimitColorsNullWithMatte() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->limitColors(null, '#ff00ff'); - $this->assertColorAtPosition('#b4e000', $img, 0, 0); - $this->assertColorAtPosition('#445160', $img, 8, 8); - $this->assertColorAtPosition('#ff00ff', $img, 0, 8); - $this->assertColorAtPosition('#ff00ff', $img, 15, 0); - } - - public function testPickColorFromTrueColor() - { - $img = $this->manager()->make('tests/images/star.png'); - $c = $img->pickColor(0, 0); - $this->assertEquals(255, $c[0]); - $this->assertEquals(255, $c[1]); - $this->assertEquals(255, $c[2]); - $this->assertEquals(0, $c[3]); - - $c = $img->pickColor(11, 11); - $this->assertEquals(34, $c[0]); - $this->assertEquals(0, $c[1]); - $this->assertEquals(160, $c[2]); - $this->assertEquals(0.47, $c[3]); - - $c = $img->pickColor(16, 16); - $this->assertEquals(231, $c[0]); - $this->assertEquals(0, $c[1]); - $this->assertEquals(18, $c[2]); - $this->assertEquals(1, $c[3]); - } - - public function testPickColorFromIndexed() - { - $img = $this->manager()->make('tests/images/tile.png'); - $c = $img->pickColor(0, 0); - $this->assertEquals(180, $c[0]); - $this->assertEquals(224, $c[1]); - $this->assertEquals(0, $c[2]); - $this->assertEquals(1, $c[3]); - - $c = $img->pickColor(8, 8); - $this->assertEquals(68, $c[0]); - $this->assertEquals(81, $c[1]); - $this->assertEquals(96, $c[2]); - $this->assertEquals(1, $c[3]); - - $c = $img->pickColor(0, 15); - $this->assertEquals(0, $c[0]); - $this->assertEquals(0, $c[1]); - $this->assertEquals(0, $c[2]); - $this->assertEquals(0, $c[3]); - } - - public function testPickColorFromPalette() - { - $img = $this->manager()->make('tests/images/tile.png'); - $img->getCore()->quantizeImage(200, \Imagick::COLORSPACE_RGB, 0, false, false); - - $c = $img->pickColor(0, 0); - $this->assertEquals(180, $c[0]); - $this->assertEquals(224, $c[1]); - $this->assertEquals(0, $c[2]); - $this->assertEquals(1, $c[3]); - - $c = $img->pickColor(8, 8); - $this->assertEquals(68, $c[0]); - $this->assertEquals(81, $c[1]); - $this->assertEquals(96, $c[2]); - $this->assertEquals(1, $c[3]); - - $c = $img->pickColor(0, 15); - $this->assertEquals(0, $c[0]); - $this->assertEquals(0, $c[1]); - $this->assertEquals(0, $c[2]); - $this->assertEquals(0, $c[3]); - } - - public function testInterlaceImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->interlace(); - $img->encode('png'); - $this->assertTrue((ord($img->encoded[28]) != '0')); - $img->interlace(false); - $img->encode('png'); - $this->assertFalse((ord($img->encoded[28]) != '0')); - } - - public function testGammaImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->gamma(1.6); - $this->assertColorAtPosition('#00c9f6', $img, 0, 0); - $this->assertColorAtPosition('#ffc308', $img, 24, 24); - } - - public function testBrightnessImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->brightness(35); - $this->assertColorAtPosition('#45ccff', $img, 0, 0); - $this->assertColorAtPosition('#ffc55b', $img, 24, 24); - } - - public function testContrastImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->contrast(35); - $this->assertColorAtPosition('#00feff', $img, 0, 0); - $this->assertColorAtPosition('#fffd04', $img, 24, 24); - } - - public function testColorizeImage() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->colorize(40, 25, -50); - $this->assertColorAtPosition('#00ece2', $img, 0, 0); - $this->assertColorAtPosition('#ffea00', $img, 24, 24); - } - - public function testTrimGradient() - { - $canvas = $this->manager()->make('tests/images/gradient.png'); - - $img = clone $canvas; - $img->trim(); - $this->assertEquals($img->getWidth(), 46); - $this->assertEquals($img->getHeight(), 46); - - $img = clone $canvas; - $img->trim(null, null, 10); - $this->assertEquals($img->getWidth(), 38); - $this->assertEquals($img->getHeight(), 38); - - $img = clone $canvas; - $img->trim(null, null, 20); - $this->assertEquals($img->getWidth(), 34); - $this->assertEquals($img->getHeight(), 34); - - $img = clone $canvas; - $img->trim(null, null, 30); - $this->assertEquals($img->getWidth(), 30); - $this->assertEquals($img->getHeight(), 30); - - $img = clone $canvas; - $img->trim(null, null, 40); - $this->assertEquals($img->getWidth(), 26); - $this->assertEquals($img->getHeight(), 26); - - $img = clone $canvas; - $img->trim(null, null, 50); - $this->assertEquals($img->getWidth(), 22); - $this->assertEquals($img->getHeight(), 22); - - $img = clone $canvas; - $img->trim(null, null, 60); - $this->assertEquals($img->getWidth(), 20); - $this->assertEquals($img->getHeight(), 20); - - $img = clone $canvas; - $img->trim(null, null, 70); - $this->assertEquals($img->getWidth(), 16); - $this->assertEquals($img->getHeight(), 16); - - $img = clone $canvas; - $img->trim(null, null, 80); - $this->assertEquals($img->getWidth(), 12); - $this->assertEquals($img->getHeight(), 12); - - $img = clone $canvas; - $img->trim(null, null, 90); - $this->assertEquals($img->getWidth(), 8); - $this->assertEquals($img->getHeight(), 8); - } - - public function testTrimOnlyLeftAndRight() - { - $img = $this->manager()->make('tests/images/gradient.png'); - $img->trim(null, array('left', 'right'), 60); - $this->assertEquals($img->getWidth(), 20); - $this->assertEquals($img->getHeight(), 50); - } - - public function testTrimOnlyTopAndBottom() - { - $img = $this->manager()->make('tests/images/gradient.png'); - $img->trim(null, array('top', 'bottom'), 60); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 20); - } - - public function testTrimOnlyTop() - { - $img = $this->manager()->make('tests/images/gradient.png'); - $img->trim(null, 'top', 60); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 35); - } - - public function testTrimOnlyBottom() - { - $img = $this->manager()->make('tests/images/gradient.png'); - $img->trim(null, 'top', 60); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 35); - } - - public function testTrimWithFeather() - { - $img = $this->manager()->make('tests/images/trim.png'); - $feather = 5; - $img->trim(null, null, null, $feather); - $this->assertEquals($img->getWidth(), 28 + $feather * 2); - $this->assertEquals($img->getHeight(), 28 + $feather * 2); - $img->destroy(); - - $img = $this->manager()->make('tests/images/trim.png'); - $feather = 10; - $img->trim(null, null, null, $feather); - $this->assertEquals($img->getWidth(), 28 + $feather * 2); - $this->assertEquals($img->getHeight(), 28 + $feather * 2); - $img->destroy(); - - $img = $this->manager()->make('tests/images/trim.png'); - $feather = 20; // must respect original dimensions of image - $img->trim(null, null, null, $feather); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 50); - $img->destroy(); - - $img = $this->manager()->make('tests/images/trim.png'); - $feather = -5; - $img->trim(null, null, null, $feather); - $this->assertEquals($img->getWidth(), 28 + $feather * 2); - $this->assertEquals($img->getHeight(), 28 + $feather * 2); - $img->destroy(); - - $img = $this->manager()->make('tests/images/trim.png'); - $feather = -10; - $img->trim(null, null, null, $feather); - $this->assertEquals($img->getWidth(), 28 + $feather * 2); - $this->assertEquals($img->getHeight(), 28 + $feather * 2); - $img->destroy(); - - // trim only left and right with feather - $img = $this->manager()->make('tests/images/trim.png'); - $feather = 10; - $img->trim(null, array('left', 'right'), null, $feather); - $this->assertEquals($img->getWidth(), 28 + $feather * 2); - $this->assertEquals($img->getHeight(), 50); - $img->destroy(); - - // trim only top and bottom with feather - $img = $this->manager()->make('tests/images/trim.png'); - $feather = 10; - $img->trim(null, array('top', 'bottom'), null, $feather); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 28 + $feather * 2); - $img->destroy(); - - // trim with tolerance and feather - $img = $this->manager()->make('tests/images/gradient.png'); - $feather = 2; - $img->trim(null, null, 10, $feather); - $this->assertEquals($img->getWidth(), 38 + $feather * 2); - $this->assertEquals($img->getHeight(), 38 + $feather * 2); - $img->destroy(); - - $img = $this->manager()->make('tests/images/gradient.png'); - $feather = 5; - $img->trim(null, null, 10, $feather); - $this->assertEquals($img->getWidth(), 38 + $feather * 2); - $this->assertEquals($img->getHeight(), 38 + $feather * 2); - $img->destroy(); - - $img = $this->manager()->make('tests/images/gradient.png'); - $feather = 10; // should respect original dimensions - $img->trim(null, null, 20, $feather); - $this->assertEquals($img->getWidth(), 50); - $this->assertEquals($img->getHeight(), 50); - $img->destroy(); - } - - public function testEncodeDefault() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->encode(); - $mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $img->encoded); - $this->assertEquals('image/png', $mime); - } - - public function testEncodeJpeg() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->encode('jpg'); - $mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $img->encoded); - $this->assertEquals('image/jpeg', $mime); - } - - public function testEncodeGif() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->encode('gif'); - $mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $img->encoded); - $this->assertEquals('image/gif', $mime); - } - - public function testEncodeDataUrl() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->encode('data-url'); - $this->assertEquals('data:image/png;base64', substr($img->encoded, 0, 21)); - } - - public function testExifReadAll() - { - $img = $this->manager()->make('tests/images/exif.jpg'); - $data = $img->exif(); - $this->assertInternalType('array', $data); - $this->assertGreaterThanOrEqual(13, count($data)); - } - - public function testExifReadKey() - { - $img = $this->manager()->make('tests/images/exif.jpg'); - $data = $img->exif('Artist'); - $this->assertInternalType('string', $data); - $this->assertEquals('Oliver Vogel', $data); - } - - public function testExifReadNotExistingKey() - { - $img = $this->manager()->make('tests/images/exif.jpg'); - $data = $img->exif('xxx'); - $this->assertEquals(null, $data); - } - - public function testSaveImage() - { - $save_as = 'tests/tmp/foo.jpg'; - $img = $this->manager()->make('tests/images/trim.png'); - $img->save($save_as, 80); - $this->assertFileExists($save_as); - $this->assertEquals($img->dirname, 'tests/tmp'); - $this->assertEquals($img->basename, 'foo.jpg'); - $this->assertEquals($img->extension, 'jpg'); - $this->assertEquals($img->filename, 'foo'); - $this->assertEquals($img->mime, 'image/jpeg'); - @unlink($save_as); - - $save_as = 'tests/tmp/foo.png'; - $img = $this->manager()->make('tests/images/trim.png'); - $img->save($save_as); - $this->assertEquals($img->dirname, 'tests/tmp'); - $this->assertEquals($img->basename, 'foo.png'); - $this->assertEquals($img->extension, 'png'); - $this->assertEquals($img->filename, 'foo'); - $this->assertEquals($img->mime, 'image/png'); - $this->assertFileExists($save_as); - @unlink($save_as); - - $save_as = 'tests/tmp/foo.jpg'; - $img = $this->manager()->make('tests/images/trim.png'); - $img->save($save_as, 0); - $this->assertEquals($img->dirname, 'tests/tmp'); - $this->assertEquals($img->basename, 'foo.jpg'); - $this->assertEquals($img->extension, 'jpg'); - $this->assertEquals($img->filename, 'foo'); - $this->assertEquals($img->mime, 'image/jpeg'); - $this->assertFileExists($save_as); - @unlink($save_as); - } - - public function testSaveImageWithoutParameter() - { - $path = 'tests/tmp/bar.png'; - - // create temp. test image (red) - $img = $this->manager()->canvas(16, 16, '#ff0000'); - $img->save($path); - $img->destroy(); - - // open test image again - $img = $this->manager()->make($path); - $this->assertColorAtPosition('#ff0000', $img, 0, 0); - - // fill with green and save wthout paramater - $img->fill('#00ff00'); - $img->save(); - $img->destroy(); - - // re-open test image (should be green) - $img = $this->manager()->make($path); - $this->assertColorAtPosition('#00ff00', $img, 0, 0); - $img->destroy(); - - @unlink($path); - } - - /** - * @expectedException ImagickException - */ - public function testDestroy() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->destroy(); - $img->getCore()->getImageWidth(); // try to get width (should throw exception) - } - - /** - * @expectedException Exception - */ - public function testDestroyWithBackup() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->backup(); - $img->destroy(); - $img->getBackup()->getImageWidth(); // try to get width (should throw exception) - } - - public function testStringConversion() - { - $img = $this->manager()->make('tests/images/trim.png'); - $value = strval($img); - $this->assertInternalType('string', $value); - } - - public function testFilter() - { - $img = $this->manager()->make('tests/images/trim.png'); - $img->filter(new \Intervention\Image\Filters\DemoFilter(10)); - $this->assertInstanceOf('Intervention\Image\Image', $img); - } - - public function testCloneImageObject() - { - $img = $this->manager()->make('tests/images/trim.png'); - $cln = clone $img; - - // destroy original - $img->destroy(); - unset($img); - - // clone should be still intact - $this->assertInstanceOf('Intervention\Image\Image', $cln); - $this->assertInstanceOf('Imagick', $cln->getCore()); - } - - public function testGifConversionKeepsTransparency() - { - $save_as = 'tests/tmp/foo.gif'; - - // create gif image from transparent png - $img = $this->manager()->make('tests/images/star.png'); - $img->save($save_as); - - // new gif image should be transparent - $img = $this->manager()->make($save_as); - $this->assertTransparentPosition($img, 0, 0); - @unlink($save_as); - } - - private function assertColorAtPosition($color, $img, $x, $y) - { - $pick = $img->pickColor($x, $y, 'hex'); - $this->assertEquals($color, $pick); - $this->assertInstanceOf('Intervention\Image\Image', $img); - } - - private function assertTransparentPosition($img, $x, $y, $transparent = 0) - { - // background should be transparent - $color = $img->pickColor($x, $y, 'array'); - $this->assertEquals($transparent, $color[3]); // alpha channel - } - - private function manager() - { - return new \Intervention\Image\ImageManager(array( - 'driver' => 'imagick' - )); - } -} diff --git a/tests/ImagickTestCase.php b/tests/ImagickTestCase.php new file mode 100644 index 000000000..030422cb6 --- /dev/null +++ b/tests/ImagickTestCase.php @@ -0,0 +1,57 @@ +specializeDecoder(new FilePathImageDecoder())->decode( + Resource::create($filename)->path() + ); + } + + /** + * Create test image with red (#ff0000) background. + */ + public static function createTestImage(int $width, int $height): Image + { + $background = new ImagickPixel('rgb(255, 0, 0)'); + $imagick = new Imagick(); + $imagick->newImage($width, $height, $background, 'png'); + $imagick->setType(Imagick::IMGTYPE_UNDEFINED); + $imagick->setImageType(Imagick::IMGTYPE_UNDEFINED); + $imagick->setColorspace(Imagick::COLORSPACE_SRGB); + $imagick->setImageResolution(96, 96); + $imagick->setImageBackgroundColor($background); + + return new Image( + new Driver(), + new Core($imagick), + ); + } + + public static function createTestAnimation(): Image + { + $imagick = new Imagick(); + $imagick->setFormat('gif'); + + for ($i = 0; $i < 3; $i++) { + $frame = new Imagick(); + $frame->newImage(3, 2, new ImagickPixel('rgb(255, 0, 0)'), 'gif'); + $frame->setImageDelay(10); + $imagick->addImage($frame); + } + + return new Image(new Driver(), new Core($imagick)); + } +} diff --git a/tests/InsertCommandTest.php b/tests/InsertCommandTest.php deleted file mode 100644 index f2fa9001e..000000000 --- a/tests/InsertCommandTest.php +++ /dev/null @@ -1,66 +0,0 @@ -shouldReceive('align')->with('center', 10, 20)->once()->andReturn($image_size); - $watermark_size = Mockery::mock('\Intervention\Image\Size', array(800, 600)); - $watermark_size->shouldReceive('align')->with('center')->once()->andReturn($watermark_size); - $image_size->shouldReceive('relativePosition')->with($watermark_size)->once()->andReturn($position); - - $path = __DIR__.'/images/test.jpg'; - $resource = imagecreatefromjpeg($path); - $watermark = Mockery::mock('Intervention\Image\Image'); - $driver = Mockery::mock('Intervention\Image\Gd\Driver'); - $driver->shouldReceive('init')->with($path)->once()->andReturn($watermark); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->times(2)->andReturn($resource); - $image->shouldReceive('getSize')->once()->andReturn($image_size); - $watermark->shouldReceive('getSize')->once()->andReturn($watermark_size); - $watermark->shouldReceive('getCore')->once()->andReturn($resource); - - $command = new InsertGd(array($path, 'center', 10, 20)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $position = Mockery::mock('\Intervention\Image\Point', array(10, 20)); - - $image_size = Mockery::mock('\Intervention\Image\Size', array(800, 600)); - $image_size->shouldReceive('align')->with('center', 10, 20)->once()->andReturn($image_size); - $watermark_size = Mockery::mock('\Intervention\Image\Size', array(800, 600)); - $watermark_size->shouldReceive('align')->with('center')->once()->andReturn($watermark_size); - $image_size->shouldReceive('relativePosition')->with($watermark_size)->once()->andReturn($position); - - $path = __DIR__.'/images/test.jpg'; - $watermark = Mockery::mock('Intervention\Image\Image'); - $driver = Mockery::mock('Intervention\Image\Imagick\Driver'); - $driver->shouldReceive('init')->with($path)->once()->andReturn($watermark); - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('compositeimage')->with($imagick, \Imagick::COMPOSITE_DEFAULT, 10, 20)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getSize')->once()->andReturn($image_size); - $watermark->shouldReceive('getSize')->once()->andReturn($watermark_size); - $watermark->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new InsertImagick(array($path, 'center', 10, 20)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/InterlaceCommandTest.php b/tests/InterlaceCommandTest.php deleted file mode 100644 index 102f1faad..000000000 --- a/tests/InterlaceCommandTest.php +++ /dev/null @@ -1,33 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $command = new InterlaceGd(array(true)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('setinterlacescheme')->with(\Imagick::INTERLACE_LINE)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new InterlaceImagick(array(true)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/InvertCommandTest.php b/tests/InvertCommandTest.php deleted file mode 100644 index e7cabba25..000000000 --- a/tests/InvertCommandTest.php +++ /dev/null @@ -1,33 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $command = new InvertGd(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('negateimage')->with(false)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new InvertImagick(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/IptcCommandTest.php b/tests/IptcCommandTest.php deleted file mode 100644 index d1ca865ff..000000000 --- a/tests/IptcCommandTest.php +++ /dev/null @@ -1,71 +0,0 @@ -dirname = __DIR__.'/images'; - $image->basename = 'iptc.jpg'; - $command = new IptcCommand(array()); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertInternalType('array', $command->getOutput()); - } - - public function testFetchDefined() - { - $image = Mockery::mock('Intervention\Image\Image'); - $image->dirname = __DIR__.'/images'; - $image->basename = 'exif.jpg'; - $command = new IptcCommand(array('AuthorByline')); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertEquals('Oliver Vogel', $command->getOutput()); - } - - - public function testFetchNonExisting() - { - $image = Mockery::mock('Intervention\Image\Image'); - $image->dirname = __DIR__.'/images'; - $image->basename = 'exif.jpg'; - $command = new IptcCommand(array('xxx')); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertEquals(null, $command->getOutput()); - } - - - public function testFetchFromPng() - { - $image = Mockery::mock('Intervention\Image\Image'); - $image->dirname = __DIR__.'/images'; - $image->basename = 'star.png'; - $command = new IptcCommand(array('Orientation')); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertEquals(null, $command->getOutput()); - } - - public function testReturnNullOnIptcReadFail() - { - $image = Mockery::mock('Intervention\Image\Image'); - $command = new IptcCommand(array('Orientation')); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertEquals(null, $command->getOutput()); - } -} diff --git a/tests/LimitColorsCommandTest.php b/tests/LimitColorsCommandTest.php deleted file mode 100644 index 181343083..000000000 --- a/tests/LimitColorsCommandTest.php +++ /dev/null @@ -1,42 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('setCore')->once(); - $image->shouldReceive('getSize')->once()->andReturn($size); - $command = new LimitColorsGd(array(16)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $size = Mockery::mock('\Intervention\Image\Size', array(32, 32)); - $imagick = Mockery::mock('\Imagick'); - $imagick->shouldReceive('separateimagechannel')->with(\Imagick::CHANNEL_ALPHA)->times(2); - $imagick->shouldReceive('transparentpaintimage')->with('#ffffff', 0, 0, false)->once(); - $imagick->shouldReceive('negateimage')->with(false)->once(); - $imagick->shouldReceive('quantizeimage')->with(16, \Imagick::COLORSPACE_RGB, 0, false, false)->once(); - $imagick->shouldReceive('compositeimage')->once(); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getSize')->once()->andReturn($size); - $image->shouldReceive('getCore')->times(3)->andReturn($imagick); - $command = new LimitColorsImagick(array(16)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/LineCommandTest.php b/tests/LineCommandTest.php deleted file mode 100644 index 16495fd11..000000000 --- a/tests/LineCommandTest.php +++ /dev/null @@ -1,42 +0,0 @@ -shouldReceive('getDriverName')->once()->andReturn('Gd'); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $command = new LineCommand(array(10, 15, 100, 150)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertFalse($command->hasOutput()); - } - - public function testImagick() - { - $imagick = Mockery::mock('\Imagick'); - $imagick->shouldReceive('drawimage'); - $driver = Mockery::mock('\Intervention\Image\Imagick\Driver'); - $driver->shouldReceive('getDriverName')->once()->andReturn('Imagick'); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - - $command = new LineCommand(array(10, 15, 100, 150)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertFalse($command->hasOutput()); - } - -} diff --git a/tests/LineShapeTest.php b/tests/LineShapeTest.php deleted file mode 100644 index 506fd4ed0..000000000 --- a/tests/LineShapeTest.php +++ /dev/null @@ -1,49 +0,0 @@ -assertInstanceOf('Intervention\Image\Gd\Shapes\LineShape', $line); - $this->assertEquals(10, $line->x); - $this->assertEquals(15, $line->y); - - // imagick - $line = new LineImagick(10, 15); - $this->assertInstanceOf('Intervention\Image\Imagick\Shapes\LineShape', $line); - $this->assertEquals(10, $line->x); - $this->assertEquals(15, $line->y); - } - - public function testApplyToImage() - { - // gd - $core = imagecreatetruecolor(300, 200); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $line = new LineGd(10, 15); - $result = $line->applyToImage($image, 100, 200); - $this->assertInstanceOf('Intervention\Image\Gd\Shapes\LineShape', $line); - $this->assertTrue($result); - - // imagick - $core = Mockery::mock('\Imagick'); - $core->shouldReceive('drawimage')->once(); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $line = new LineImagick(10, 15); - $result = $line->applyToImage($image, 100, 200); - $this->assertInstanceOf('Intervention\Image\Imagick\Shapes\LineShape', $line); - $this->assertTrue($result); - } -} diff --git a/tests/MaskCommandTest.php b/tests/MaskCommandTest.php deleted file mode 100644 index fd9a734dc..000000000 --- a/tests/MaskCommandTest.php +++ /dev/null @@ -1,67 +0,0 @@ -shouldReceive('getSize')->once()->andReturn($mask_size); - $mask_image->shouldReceive('pickColor')->andReturn(array(0,0,0,0)); - - $canvas_image = Mockery::mock('Intervention\Image\Image'); - $canvas_core = imagecreatetruecolor(32, 32); - $canvas_image->shouldReceive('getCore')->times(2)->andReturn($canvas_core); - $canvas_image->shouldReceive('pixel'); - - $driver = Mockery::mock('Intervention\Image\Gd\Driver'); - $driver->shouldReceive('newImage')->with(32, 32, array(0,0,0,0))->once()->andReturn($canvas_image); - $driver->shouldReceive('init')->with($mask_path)->once()->andReturn($mask_image); - - $image_size = Mockery::mock('Intervention\Image\Size', array(32, 32)); - $image_core = imagecreatefrompng(__DIR__.'/images/trim.png'); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getSize')->once()->andReturn($image_size); - $image->shouldReceive('getDriver')->times(2)->andReturn($driver); - $image->shouldReceive('pickColor')->andReturn(array(0,0,0,0)); - $image->shouldReceive('setCore')->with($canvas_core)->once(); - - $command = new MaskGd(array($mask_path, true)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $mask_core = Mockery::mock('Imagick'); - $mask_path = __DIR__.'images/star.png'; - $mask_image = Mockery::mock('Intervention\Image\Image'); - $mask_image->shouldReceive('getCore')->once()->andReturn($mask_core); - $mask_size = Mockery::mock('Intervention\Image\Size', array(32, 32)); - $mask_image->shouldReceive('getSize')->once()->andReturn($mask_size); - - $driver = Mockery::mock('Intervention\Image\Imagick\Driver'); - $driver->shouldReceive('init')->with($mask_path)->once()->andReturn($mask_image); - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('setimagematte')->with(true)->once(); - $imagick->shouldReceive('compositeimage')->with($mask_core, \Imagick::COMPOSITE_DSTIN, 0, 0)->once(); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $image_size = Mockery::mock('Intervention\Image\Size', array(32, 32)); - $image->shouldReceive('getSize')->once()->andReturn($image_size); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - - $command = new MaskImagick(array($mask_path, true)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/OpacityCommandTest.php b/tests/OpacityCommandTest.php deleted file mode 100644 index 9fdf7a98d..000000000 --- a/tests/OpacityCommandTest.php +++ /dev/null @@ -1,43 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($mask_core); - - $resource = imagecreatefrompng(__DIR__.'/images/trim.png'); - $driver = Mockery::mock('\Intervention\Image\Gd\Driver'); - $driver->shouldReceive('newImage')->with(32, 32, 'rgba(0, 0, 0, 0.5)')->andReturn($mask); - - $size = Mockery::mock('\Intervention\Image\Size', array(32, 32)); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getSize')->once()->andReturn($size); - $image->shouldReceive('mask')->with($mask_core, true)->once(); - $command = new OpacityGd(array(50)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('evaluateimage')->with(\Imagick::EVALUATE_DIVIDE, 2, \Imagick::CHANNEL_ALPHA)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new OpacityImagick(array(50)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/OrientateCommandTest.php b/tests/OrientateCommandTest.php deleted file mode 100644 index 6b86c23f3..000000000 --- a/tests/OrientateCommandTest.php +++ /dev/null @@ -1,93 +0,0 @@ -shouldReceive('exif')->with('Orientation')->once()->andReturn(2); - $image->shouldReceive('flip')->once(); - $command = new OrientateCommand(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testExecuteOrientationThree() - { - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('exif')->with('Orientation')->once()->andReturn(3); - $image->shouldReceive('rotate')->with(180)->once(); - $command = new OrientateCommand(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testExecuteOrientationFour() - { - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('exif')->with('Orientation')->once()->andReturn(4); - $image->shouldReceive('rotate')->with(180)->once()->andReturn($image); - $image->shouldReceive('flip')->once(); - $command = new OrientateCommand(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testExecuteOrientationFive() - { - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('exif')->with('Orientation')->once()->andReturn(5); - $image->shouldReceive('rotate')->with(270)->once()->andReturn($image); - $image->shouldReceive('flip')->once(); - $command = new OrientateCommand(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testExecuteOrientationSix() - { - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('exif')->with('Orientation')->once()->andReturn(6); - $image->shouldReceive('rotate')->with(270)->once(); - $command = new OrientateCommand(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testExecuteOrientationSeven() - { - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('exif')->with('Orientation')->once()->andReturn(7); - $image->shouldReceive('rotate')->with(90)->once()->andReturn($image); - $image->shouldReceive('flip')->once(); - $command = new OrientateCommand(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testExecuteOrientationEight() - { - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('exif')->with('Orientation')->once()->andReturn(8); - $image->shouldReceive('rotate')->with(90)->once(); - $command = new OrientateCommand(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testExecuteOrientationNoExifData() - { - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('exif')->with('Orientation')->once()->andReturn(null); - $command = new OrientateCommand(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/PickColorCommandTest.php b/tests/PickColorCommandTest.php deleted file mode 100644 index 731de04e5..000000000 --- a/tests/PickColorCommandTest.php +++ /dev/null @@ -1,66 +0,0 @@ -shouldReceive('getCore')->times(2)->andReturn($resource); - $command = new PickColorGd(array(1, 2)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertInternalType('array', $command->getOutput()); - $this->assertEquals(4, count($command->getOutput())); - } - - public function testGdWithFormat() - { - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->times(2)->andReturn($resource); - $command = new PickColorGd(array(1, 2, 'hex')); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertInternalType('string', $command->getOutput()); - $this->assertEquals('#ffffff', $command->getOutput()); - } - - public function testImagickWithCoordinates() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('getimagepixelcolor')->with(1, 2)->andReturn(new ImagickPixel); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new PickColorImagick(array(1, 2)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertInternalType('array', $command->getOutput()); - $this->assertEquals(4, count($command->getOutput())); - } - - public function testImagickWithFormat() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('getimagepixelcolor')->with(1, 2)->andReturn(new ImagickPixel('#ff0000')); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new PickColorImagick(array(1, 2, 'hex')); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - $this->assertInternalType('string', $command->getOutput()); - $this->assertEquals('#ff0000', $command->getOutput()); - } -} diff --git a/tests/PixelCommandTest.php b/tests/PixelCommandTest.php deleted file mode 100644 index e1e2dcfb6..000000000 --- a/tests/PixelCommandTest.php +++ /dev/null @@ -1,33 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $command = new PixelGd(array('#b53717', 10, 20)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('drawimage')->once()->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new PixelImagick(array('#b53717', 10, 20)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/PixelateCommandTest.php b/tests/PixelateCommandTest.php deleted file mode 100644 index 76314ba4f..000000000 --- a/tests/PixelateCommandTest.php +++ /dev/null @@ -1,36 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $command = new PixelateGd(array(10)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('scaleimage')->with(80, 60)->once()->andReturn(true); - $imagick->shouldReceive('scaleimage')->with(800, 600)->once()->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->times(2)->andReturn($imagick); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $command = new PixelateImagick(array(10)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/PointTest.php b/tests/PointTest.php deleted file mode 100644 index aaac80d8b..000000000 --- a/tests/PointTest.php +++ /dev/null @@ -1,46 +0,0 @@ -assertInstanceOf('Intervention\Image\Point', $point); - $this->assertEquals(0, $point->x); - $this->assertEquals(0, $point->y); - } - - public function testConstructorWithParameters() - { - $point = new Point(40, 50); - $this->assertInstanceOf('Intervention\Image\Point', $point); - $this->assertEquals(40, $point->x); - $this->assertEquals(50, $point->y); - } - - public function testSetX() - { - $point = new Point(0, 0); - $point->setX(100); - $this->assertEquals(100, $point->x); - $this->assertEquals(0, $point->y); - } - - public function testSetY() - { - $point = new Point(0, 0); - $point->setY(100); - $this->assertEquals(0, $point->x); - $this->assertEquals(100, $point->y); - } - - public function testSetPosition() - { - $point = new Point(0, 0); - $point->setPosition(100, 200); - $this->assertEquals(100, $point->x); - $this->assertEquals(200, $point->y); - } -} diff --git a/tests/PolygonCommandTest.php b/tests/PolygonCommandTest.php deleted file mode 100644 index c3eaf7165..000000000 --- a/tests/PolygonCommandTest.php +++ /dev/null @@ -1,44 +0,0 @@ -shouldReceive('getDriverName')->once()->andReturn('Gd'); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $command = new PolygonCommand(array($points)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertFalse($command->hasOutput()); - } - - public function testImagick() - { - $points = array(1, 2, 3, 4, 5, 6); - $imagick = Mockery::mock('\Imagick'); - $imagick->shouldReceive('drawimage'); - $driver = Mockery::mock('\Intervention\Image\Imagick\Driver'); - $driver->shouldReceive('getDriverName')->once()->andReturn('Imagick'); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - - $command = new PolygonCommand(array($points)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertFalse($command->hasOutput()); - } - -} diff --git a/tests/PolygonShapeTest.php b/tests/PolygonShapeTest.php deleted file mode 100644 index 4f87b2b30..000000000 --- a/tests/PolygonShapeTest.php +++ /dev/null @@ -1,56 +0,0 @@ -assertInstanceOf('Intervention\Image\Gd\Shapes\PolygonShape', $polygon); - $this->assertEquals(array(1, 2, 3, 4, 5, 6), $polygon->points); - - } - - public function testGdApplyToImage() - { - $core = imagecreatetruecolor(300, 200); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $polygon = new PolygonGd(array(1, 2, 3, 4, 5, 6)); - $result = $polygon->applyToImage($image); - $this->assertInstanceOf('Intervention\Image\Gd\Shapes\PolygonShape', $polygon); - $this->assertTrue($result); - } - - public function testImagickConstructor() - { - $polygon = new PolygonImagick(array(1, 2, 3, 4, 5, 6)); - $this->assertInstanceOf('Intervention\Image\Imagick\Shapes\PolygonShape', $polygon); - $this->assertEquals(array( - array('x' => 1, 'y' => 2), - array('x' => 3, 'y' => 4), - array('x' => 5, 'y' => 6)), - $polygon->points); - - } - - public function testImagickApplyToImage() - { - $core = Mockery::mock('\Imagick'); - $core->shouldReceive('drawimage')->once(); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $polygon = new PolygonImagick(array(1, 2, 3, 4, 5, 6)); - $result = $polygon->applyToImage($image); - $this->assertInstanceOf('Intervention\Image\Imagick\Shapes\PolygonShape', $polygon); - $this->assertTrue($result); - } - -} diff --git a/tests/Providers/ColorDataProvider.php b/tests/Providers/ColorDataProvider.php new file mode 100644 index 000000000..217ece5f0 --- /dev/null +++ b/tests/Providers/ColorDataProvider.php @@ -0,0 +1,298 @@ +alert(\'hi\');', + '', + ]; + } + + public static function invalidDataUris(): Generator + { + yield [ + 'foo', + InvalidArgumentException::class, + ]; + yield [ + 'bar', + InvalidArgumentException::class, + ]; + yield [ + 'data:', + InvalidArgumentException::class, + ]; + yield [ + 'VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4=', + InvalidArgumentException::class, + ]; + yield [ + 'data;xt;4,SGVsbG8sIFdvcmxkIQ==', + InvalidArgumentException::class, + ]; + } +} diff --git a/tests/Providers/DriverProvider.php b/tests/Providers/DriverProvider.php new file mode 100644 index 000000000..a0732c96f --- /dev/null +++ b/tests/Providers/DriverProvider.php @@ -0,0 +1,24 @@ +supports($yield['format'])) { + yield ['driver' => new Driver(), ...$yield]; + } + } + } + + public static function sizeData(): Generator + { + foreach (static::baseData() as $yield) { + yield [$yield['driver'], $yield['resource'], $yield['size']]; + } + } + + public static function colorspaceData(): Generator + { + foreach (static::baseData() as $yield) { + if ($yield['colorspace'] === RgbSpace::class) { + yield [$yield['driver'], $yield['resource'], $yield['colorspace']]; + } + } + } + + public static function resolutionData(): Generator + { + foreach (static::baseData() as $yield) { + if (in_array($yield['format'], [Format::JPEG, Format::PNG])) { + yield [$yield['driver'], $yield['resource'], $yield['resolution']]; + } + } + } +} diff --git a/tests/Providers/ImageSourceProvider.php b/tests/Providers/ImageSourceProvider.php new file mode 100644 index 000000000..5094148a4 --- /dev/null +++ b/tests/Providers/ImageSourceProvider.php @@ -0,0 +1,77 @@ +supports($yield['format'])) { + yield ['driver' => new Driver(), ...$yield]; + } + } + } + + public static function sizeData(): Generator + { + foreach (static::baseData() as $yield) { + yield [$yield['driver'], $yield['resource'], $yield['size']]; + } + } + + public static function colorspaceData(): Generator + { + foreach (static::baseData() as $yield) { + yield [$yield['driver'], $yield['resource'], $yield['colorspace']]; + } + } + + public static function resolutionData(): Generator + { + foreach (static::baseData() as $yield) { + if (in_array($yield['format'], [Format::JPEG, Format::PNG])) { + yield [$yield['driver'], $yield['resource'], $yield['resolution']]; + } + } + } +} diff --git a/tests/Providers/InputDataProvider.php b/tests/Providers/InputDataProvider.php new file mode 100644 index 000000000..f342444e0 --- /dev/null +++ b/tests/Providers/InputDataProvider.php @@ -0,0 +1,157 @@ +path(), + null, + ImageInterface::class, + ]; + + yield [ + Resource::create('test.jpg')->data(), + null, + ImageInterface::class, + ]; + } + + public static function decodeColorDataProvider(): Generator + { + yield [ + 'ffffff', + null, + ColorInterface::class, + ]; + + yield [ + 'ffffff', + [new HexColorDecoder()], + ColorInterface::class, + ]; + + yield [ + 'ffffff', + [HexColorDecoder::class], + ColorInterface::class, + ]; + } + + public static function alignmentInputs(): Generator + { + yield [Alignment::TOP, Alignment::TOP]; + yield ['top', Alignment::TOP]; + yield ['top-center', Alignment::TOP]; + yield ['top_center', Alignment::TOP]; + yield ['topcenter', Alignment::TOP]; + yield ['center-top', Alignment::TOP]; + yield ['center_top', Alignment::TOP]; + yield ['centertop', Alignment::TOP]; + yield ['top-middle', Alignment::TOP]; + yield ['top_middle', Alignment::TOP]; + yield ['topmiddle', Alignment::TOP]; + yield ['middle-top', Alignment::TOP]; + yield ['middle_top', Alignment::TOP]; + yield ['middletop', Alignment::TOP]; + yield ['top-right', Alignment::TOP_RIGHT]; + yield ['top_right', Alignment::TOP_RIGHT]; + yield ['topright', Alignment::TOP_RIGHT]; + yield ['right-top', Alignment::TOP_RIGHT]; + yield ['right_top', Alignment::TOP_RIGHT]; + yield ['righttop', Alignment::TOP_RIGHT]; + + yield [Alignment::RIGHT, Alignment::RIGHT]; + yield ['right', Alignment::RIGHT]; + yield ['right-center', Alignment::RIGHT]; + yield ['right_center', Alignment::RIGHT]; + yield ['rightcenter', Alignment::RIGHT]; + yield ['center-right', Alignment::RIGHT]; + yield ['center_right', Alignment::RIGHT]; + yield ['centerright', Alignment::RIGHT]; + yield ['right-middle', Alignment::RIGHT]; + yield ['right_middle', Alignment::RIGHT]; + yield ['rightmiddle', Alignment::RIGHT]; + yield ['middle-right', Alignment::RIGHT]; + yield ['middle_right', Alignment::RIGHT]; + yield ['middleright', Alignment::RIGHT]; + + yield [Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_RIGHT]; + yield ['right-bottom', Alignment::BOTTOM_RIGHT]; + yield ['right_bottom', Alignment::BOTTOM_RIGHT]; + yield ['rightbottom', Alignment::BOTTOM_RIGHT]; + yield ['bottom-right', Alignment::BOTTOM_RIGHT]; + yield ['bottom_right', Alignment::BOTTOM_RIGHT]; + yield ['bottomright', Alignment::BOTTOM_RIGHT]; + + yield [Alignment::BOTTOM, Alignment::BOTTOM]; + yield ['bottom', Alignment::BOTTOM]; + yield ['bottom-center', Alignment::BOTTOM]; + yield ['bottom_center', Alignment::BOTTOM]; + yield ['bottomcenter', Alignment::BOTTOM]; + yield ['center-bottom', Alignment::BOTTOM]; + yield ['center_bottom', Alignment::BOTTOM]; + yield ['centerbottom', Alignment::BOTTOM]; + yield ['bottom-middle', Alignment::BOTTOM]; + yield ['bottom_middle', Alignment::BOTTOM]; + yield ['bottommiddle', Alignment::BOTTOM]; + yield ['middle-bottom', Alignment::BOTTOM]; + yield ['middle_bottom', Alignment::BOTTOM]; + yield ['middlebottom', Alignment::BOTTOM]; + + yield [Alignment::BOTTOM_LEFT, Alignment::BOTTOM_LEFT]; + yield ['left-bottom', Alignment::BOTTOM_LEFT]; + yield ['left_bottom', Alignment::BOTTOM_LEFT]; + yield ['leftbottom', Alignment::BOTTOM_LEFT]; + yield ['bottom-left', Alignment::BOTTOM_LEFT]; + yield ['bottom_left', Alignment::BOTTOM_LEFT]; + yield ['bottomleft', Alignment::BOTTOM_LEFT]; + + yield [Alignment::LEFT, Alignment::LEFT]; + yield ['left', Alignment::LEFT]; + yield ['left-center', Alignment::LEFT]; + yield ['left_center', Alignment::LEFT]; + yield ['leftcenter', Alignment::LEFT]; + yield ['center-left', Alignment::LEFT]; + yield ['center_left', Alignment::LEFT]; + yield ['centerleft', Alignment::LEFT]; + yield ['left-middle', Alignment::LEFT]; + yield ['left_middle', Alignment::LEFT]; + yield ['leftmiddle', Alignment::LEFT]; + yield ['middle-left', Alignment::LEFT]; + yield ['middle_left', Alignment::LEFT]; + yield ['middleleft', Alignment::LEFT]; + + yield [Alignment::TOP_LEFT, Alignment::TOP_LEFT]; + yield ['left-top', Alignment::TOP_LEFT]; + yield ['left_top', Alignment::TOP_LEFT]; + yield ['lefttop', Alignment::TOP_LEFT]; + yield ['top-left', Alignment::TOP_LEFT]; + yield ['top_left', Alignment::TOP_LEFT]; + yield ['topleft', Alignment::TOP_LEFT]; + + yield [Alignment::CENTER, Alignment::CENTER]; + yield ['center', Alignment::CENTER]; + yield ['middle', Alignment::CENTER]; + yield ['center-center', Alignment::CENTER]; + yield ['center_center', Alignment::CENTER]; + yield ['centercenter', Alignment::CENTER]; + yield ['center-middle', Alignment::CENTER]; + yield ['center_middle', Alignment::CENTER]; + yield ['centermiddle', Alignment::CENTER]; + yield ['middle-center', Alignment::CENTER]; + yield ['middle_center', Alignment::CENTER]; + yield ['middlecenter', Alignment::CENTER]; + } +} diff --git a/tests/Providers/ResizeDataProvider.php b/tests/Providers/ResizeDataProvider.php new file mode 100644 index 000000000..0c4d0af06 --- /dev/null +++ b/tests/Providers/ResizeDataProvider.php @@ -0,0 +1,150 @@ + 150], new Size(150, 200)]; + yield [new Size(300, 200), ['height' => 150], new Size(300, 150)]; + yield [new Size(300, 200), ['width' => 20, 'height' => 10], new Size(20, 10)]; + yield [new Size(300, 200), [], new Size(300, 200)]; + } + + public static function resizeDownDataProvider(): Generator + { + yield [new Size(800, 600), ['width' => 1000, 'height' => 2000], new Size(800, 600)]; + yield [new Size(800, 600), ['width' => 400, 'height' => 1000], new Size(400, 600)]; + yield [new Size(800, 600), ['width' => 1000, 'height' => 400], new Size(800, 400)]; + yield [new Size(800, 600), ['width' => 400, 'height' => 300], new Size(400, 300)]; + yield [new Size(800, 600), ['width' => 1000], new Size(800, 600)]; + yield [new Size(800, 600), ['height' => 1000], new Size(800, 600)]; + yield [new Size(800, 600), [], new Size(800, 600)]; + } + + public static function scaleDataProvider(): Generator + { + yield [new Size(800, 600), ['width' => 1000, 'height' => 2000], new Size(1000, 750)]; + yield [new Size(800, 600), ['width' => 2000, 'height' => 1000], new Size(1333, 1000)]; + yield [new Size(800, 600), ['height' => 3000], new Size(4000, 3000)]; + yield [new Size(800, 600), ['width' => 8000], new Size(8000, 6000)]; + yield [new Size(800, 600), ['width' => 100, 'height' => 400], new Size(100, 75)]; + yield [new Size(800, 600), ['width' => 400, 'height' => 100], new Size(133, 100)]; + yield [new Size(800, 600), ['height' => 300], new Size(400, 300)]; + yield [new Size(800, 600), ['width' => 80], new Size(80, 60)]; + yield [new Size(640, 480), ['width' => 225], new Size(225, 169)]; + yield [new Size(640, 480), ['width' => 223], new Size(223, 167)]; + yield [new Size(600, 800), ['width' => 300, 'height' => 300], new Size(225, 300)]; + yield [new Size(800, 600), ['width' => 400, 'height' => 10], new Size(13, 10)]; + yield [new Size(800, 600), ['width' => 1000, 'height' => 1200], new Size(1000, 750)]; + yield [new Size(12000, 12), ['width' => 4000, 'height' => 3000], new Size(4000, 4)]; + yield [new Size(12, 12000), ['width' => 4000, 'height' => 3000], new Size(3, 3000)]; + yield [new Size(12000, 6000), ['width' => 4000, 'height' => 3000], new Size(4000, 2000)]; + yield [new Size(3, 3000), ['height' => 300], new Size(1, 300)]; + yield [new Size(800, 600), [], new Size(800, 600)]; + } + + public static function scaleDownDataProvider(): Generator + { + yield [new Size(800, 600), ['width' => 1000, 'height' => 2000], new Size(800, 600)]; + yield [new Size(800, 600), ['width' => 1000, 'height' => 600], new Size(800, 600)]; + yield [new Size(800, 600), ['width' => 1000, 'height' => 300], new Size(400, 300)]; + yield [new Size(800, 600), ['width' => 400, 'height' => 1000], new Size(400, 300)]; + yield [new Size(800, 600), ['width' => 400], new Size(400, 300)]; + yield [new Size(800, 600), ['height' => 300], new Size(400, 300)]; + yield [new Size(800, 600), ['width' => 1000], new Size(800, 600)]; + yield [new Size(800, 600), ['height' => 1000], new Size(800, 600)]; + yield [new Size(800, 600), ['width' => 100], new Size(100, 75)]; + yield [new Size(800, 600), ['width' => 300, 'height' => 200], new Size(267, 200)]; + yield [new Size(600, 800), ['width' => 300, 'height' => 300], new Size(225, 300)]; + yield [new Size(800, 600), ['width' => 400, 'height' => 10], new Size(13, 10)]; + yield [new Size(3, 3000), ['height' => 300], new Size(1, 300)]; + yield [new Size(800, 600), [], new Size(800, 600)]; + } + + public static function coverDataProvider(): Generator + { + yield [new Size(800, 600), new Size(100, 100), new Size(133, 100)]; + yield [new Size(800, 600), new Size(200, 100), new Size(200, 150)]; + yield [new Size(800, 600), new Size(100, 200), new Size(267, 200)]; + yield [new Size(800, 600), new Size(2000, 10), new Size(2000, 1500)]; + yield [new Size(800, 600), new Size(10, 2000), new Size(2667, 2000)]; + yield [new Size(800, 600), new Size(800, 600), new Size(800, 600)]; + yield [new Size(400, 300), new Size(120, 120), new Size(160, 120)]; + yield [new Size(600, 800), new Size(100, 100), new Size(100, 133)]; + yield [new Size(100, 100), new Size(800, 600), new Size(800, 800)]; + } + + public static function containDataProvider(): Generator + { + yield [new Size(800, 600), new Size(100, 100), new Size(100, 75)]; + yield [new Size(800, 600), new Size(200, 100), new Size(133, 100)]; + yield [new Size(800, 600), new Size(100, 200), new Size(100, 75)]; + yield [new Size(800, 600), new Size(2000, 10), new Size(13, 10)]; + yield [new Size(800, 600), new Size(10, 2000), new Size(10, 8)]; + yield [new Size(800, 600), new Size(800, 600), new Size(800, 600)]; + yield [new Size(400, 300), new Size(120, 120), new Size(120, 90)]; + yield [new Size(600, 800), new Size(100, 100), new Size(75, 100)]; + yield [new Size(100, 100), new Size(800, 600), new Size(600, 600)]; + } + + public static function cropDataProvider(): Generator + { + yield [ + new Size(800, 600), + new Size(100, 100), + Alignment::CENTER, + new Size(100, 100, new Point(350, 250)) + ]; + yield [ + new Size(800, 600), + new Size(200, 100), + Alignment::CENTER, + new Size(200, 100, new Point(300, 250)) + ]; + yield [ + new Size(800, 600), + new Size(100, 200), + Alignment::CENTER, + new Size(100, 200, new Point(350, 200)) + ]; + yield [ + new Size(800, 600), + new Size(2000, 10), + Alignment::CENTER, + new Size(2000, 10, new Point(-600, 295)) + ]; + yield [ + new Size(800, 600), + new Size(10, 2000), + Alignment::CENTER, + new Size(10, 2000, new Point(395, -700)) + ]; + yield [ + new Size(800, 600), + new Size(800, 600), + Alignment::CENTER, + new Size(800, 600, new Point(0, 0)) + ]; + yield [ + new Size(400, 300), + new Size(120, 120), + Alignment::CENTER, + new Size(120, 120, new Point(140, 90)) + ]; + yield [ + new Size(600, 800), + new Size(100, 100), + Alignment::CENTER, + new Size(100, 100, new Point(250, 350)) + ]; + } +} diff --git a/tests/Providers/ResourceProvider.php b/tests/Providers/ResourceProvider.php new file mode 100644 index 000000000..fc60f8fd3 --- /dev/null +++ b/tests/Providers/ResourceProvider.php @@ -0,0 +1,207 @@ + Format::PNG, + 'resource' => new Resource('300dpi.png'), + 'size' => new Size(200, 100), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(300, 300), + ]; + + yield [ + 'format' => Format::PNG, + 'resource' => new Resource('150.dpi.png'), + 'size' => new Size(32, 32), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(150, 150), + ]; + + yield [ + 'format' => Format::JPEG, + 'resource' => new Resource('150.dpi.jpg'), + 'size' => new Size(32, 32), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(150, 150), + ]; + + yield [ + 'format' => Format::TIFF, + 'resource' => new Resource('150.dpi.tif'), + 'size' => new Size(32, 32), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(150, 150), + ]; + + yield [ + 'format' => Format::GIF, + 'resource' => new Resource('animation.gif'), + 'size' => new Size(20, 15), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(0, 0), + ]; + + yield [ + 'format' => Format::PNG, + 'resource' => new Resource('blocks.png'), + 'size' => new Size(640, 480), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(72, 72), + ]; + + yield [ + 'format' => Format::GIF, + 'resource' => new Resource('blue.gif'), + 'size' => new Size(16, 16), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(0, 0), + ]; + + yield [ + 'format' => Format::GIF, + 'resource' => new Resource('cats.gif'), + 'size' => new Size(75, 50), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(0, 0), + ]; + + yield [ + 'format' => Format::PNG, + 'resource' => new Resource('circle.png'), + 'size' => new Size(50, 50), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(0, 0), + ]; + + yield [ + 'format' => Format::JPEG, + 'resource' => new Resource('cmyk.jpg'), + 'size' => new Size(12, 12), + 'colorspace' => CmykSpace::class, + 'resolution' => new Resolution(300, 300), + ]; + + yield [ + 'format' => Format::JPEG, + 'resource' => new Resource('exif.jpg'), + 'size' => new Size(16, 16), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(72, 72), + ]; + + yield [ + 'format' => Format::GIF, + 'resource' => new Resource('gradient.gif'), + 'size' => new Size(16, 16), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(0, 0), + ]; + + yield [ + 'format' => Format::BMP, + 'resource' => new Resource('gradient.bmp'), + 'size' => new Size(8, 8), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(72, 72), + ]; + + yield [ + 'format' => Format::GIF, + 'resource' => new Resource('green.gif'), + 'size' => new Size(16, 16), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(0, 0), + ]; + + yield [ + 'format' => Format::JPEG, + 'resource' => new Resource('orientation.jpg'), + 'size' => new Size(20, 30), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(72, 72), + ]; + + yield [ + 'format' => Format::PNG, + 'resource' => new Resource('radial.png'), + 'size' => new Size(50, 50), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(72, 72), + ]; + + yield [ + 'format' => Format::GIF, + 'resource' => new Resource('red.gif'), + 'size' => new Size(16, 16), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(0, 0), + ]; + + yield [ + 'format' => Format::JPEG, + 'resource' => new Resource('test.jpg'), + 'size' => new Size(320, 240), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(72, 72), + ]; + + yield [ + 'format' => Format::PNG, + 'resource' => new Resource('tile.png'), + 'size' => new Size(16, 16), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(0, 0), + ]; + + yield [ + 'format' => Format::PNG, + 'resource' => new Resource('trim.png'), + 'size' => new Size(50, 50), + 'colorspace' => RgbSpace::class, + 'resolution' => new Resolution(0, 0), + ]; + } + + public static function resourceData(): Generator + { + foreach (static::baseData() as $yield) { + yield [$yield['resource']]; + } + } + + public static function sizeData(): Generator + { + foreach (static::baseData() as $yield) { + yield [$yield['resource'], $yield['size']]; + } + } + + public static function colorspaceData(): Generator + { + foreach (static::baseData() as $yield) { + yield [$yield['resource'], $yield['colorspace']]; + } + } + + public static function resolutionData(): Generator + { + foreach (static::baseData() as $yield) { + yield [$yield['resource'], $yield['resolution']]; + } + } +} diff --git a/tests/PsrResponseCommandTest.php b/tests/PsrResponseCommandTest.php deleted file mode 100644 index b28a45f99..000000000 --- a/tests/PsrResponseCommandTest.php +++ /dev/null @@ -1,57 +0,0 @@ -'; - - $image = Mockery::mock('Intervention\Image\Image'); - $stream = \GuzzleHttp\Psr7\stream_for($encodedContent); - - $image->shouldReceive('stream') - ->with('jpg', 87) - ->once() - ->andReturn($stream); - - $image->shouldReceive('getEncoded') - ->twice() - ->andReturn($encodedContent); - - $command = new PsrResponseCommand(array('jpg', 87)); - $result = $command->execute($image); - - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - - $output = $command->getOutput(); - - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $output); - - /** - * @var \Psr\Http\Message\ResponseInterface $output - */ - $this->assertEquals($stream, $output->getBody()); - $this->assertEquals($encodedContent, (string)$output->getBody()); - - $this->assertTrue($output->hasHeader('Content-Type')); - $this->assertTrue($output->hasHeader('Content-Length')); - - $this->assertEquals( - "application/xml", - $output->getHeaderLine('Content-Type') - ); - - $this->assertEquals( - strlen($encodedContent), - $output->getHeaderLine('Content-Length') - ); - } -} \ No newline at end of file diff --git a/tests/RectangleCommandTest.php b/tests/RectangleCommandTest.php deleted file mode 100644 index d3742f132..000000000 --- a/tests/RectangleCommandTest.php +++ /dev/null @@ -1,42 +0,0 @@ -shouldReceive('getDriverName')->once()->andReturn('Gd'); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $command = new RectangleCommand(array(10, 15, 100, 150)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertFalse($command->hasOutput()); - } - - public function testImagick() - { - $imagick = Mockery::mock('\Imagick'); - $imagick->shouldReceive('drawimage'); - $driver = Mockery::mock('\Intervention\Image\Imagick\Driver'); - $driver->shouldReceive('getDriverName')->once()->andReturn('Imagick'); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - - $command = new RectangleCommand(array(10, 15, 100, 150)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertFalse($command->hasOutput()); - } - -} diff --git a/tests/RectangleShapeTest.php b/tests/RectangleShapeTest.php deleted file mode 100644 index 1f652ab0d..000000000 --- a/tests/RectangleShapeTest.php +++ /dev/null @@ -1,53 +0,0 @@ -assertInstanceOf('Intervention\Image\Gd\Shapes\RectangleShape', $rectangle); - $this->assertEquals(10, $rectangle->x1); - $this->assertEquals(15, $rectangle->y1); - $this->assertEquals(100, $rectangle->x2); - $this->assertEquals(150, $rectangle->y2); - - // imagick - $rectangle = new RectangleImagick(10, 15, 100, 150); - $this->assertInstanceOf('Intervention\Image\Imagick\Shapes\RectangleShape', $rectangle); - $this->assertEquals(10, $rectangle->x1); - $this->assertEquals(15, $rectangle->y1); - $this->assertEquals(100, $rectangle->x2); - $this->assertEquals(150, $rectangle->y2); - } - - public function testApplyToImage() - { - // gd - $core = imagecreatetruecolor(300, 200); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $rectangle = new RectangleGd(10, 15, 100, 150); - $result = $rectangle->applyToImage($image, 10, 20); - $this->assertInstanceOf('Intervention\Image\Gd\Shapes\RectangleShape', $rectangle); - $this->assertTrue($result); - - // imagick - $core = Mockery::mock('\Imagick'); - $core->shouldReceive('drawimage')->once(); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($core); - $rectangle = new RectangleImagick(10, 15, 100, 150); - $result = $rectangle->applyToImage($image, 10, 20); - $this->assertInstanceOf('Intervention\Image\Imagick\Shapes\RectangleShape', $rectangle); - $this->assertTrue($result); - } -} diff --git a/tests/ResetCommandTest.php b/tests/ResetCommandTest.php deleted file mode 100644 index 18d823dda..000000000 --- a/tests/ResetCommandTest.php +++ /dev/null @@ -1,70 +0,0 @@ -shouldReceive('cloneCore')->with($resource)->once()->andReturn($resource); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('setCore')->once(); - $image->shouldReceive('getBackup')->once()->andReturn($resource); - $command = new ResetGd(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testGdWithName() - { - $size = Mockery::mock('Intervention\Image\Size', array(800, 600)); - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $image = Mockery::mock('Intervention\Image\Image'); - $driver = Mockery::mock('Intervention\Image\Gd\Driver'); - $driver->shouldReceive('cloneCore')->with($resource)->once()->andReturn($resource); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('setCore')->once(); - $image->shouldReceive('getBackup')->once()->withArgs(array('fooBackup'))->andReturn($resource); - $command = new ResetGd(array('fooBackup')); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagickWithoutName() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('clear')->once(); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $image->shouldReceive('setCore')->once(); - $image->shouldReceive('getBackup')->once()->andReturn($imagick); - $command = new ResetImagick(array()); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagickWithName() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('clear')->once(); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $image->shouldReceive('setCore')->once(); - $image->shouldReceive('getBackup')->once()->withArgs(array('fooBackup'))->andReturn($imagick); - $command = new ResetImagick(array('fooBackup')); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/ResizeCanvasCommandTest.php b/tests/ResizeCanvasCommandTest.php deleted file mode 100644 index c183ff1bf..000000000 --- a/tests/ResizeCanvasCommandTest.php +++ /dev/null @@ -1,79 +0,0 @@ -shouldReceive('align')->with('center')->andReturn($canvas_size); - $canvas_size->shouldReceive('relativePosition')->andReturn($canvas_pos); - $image_pos = Mockery::mock('\Intervention\Image\Point', array(0, 0)); - $image_size = Mockery::mock('\Intervention\Image\Size', array(800, 600)); - $image_size->shouldReceive('align')->with('center')->andReturn($image_size); - $image_size->shouldReceive('relativePosition')->andReturn($image_pos); - $canvas = Mockery::mock('\Intervention\Image\Image'); - $canvas->shouldReceive('getCore')->times(5)->andReturn($resource); - $canvas->shouldReceive('getSize')->andReturn($canvas_size); - $driver = Mockery::mock('\Intervention\Image\Gd\Driver'); - $driver->shouldReceive('newImage')->with(820, 640, '#b53717')->once()->andReturn($canvas); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getSize')->once()->andReturn($image_size); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('setCore')->once(); - $command = new ResizeCanvasGd(array(20, 40, 'center', true, '#b53717')); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $canvas_pos = Mockery::mock('\Intervention\Image\Point', array(0, 0)); - $canvas_size = Mockery::mock('\Intervention\Image\Size', array(820, 640)); - $canvas_size->shouldReceive('align')->with('center')->andReturn($canvas_size); - $canvas_size->shouldReceive('relativePosition')->andReturn($canvas_pos); - $image_pos = Mockery::mock('\Intervention\Image\Point', array(0, 0)); - $image_size = Mockery::mock('\Intervention\Image\Size', array(800, 600)); - $image_size->shouldReceive('align')->with('center')->andReturn($image_size); - $image_size->shouldReceive('relativePosition')->andReturn($image_pos); - $canvas = Mockery::mock('\Intervention\Image\Image'); - - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('cropimage')->with(800, 600, 0, 0)->once(); - $imagick->shouldReceive('compositeimage')->with($imagick, 40, 0, 0)->once(); - $imagick->shouldReceive('setimagepage')->with(0, 0, 0, 0)->once(); - $imagick->shouldReceive('drawimage')->once(); - $imagick->shouldReceive('transparentpaintimage')->once(); - $imagick->shouldReceive('getimagecolorspace')->once(); - $imagick->shouldReceive('setimagecolorspace')->once(); - - $canvas->shouldReceive('getCore')->times(6)->andReturn($imagick); - $canvas->shouldReceive('getSize')->andReturn($canvas_size); - $canvas->shouldReceive('pickColor')->with(0, 0, 'hex')->once()->andReturn('#000000'); - $driver = Mockery::mock('\Intervention\Image\Gd\Driver'); - $driver->shouldReceive('newImage')->with(820, 640, '#b53717')->once()->andReturn($canvas); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getSize')->once()->andReturn($image_size); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('getCore')->times(3)->andReturn($imagick); - $image->shouldReceive('setCore')->once(); - $command = new ResizeCanvasImagick(array(20, 40, 'center', true, '#b53717')); - $result = $command->execute($image); - $this->assertTrue($result); - } - -} diff --git a/tests/ResizeCommandTest.php b/tests/ResizeCommandTest.php deleted file mode 100644 index e9af7669b..000000000 --- a/tests/ResizeCommandTest.php +++ /dev/null @@ -1,48 +0,0 @@ -upsize(); }; - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $image = Mockery::mock('Intervention\Image\Image'); - $size = Mockery::mock('Intervention\Image\Size', array(800, 600)); - $size->shouldReceive('resize')->with(300, 200, $callback)->once()->andReturn($size); - $size->shouldReceive('getWidth')->once()->andReturn(800); - $size->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('getSize')->once()->andReturn($size); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('setCore')->once(); - $command = new ResizeCommandGd(array(300, 200, $callback)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $callback = function ($constraint) { $constraint->upsize(); }; - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('scaleimage')->with(300, 200)->once()->andReturn(true); - $size = Mockery::mock('Intervention\Image\Size', array(800, 600)); - $size->shouldReceive('resize')->with(300, 200, $callback)->once()->andReturn($size); - $size->shouldReceive('getWidth')->once()->andReturn(300); - $size->shouldReceive('getHeight')->once()->andReturn(200); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $image->shouldReceive('getSize')->once()->andReturn($size); - $command = new ResizeCommandImagick(array(300, 200, $callback)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/Resource.php b/tests/Resource.php new file mode 100644 index 000000000..3aa3b3320 --- /dev/null +++ b/tests/Resource.php @@ -0,0 +1,107 @@ +filename; + } + + public function path(): string + { + return sprintf('%s/resources/%s', __DIR__, $this->filename); + } + + public function stringablePath(): Stringable + { + $path = $this->path(); + + return new class ($path) implements Stringable + { + public function __construct(private string $path) + { + // + } + + public function __toString(): string + { + return $this->path; + } + }; + } + + public function data(): string + { + return file_get_contents($this->path()); + } + + public function base64(): string + { + return base64_encode($this->data()); + } + + public function dataUri(): string + { + return DataUri::create($this->data())->toString(); + } + + public function splFileInfo(): SplFileInfo + { + return new SplFileInfo($this->path()); + } + + public function stringableData(): Stringable + { + $data = $this->data(); + + return new class ($data) implements Stringable + { + public function __construct(private string $data) + { + // + } + + public function __toString(): string + { + return $this->data; + } + }; + } + + public function stream(): mixed + { + $stream = fopen('php://temp', 'rw'); + fwrite($stream, $this->data()); + rewind($stream); + + return $stream; + } + + public function imageObject(string|DriverInterface $driver): ImageInterface + { + return (is_string($driver) ? new $driver() : $driver) + ->specializeDecoder(new FilePathImageDecoder()) + ->decode($this->path()); + } +} diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php deleted file mode 100644 index f1181de20..000000000 --- a/tests/ResponseTest.php +++ /dev/null @@ -1,29 +0,0 @@ -assertInstanceOf('\Intervention\Image\Response', $response); - $this->assertInstanceOf('\Intervention\Image\Image', $response->image); - } - - public function testConstructorWithParameters() - { - $image = Mockery::mock('\Intervention\Image\Image'); - $response = new Response($image, 'jpg', 75); - $this->assertInstanceOf('\Intervention\Image\Response', $response); - $this->assertInstanceOf('\Intervention\Image\Image', $response->image); - $this->assertEquals('jpg', $response->format); - $this->assertEquals(75, $response->quality); - } -} diff --git a/tests/RotateCommandTest.php b/tests/RotateCommandTest.php deleted file mode 100644 index dedf8a0fc..000000000 --- a/tests/RotateCommandTest.php +++ /dev/null @@ -1,35 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('setCore')->once()->andReturn($resource); - $command = new RotateGd(array(45, '#b53717')); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $pixel = Mockery::mock('ImagickPixel', array('#b53717')); - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('rotateimage')->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new RotateImagick(array(45, '#b53717')); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/SharpenCommandTest.php b/tests/SharpenCommandTest.php deleted file mode 100644 index b88869468..000000000 --- a/tests/SharpenCommandTest.php +++ /dev/null @@ -1,33 +0,0 @@ -shouldReceive('getCore')->once()->andReturn($resource); - $command = new SharpenGd(array(50)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('unsharpmaskimage')->with(1, 1, 8, 0)->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $command = new SharpenImagick(array(50)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/SizeTest.php b/tests/SizeTest.php deleted file mode 100644 index 4be87ee09..000000000 --- a/tests/SizeTest.php +++ /dev/null @@ -1,435 +0,0 @@ -assertInstanceOf('Intervention\Image\Size', $size); - $this->assertInstanceOf('Intervention\Image\Point', $size->pivot); - $this->assertEquals(1, $size->width); - $this->assertEquals(1, $size->height); - } - - public function testConstructorWithCoordinates() - { - $pivot = Mockery::mock('Intervention\Image\Point'); - $size = new Size(300, 200, $pivot); - $this->assertInstanceOf('Intervention\Image\Size', $size); - $this->assertInstanceOf('Intervention\Image\Point', $size->pivot); - $this->assertEquals(300, $size->width); - $this->assertEquals(200, $size->height); - } - - public function testGetWidth() - { - $size = new Size(800, 600); - $this->assertEquals(800, $size->getWidth()); - } - - public function testGetHeight() - { - $size = new Size(800, 600); - $this->assertEquals(600, $size->getHeight()); - } - - public function testGetRatio() - { - $size = new Size(800, 600); - $this->assertEquals(1.33333333333, $size->getRatio()); - - $size = new Size(100, 100); - $this->assertEquals(1, $size->getRatio()); - - $size = new Size(1920, 1080); - $this->assertEquals(1.777777777778, $size->getRatio()); - } - - public function testResize() - { - $size = new Size(800, 600); - $size->resize(1000, 2000); - $this->assertEquals(1000, $size->width); - $this->assertEquals(2000, $size->height); - - $size = new Size(800, 600); - $size->resize(2000, null); - $this->assertEquals(2000, $size->width); - $this->assertEquals(600, $size->height); - - $size = new Size(800, 600); - $size->resize(null, 1000); - $this->assertEquals(800, $size->width); - $this->assertEquals(1000, $size->height); - } - - public function testResizeWithCallbackAspectRatio() - { - $size = new Size(800, 600); - $size->resize(1000, 2000, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(1000, $size->width); - $this->assertEquals(750, $size->height); - - $size = new Size(800, 600); - $size->resize(2000, 1000, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(1333, $size->width); - $this->assertEquals(1000, $size->height); - - $size = new Size(800, 600); - $size->resize(null, 3000, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(4000, $size->width); - $this->assertEquals(3000, $size->height); - - $size = new Size(800, 600); - $size->resize(8000, null, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(8000, $size->width); - $this->assertEquals(6000, $size->height); - - $size = new Size(800, 600); - $size->resize(100, 400, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(100, $size->width); - $this->assertEquals(75, $size->height); - - $size = new Size(800, 600); - $size->resize(400, 100, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(133, $size->width); - $this->assertEquals(100, $size->height); - - $size = new Size(800, 600); - $size->resize(null, 300, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(400, $size->width); - $this->assertEquals(300, $size->height); - - $size = new Size(800, 600); - $size->resize(80, null, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(80, $size->width); - $this->assertEquals(60, $size->height); - - $size = new Size(640, 480); - $size->resize(225, null, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(225, $size->width); - $this->assertEquals(169, $size->height); - - $size = new Size(640, 480); - $size->resize(223, null, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(223, $size->width); - $this->assertEquals(167, $size->height); - - $size = new Size(600, 800); - $size->resize(300, 300, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(225, $size->width); - $this->assertEquals(300, $size->height); - - $size = new Size(800, 600); - $size->resize(400, 10, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(13, $size->width); - $this->assertEquals(10, $size->height); - - $size = new Size(800, 600); - $size->resize(1000, 1200, function ($c) { $c->aspectRatio(); }); - $this->assertEquals(1000, $size->width); - $this->assertEquals(750, $size->height); - } - - public function testResizeWithCallbackUpsize() - { - $size = new Size(800, 600); - $size->resize(1000, 2000, function ($c) { $c->upsize(); }); - $this->assertEquals(800, $size->width); - $this->assertEquals(600, $size->height); - - $size = new Size(800, 600); - $size->resize(400, 1000, function ($c) { $c->upsize(); }); - $this->assertEquals(400, $size->width); - $this->assertEquals(600, $size->height); - - $size = new Size(800, 600); - $size->resize(1000, 400, function ($c) { $c->upsize(); }); - $this->assertEquals(800, $size->width); - $this->assertEquals(400, $size->height); - - $size = new Size(800, 600); - $size->resize(400, 300, function ($c) { $c->upsize(); }); - $this->assertEquals(400, $size->width); - $this->assertEquals(300, $size->height); - - $size = new Size(800, 600); - $size->resize(1000, null, function ($c) { $c->upsize(); }); - $this->assertEquals(800, $size->width); - $this->assertEquals(600, $size->height); - - $size = new Size(800, 600); - $size->resize(null, 1000, function ($c) { $c->upsize(); }); - $this->assertEquals(800, $size->width); - $this->assertEquals(600, $size->height); - } - - public function testResizeWithCallbackAspectRatioAndUpsize() - { - $size = new Size(800, 600); - $size->resize(1000, 2000, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(800, $size->width); - $this->assertEquals(600, $size->height); - - $size = new Size(800, 600); - $size->resize(1000, 600, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(800, $size->width); - $this->assertEquals(600, $size->height); - - $size = new Size(800, 600); - $size->resize(1000, 300, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(400, $size->width); - $this->assertEquals(300, $size->height); - - $size = new Size(800, 600); - $size->resize(400, 1000, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(400, $size->width); - $this->assertEquals(300, $size->height); - - $size = new Size(800, 600); - $size->resize(400, null, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(400, $size->width); - $this->assertEquals(300, $size->height); - - $size = new Size(800, 600); - $size->resize(null, 300, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(400, $size->width); - $this->assertEquals(300, $size->height); - - $size = new Size(800, 600); - $size->resize(1000, null, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(800, $size->width); - $this->assertEquals(600, $size->height); - - $size = new Size(800, 600); - $size->resize(null, 1000, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(800, $size->width); - $this->assertEquals(600, $size->height); - - $size = new Size(800, 600); - $size->resize(100, 100, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(100, $size->width); - $this->assertEquals(75, $size->height); - - $size = new Size(800, 600); - $size->resize(300, 200, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(267, $size->width); - $this->assertEquals(200, $size->height); - - $size = new Size(600, 800); - $size->resize(300, 300, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(225, $size->width); - $this->assertEquals(300, $size->height); - - $size = new Size(800, 600); - $size->resize(400, 10, function ($c) { $c->aspectRatio(); $c->upsize(); }); - $this->assertEquals(13, $size->width); - $this->assertEquals(10, $size->height); - } - - public function testRelativePosition() - { - $container = new Size(800, 600); - $input = new Size(200, 100); - $container->align('top-left'); - $input->align('top-left'); - $pos = $container->relativePosition($input); - $this->assertEquals(0, $pos->x); - $this->assertEquals(0, $pos->y); - - $container = new Size(800, 600); - $input = new Size(200, 100); - $container->align('center'); - $input->align('top-left'); - $pos = $container->relativePosition($input); - $this->assertEquals(400, $pos->x); - $this->assertEquals(300, $pos->y); - - $container = new Size(800, 600); - $input = new Size(200, 100); - $container->align('bottom-right'); - $input->align('top-right'); - $pos = $container->relativePosition($input); - $this->assertEquals(600, $pos->x); - $this->assertEquals(600, $pos->y); - - $container = new Size(800, 600); - $input = new Size(200, 100); - $container->align('center'); - $input->align('center'); - $pos = $container->relativePosition($input); - $this->assertEquals(300, $pos->x); - $this->assertEquals(250, $pos->y); - } - - public function testAlign() - { - $width = 640; - $height = 480; - $pivot = Mockery::mock('Intervention\Image\Point'); - $pivot->shouldReceive('setPosition')->with(0, 0)->once(); - $pivot->shouldReceive('setPosition')->with(intval($width/2), 0)->once(); - $pivot->shouldReceive('setPosition')->with($width, 0)->once(); - $pivot->shouldReceive('setPosition')->with(0, intval($height/2))->once(); - $pivot->shouldReceive('setPosition')->with(intval($width/2), intval($height/2))->once(); - $pivot->shouldReceive('setPosition')->with($width, intval($height/2))->once(); - $pivot->shouldReceive('setPosition')->with(0, $height)->once(); - $pivot->shouldReceive('setPosition')->with(intval($width/2), $height)->once(); - $pivot->shouldReceive('setPosition')->with($width, $height)->once(); - - $box = new Size($width, $height, $pivot); - $box->align('top-left'); - $box->align('top'); - $box->align('top-right'); - $box->align('left'); - $box->align('center'); - $box->align('right'); - $box->align('bottom-left'); - $box->align('bottom'); - $b = $box->align('bottom-right'); - $this->assertInstanceOf('Intervention\Image\Size', $b); - } - - public function testFit() - { - $box = new Size(800, 600); - $fitted = $box->fit(new Size(100, 100)); - $this->assertEquals(600, $fitted->width); - $this->assertEquals(600, $fitted->height); - $this->assertEquals(100, $fitted->pivot->x); - $this->assertEquals(0, $fitted->pivot->y); - - $box = new Size(800, 600); - $fitted = $box->fit(new Size(200, 100)); - $this->assertEquals(800, $fitted->width); - $this->assertEquals(400, $fitted->height); - $this->assertEquals(0, $fitted->pivot->x); - $this->assertEquals(100, $fitted->pivot->y); - - $box = new Size(800, 600); - $fitted = $box->fit(new Size(100, 200)); - $this->assertEquals(300, $fitted->width); - $this->assertEquals(600, $fitted->height); - $this->assertEquals(250, $fitted->pivot->x); - $this->assertEquals(0, $fitted->pivot->y); - - $box = new Size(800, 600); - $fitted = $box->fit(new Size(2000, 10)); - $this->assertEquals(800, $fitted->width); - $this->assertEquals(4, $fitted->height); - $this->assertEquals(0, $fitted->pivot->x); - $this->assertEquals(298, $fitted->pivot->y); - - $box = new Size(800, 600); - $fitted = $box->fit(new Size(10, 2000)); - $this->assertEquals(3, $fitted->width); - $this->assertEquals(600, $fitted->height); - $this->assertEquals(399, $fitted->pivot->x); - $this->assertEquals(0, $fitted->pivot->y); - - $box = new Size(800, 600); - $fitted = $box->fit(new Size(800, 600)); - $this->assertEquals(800, $fitted->width); - $this->assertEquals(600, $fitted->height); - $this->assertEquals(0, $fitted->pivot->x); - $this->assertEquals(0, $fitted->pivot->y); - - $box = new Size(400, 300); - $fitted = $box->fit(new Size(120, 120)); - $this->assertEquals(300, $fitted->width); - $this->assertEquals(300, $fitted->height); - $this->assertEquals(50, $fitted->pivot->x); - $this->assertEquals(0, $fitted->pivot->y); - - $box = new Size(600, 800); - $fitted = $box->fit(new Size(100, 100)); - $this->assertEquals(600, $fitted->width); - $this->assertEquals(600, $fitted->height); - $this->assertEquals(0, $fitted->pivot->x); - $this->assertEquals(100, $fitted->pivot->y); - } - - /** - * @dataProvider providerFitWithPosition - */ - public function testFitWithPosition(Size $box, $position, $x, $y) - { - $fitted = $box->fit(new Size(100, 100), $position); - $this->assertEquals(600, $fitted->width); - $this->assertEquals(600, $fitted->height); - $this->assertEquals($x, $fitted->pivot->x); - $this->assertEquals($y, $fitted->pivot->y); - } - - public function providerFitWithPosition() - { - return array( - array(new Size(800, 600), 'top-left', 0, 0), - array(new Size(800, 600), 'top', 100, 0), - array(new Size(800, 600), 'top-right', 200, 0), - array(new Size(800, 600), 'left', 0, 0), - array(new Size(800, 600), 'center', 100, 0), - array(new Size(800, 600), 'right', 200, 0), - array(new Size(800, 600), 'bottom-left', 0, 0), - array(new Size(800, 600), 'bottom', 100, 0), - array(new Size(800, 600), 'bottom-right', 200, 0), - - array(new Size(600, 800), 'top-left', 0, 0), - array(new Size(600, 800), 'top', 0, 0), - array(new Size(600, 800), 'top-right', 0, 0), - array(new Size(600, 800), 'left', 0, 100), - array(new Size(600, 800), 'center', 0, 100), - array(new Size(600, 800), 'right', 0, 100), - array(new Size(600, 800), 'bottom-left', 0, 200), - array(new Size(600, 800), 'bottom', 0, 200), - array(new Size(600, 800), 'bottom-right', 0, 200), - ); - } - - public function testFitsInto() - { - $box = new Size(800, 600); - $fits = $box->fitsInto(new Size(100, 100)); - $this->assertFalse($fits); - - $box = new Size(800, 600); - $fits = $box->fitsInto(new Size(1000, 100)); - $this->assertFalse($fits); - - $box = new Size(800, 600); - $fits = $box->fitsInto(new Size(100, 1000)); - $this->assertFalse($fits); - - $box = new Size(800, 600); - $fits = $box->fitsInto(new Size(800, 600)); - $this->assertTrue($fits); - - $box = new Size(800, 600); - $fits = $box->fitsInto(new Size(1000, 1000)); - $this->assertTrue($fits); - - $box = new Size(100, 100); - $fits = $box->fitsInto(new Size(800, 600)); - $this->assertTrue($fits); - - $box = new Size(100, 100); - $fits = $box->fitsInto(new Size(80, 60)); - $this->assertFalse($fits); - } - - /** - * @expectedException \Intervention\Image\Exception\InvalidArgumentException - */ - public function testInvalidResize() - { - $size = new Size(800, 600); - $size->resize(null, null); - } -} diff --git a/tests/StreamCommandTest.php b/tests/StreamCommandTest.php deleted file mode 100644 index 79d1048f1..000000000 --- a/tests/StreamCommandTest.php +++ /dev/null @@ -1,35 +0,0 @@ -shouldReceive('encode') - ->with('jpg', 87) - ->once() - ->andReturnSelf(); - - $image->shouldReceive('getEncoded') - ->once() - ->andReturn($encodedContent); - - $command = new StreamCommand(array('jpg', 87)); - $result = $command->execute($image); - - $this->assertTrue($result); - $this->assertTrue($command->hasOutput()); - - $output = $command->getOutput(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $output); - $this->assertEquals($encodedContent, (string)$output); - } -} \ No newline at end of file diff --git a/tests/TextCommandTest.php b/tests/TextCommandTest.php deleted file mode 100644 index 53df1650d..000000000 --- a/tests/TextCommandTest.php +++ /dev/null @@ -1,31 +0,0 @@ -shouldReceive('getDriverName')->once()->andReturn('Gd'); - $image = Mockery::mock('\Intervention\Image\Image'); - $image->shouldReceive('getDriver')->once()->andReturn($driver); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $command = new TextCommand(array('test', 10, 20)); - $result = $command->execute($image); - $this->assertTrue($result); - $this->assertFalse($command->hasOutput()); - } - - public function testImagick() - { - # code... - } - -} diff --git a/tests/Traits/CanDetectProgressiveJpeg.php b/tests/Traits/CanDetectProgressiveJpeg.php new file mode 100644 index 000000000..c36692049 --- /dev/null +++ b/tests/Traits/CanDetectProgressiveJpeg.php @@ -0,0 +1,57 @@ +toStream(); + + while (!feof($f)) { + if (unpack('C', fread($f, 1))[1] !== 0xff) { + return false; + } + + $blockType = unpack('C', fread($f, 1))[1]; + + switch (true) { + case $blockType == 0xd8: + case $blockType >= 0xd0 && $blockType <= 0xd7: + break; + + case $blockType == 0xc0: + fclose($f); + return false; + + case $blockType == 0xc2: + fclose($f); + return true; + + case $blockType == 0xd9: + break 2; + + default: + $blockSize = unpack('n', fread($f, 2))[1]; + fseek($f, $blockSize - 2, SEEK_CUR); + break; + } + } + + fclose($f); + + return false; + } +} diff --git a/tests/Traits/CanInspectPngFormat.php b/tests/Traits/CanInspectPngFormat.php new file mode 100644 index 000000000..0ad2e020d --- /dev/null +++ b/tests/Traits/CanInspectPngFormat.php @@ -0,0 +1,49 @@ +toStream(); + $contents = fread($f, 32); + fclose($f); + + return ord($contents[28]) != 0; + } + + /** + * Try to detect PNG color type from given binary data. + */ + private function pngColorType(EncodedImage $image): string + { + $data = $image->toString(); + + if (substr($data, 1, 3) !== 'PNG') { + return 'unkown'; + } + + $pos = strpos($data, 'IHDR'); + $type = substr($data, $pos + 13, 1); + + return match (unpack('C', $type)[1]) { + 0 => 'grayscale', + 2 => 'truecolor', + 3 => 'indexed', + 4 => 'grayscale-alpha', + 6 => 'truecolor-alpha', + default => 'unknown', + }; + } +} diff --git a/tests/TrimCommandTest.php b/tests/TrimCommandTest.php deleted file mode 100644 index de6d65ee1..000000000 --- a/tests/TrimCommandTest.php +++ /dev/null @@ -1,53 +0,0 @@ -shouldReceive('differs')->with($baseColor, 45)->andReturn(true); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('pickColor')->with(0, 0, 'object')->times(2)->andReturn($baseColor); - $image->shouldReceive('pickColor')->with(799, 0, 'object')->once()->andReturn($baseColor); - $image->shouldReceive('setCore')->once(); - $command = new TrimGd(array('top-left', array('left', 'right'), 45, 2)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $baseColorPixel = new \ImagickPixel; - $baseColor = Mockery::mock('Intervention\Image\Gd\Color'); - $baseColor->shouldReceive('getPixel')->once()->andReturn($baseColorPixel); - $imagick = Mockery::mock('Imagick'); - $imagick->width = 100; - $imagick->height = 100; - $imagick->shouldReceive('borderimage')->with($baseColorPixel, 1, 1)->once()->andReturn(true); - $imagick->shouldReceive('trimimage')->with(29632.5)->once()->andReturn(true); - $imagick->shouldReceive('getimagepage')->once()->andReturn(array('x' => 50, 'y' => 50)); - $imagick->shouldReceive('cropimage')->with(104, 202, 47, 0)->once()->andReturn(true); - $imagick->shouldReceive('setimagepage')->with(0, 0, 0, 0)->once()->andReturn(true); - $imagick->shouldReceive('destroy')->with()->once()->andReturn(true); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('pickColor')->with(0, 0, 'object')->once()->andReturn($baseColor); - $image->shouldReceive('getCore')->times(3)->andReturn($imagick); - $command = new TrimImagick(array('top-left', array('left', 'right'), 45, 2)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/Unit/AlignmentTest.php b/tests/Unit/AlignmentTest.php new file mode 100644 index 000000000..04fa0e8de --- /dev/null +++ b/tests/Unit/AlignmentTest.php @@ -0,0 +1,277 @@ +assertEquals($result, Alignment::create($value)); + } + + #[DataProviderExternal(InputDataProvider::class, 'alignmentInputs')] + public function testTryCreate(string|Alignment $value, Alignment $result): void + { + $this->assertEquals($result, Alignment::tryCreate($value)); + } + + public function testCreateInvalid(): void + { + $this->expectException(InvalidArgumentException::class); + Alignment::create('invalid'); + } + + public function testTryCreateInvalid(): void + { + $this->assertNull(Alignment::tryCreate('invalid')); + } + + #[DataProvider('alignHorizontallyDataProvider')] + public function testAlignHorizontally(Alignment $base, Alignment $alignTo, Alignment $result): void + { + $this->assertEquals($result, $base->alignHorizontally($alignTo)); + } + + #[DataProvider('alignVerticallyDataProvider')] + public function testAlignVertically(Alignment $base, Alignment $alignTo, Alignment $result): void + { + $this->assertEquals($result, $base->alignVertically($alignTo)); + } + + public function testAlignHorizontallyFallback(): void + { + $this->assertEquals(Alignment::CENTER, Alignment::CENTER->alignHorizontally('foo')); + } + + public function testAlignVerticallyFallback(): void + { + $this->assertEquals(Alignment::CENTER, Alignment::CENTER->alignVertically('foo')); + } + + public function testHorizontal(): void + { + $this->assertEquals(Alignment::LEFT, Alignment::TOP_LEFT->horizontal()); + $this->assertEquals(Alignment::LEFT, Alignment::LEFT->horizontal()); + $this->assertEquals(Alignment::LEFT, Alignment::BOTTOM_LEFT->horizontal()); + + $this->assertEquals(Alignment::RIGHT, Alignment::TOP_RIGHT->horizontal()); + $this->assertEquals(Alignment::RIGHT, Alignment::RIGHT->horizontal()); + $this->assertEquals(Alignment::RIGHT, Alignment::BOTTOM_RIGHT->horizontal()); + + $this->assertEquals(Alignment::CENTER, Alignment::TOP->horizontal()); + $this->assertEquals(Alignment::CENTER, Alignment::BOTTOM->horizontal()); + $this->assertEquals(Alignment::CENTER, Alignment::CENTER->horizontal()); + } + + public function testVertical(): void + { + $this->assertEquals(Alignment::TOP, Alignment::TOP_LEFT->vertical()); + $this->assertEquals(Alignment::TOP, Alignment::TOP->vertical()); + $this->assertEquals(Alignment::TOP, Alignment::TOP_RIGHT->vertical()); + + $this->assertEquals(Alignment::BOTTOM, Alignment::BOTTOM_LEFT->vertical()); + $this->assertEquals(Alignment::BOTTOM, Alignment::BOTTOM->vertical()); + $this->assertEquals(Alignment::BOTTOM, Alignment::BOTTOM_RIGHT->vertical()); + + $this->assertEquals(Alignment::CENTER, Alignment::LEFT->vertical()); + $this->assertEquals(Alignment::CENTER, Alignment::RIGHT->vertical()); + $this->assertEquals(Alignment::CENTER, Alignment::CENTER->vertical()); + } + + public static function alignHorizontallyDataProvider(): Generator + { + yield [Alignment::TOP_LEFT, Alignment::TOP_LEFT, Alignment::TOP_LEFT]; + yield [Alignment::TOP, Alignment::TOP_LEFT, Alignment::TOP_LEFT]; + yield [Alignment::TOP_RIGHT, Alignment::TOP_LEFT, Alignment::TOP_LEFT]; + yield [Alignment::LEFT, Alignment::TOP_LEFT, Alignment::LEFT]; + yield [Alignment::CENTER, Alignment::TOP_LEFT, Alignment::LEFT]; + yield [Alignment::RIGHT, Alignment::TOP_LEFT, Alignment::LEFT]; + yield [Alignment::BOTTOM_LEFT, Alignment::TOP_LEFT, Alignment::BOTTOM_LEFT]; + yield [Alignment::BOTTOM, Alignment::TOP_LEFT, Alignment::BOTTOM_LEFT]; + yield [Alignment::BOTTOM_RIGHT, Alignment::TOP_LEFT, Alignment::BOTTOM_LEFT]; + + yield [Alignment::TOP_LEFT, Alignment::TOP, Alignment::TOP]; + yield [Alignment::TOP, Alignment::TOP, Alignment::TOP]; + yield [Alignment::TOP_RIGHT, Alignment::TOP, Alignment::TOP]; + yield [Alignment::LEFT, Alignment::TOP, Alignment::CENTER]; + yield [Alignment::CENTER, Alignment::TOP, Alignment::CENTER]; + yield [Alignment::RIGHT, Alignment::TOP, Alignment::CENTER]; + yield [Alignment::BOTTOM_LEFT, Alignment::TOP, Alignment::BOTTOM]; + yield [Alignment::BOTTOM, Alignment::TOP, Alignment::BOTTOM]; + yield [Alignment::BOTTOM_RIGHT, Alignment::TOP, Alignment::BOTTOM]; + + yield [Alignment::TOP_LEFT, Alignment::TOP_RIGHT, Alignment::TOP_RIGHT]; + yield [Alignment::TOP, Alignment::TOP_RIGHT, Alignment::TOP_RIGHT]; + yield [Alignment::TOP_RIGHT, Alignment::TOP_RIGHT, Alignment::TOP_RIGHT]; + yield [Alignment::LEFT, Alignment::TOP_RIGHT, Alignment::RIGHT]; + yield [Alignment::CENTER, Alignment::TOP_RIGHT, Alignment::RIGHT]; + yield [Alignment::RIGHT, Alignment::TOP_RIGHT, Alignment::RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::TOP_RIGHT, Alignment::BOTTOM_RIGHT]; + yield [Alignment::BOTTOM, Alignment::TOP_RIGHT, Alignment::BOTTOM_RIGHT]; + yield [Alignment::BOTTOM_RIGHT, Alignment::TOP_RIGHT, Alignment::BOTTOM_RIGHT]; + + yield [Alignment::TOP_LEFT, Alignment::LEFT, Alignment::TOP_LEFT]; + yield [Alignment::TOP, Alignment::LEFT, Alignment::TOP_LEFT]; + yield [Alignment::TOP_RIGHT, Alignment::LEFT, Alignment::TOP_LEFT]; + yield [Alignment::LEFT, Alignment::LEFT, Alignment::LEFT]; + yield [Alignment::CENTER, Alignment::LEFT, Alignment::LEFT]; + yield [Alignment::RIGHT, Alignment::LEFT, Alignment::LEFT]; + yield [Alignment::BOTTOM_LEFT, Alignment::LEFT, Alignment::BOTTOM_LEFT]; + yield [Alignment::BOTTOM, Alignment::LEFT, Alignment::BOTTOM_LEFT]; + yield [Alignment::BOTTOM_RIGHT, Alignment::LEFT, Alignment::BOTTOM_LEFT]; + + yield [Alignment::TOP_LEFT, Alignment::CENTER, Alignment::TOP]; + yield [Alignment::TOP, Alignment::CENTER, Alignment::TOP]; + yield [Alignment::TOP_RIGHT, Alignment::CENTER, Alignment::TOP]; + yield [Alignment::LEFT, Alignment::CENTER, Alignment::CENTER]; + yield [Alignment::CENTER, Alignment::CENTER, Alignment::CENTER]; + yield [Alignment::RIGHT, Alignment::CENTER, Alignment::CENTER]; + yield [Alignment::BOTTOM_LEFT, Alignment::CENTER, Alignment::BOTTOM]; + yield [Alignment::BOTTOM, Alignment::CENTER, Alignment::BOTTOM]; + yield [Alignment::BOTTOM_RIGHT, Alignment::CENTER, Alignment::BOTTOM]; + + yield [Alignment::TOP_LEFT, Alignment::RIGHT, Alignment::TOP_RIGHT]; + yield [Alignment::TOP, Alignment::RIGHT, Alignment::TOP_RIGHT]; + yield [Alignment::TOP_RIGHT, Alignment::RIGHT, Alignment::TOP_RIGHT]; + yield [Alignment::LEFT, Alignment::RIGHT, Alignment::RIGHT]; + yield [Alignment::CENTER, Alignment::RIGHT, Alignment::RIGHT]; + yield [Alignment::RIGHT, Alignment::RIGHT, Alignment::RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::RIGHT, Alignment::BOTTOM_RIGHT]; + yield [Alignment::BOTTOM, Alignment::RIGHT, Alignment::BOTTOM_RIGHT]; + yield [Alignment::BOTTOM_RIGHT, Alignment::RIGHT, Alignment::BOTTOM_RIGHT]; + + yield [Alignment::TOP_LEFT, Alignment::BOTTOM_LEFT, Alignment::TOP_LEFT]; + yield [Alignment::TOP, Alignment::BOTTOM_LEFT, Alignment::TOP_LEFT]; + yield [Alignment::TOP_RIGHT, Alignment::BOTTOM_LEFT, Alignment::TOP_LEFT]; + yield [Alignment::LEFT, Alignment::BOTTOM_LEFT, Alignment::LEFT]; + yield [Alignment::CENTER, Alignment::BOTTOM_LEFT, Alignment::LEFT]; + yield [Alignment::RIGHT, Alignment::BOTTOM_LEFT, Alignment::LEFT]; + yield [Alignment::BOTTOM_LEFT, Alignment::BOTTOM_LEFT, Alignment::BOTTOM_LEFT]; + yield [Alignment::BOTTOM, Alignment::BOTTOM_LEFT, Alignment::BOTTOM_LEFT]; + yield [Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_LEFT, Alignment::BOTTOM_LEFT]; + + yield [Alignment::TOP_LEFT, Alignment::BOTTOM, Alignment::TOP]; + yield [Alignment::TOP, Alignment::BOTTOM, Alignment::TOP]; + yield [Alignment::TOP_RIGHT, Alignment::BOTTOM, Alignment::TOP]; + yield [Alignment::LEFT, Alignment::BOTTOM, Alignment::CENTER]; + yield [Alignment::CENTER, Alignment::BOTTOM, Alignment::CENTER]; + yield [Alignment::RIGHT, Alignment::BOTTOM, Alignment::CENTER]; + yield [Alignment::BOTTOM_LEFT, Alignment::BOTTOM, Alignment::BOTTOM]; + yield [Alignment::BOTTOM, Alignment::BOTTOM, Alignment::BOTTOM]; + yield [Alignment::BOTTOM_RIGHT, Alignment::BOTTOM, Alignment::BOTTOM]; + + yield [Alignment::TOP_LEFT, Alignment::BOTTOM_RIGHT, Alignment::TOP_RIGHT]; + yield [Alignment::TOP, Alignment::BOTTOM_RIGHT, Alignment::TOP_RIGHT]; + yield [Alignment::TOP_RIGHT, Alignment::BOTTOM_RIGHT, Alignment::TOP_RIGHT]; + yield [Alignment::LEFT, Alignment::BOTTOM_RIGHT, Alignment::RIGHT]; + yield [Alignment::CENTER, Alignment::BOTTOM_RIGHT, Alignment::RIGHT]; + yield [Alignment::RIGHT, Alignment::BOTTOM_RIGHT, Alignment::RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_RIGHT]; + yield [Alignment::BOTTOM, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_RIGHT]; + yield [Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_RIGHT]; + } + + public static function alignVerticallyDataProvider(): Generator + { + yield [Alignment::TOP_LEFT, Alignment::TOP_LEFT, Alignment::TOP_LEFT]; + yield [Alignment::TOP, Alignment::TOP_LEFT, Alignment::TOP]; + yield [Alignment::TOP_RIGHT, Alignment::TOP_LEFT, Alignment::TOP_RIGHT]; + yield [Alignment::LEFT, Alignment::TOP_LEFT, Alignment::TOP_LEFT]; + yield [Alignment::CENTER, Alignment::TOP_LEFT, Alignment::TOP]; + yield [Alignment::RIGHT, Alignment::TOP_LEFT, Alignment::TOP_RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::TOP_LEFT, Alignment::TOP_LEFT]; + yield [Alignment::BOTTOM, Alignment::TOP_LEFT, Alignment::TOP]; + yield [Alignment::BOTTOM_RIGHT, Alignment::TOP_LEFT, Alignment::TOP_RIGHT]; + + yield [Alignment::TOP_LEFT, Alignment::TOP, Alignment::TOP_LEFT]; + yield [Alignment::TOP, Alignment::TOP, Alignment::TOP]; + yield [Alignment::TOP_RIGHT, Alignment::TOP, Alignment::TOP_RIGHT]; + yield [Alignment::LEFT, Alignment::TOP, Alignment::TOP_LEFT]; + yield [Alignment::CENTER, Alignment::TOP, Alignment::TOP]; + yield [Alignment::RIGHT, Alignment::TOP, Alignment::TOP_RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::TOP, Alignment::TOP_LEFT]; + yield [Alignment::BOTTOM, Alignment::TOP, Alignment::TOP]; + yield [Alignment::BOTTOM_RIGHT, Alignment::TOP, Alignment::TOP_RIGHT]; + + yield [Alignment::TOP_LEFT, Alignment::TOP_RIGHT, Alignment::TOP_LEFT]; + yield [Alignment::TOP, Alignment::TOP_RIGHT, Alignment::TOP]; + yield [Alignment::TOP_RIGHT, Alignment::TOP_RIGHT, Alignment::TOP_RIGHT]; + yield [Alignment::LEFT, Alignment::TOP_RIGHT, Alignment::TOP_LEFT]; + yield [Alignment::CENTER, Alignment::TOP_RIGHT, Alignment::TOP]; + yield [Alignment::RIGHT, Alignment::TOP_RIGHT, Alignment::TOP_RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::TOP_RIGHT, Alignment::TOP_LEFT]; + yield [Alignment::BOTTOM, Alignment::TOP_RIGHT, Alignment::TOP]; + yield [Alignment::BOTTOM_RIGHT, Alignment::TOP_RIGHT, Alignment::TOP_RIGHT]; + + yield [Alignment::TOP_LEFT, Alignment::LEFT, Alignment::LEFT]; + yield [Alignment::TOP, Alignment::LEFT, Alignment::CENTER]; + yield [Alignment::TOP_RIGHT, Alignment::LEFT, Alignment::RIGHT]; + yield [Alignment::LEFT, Alignment::LEFT, Alignment::LEFT]; + yield [Alignment::CENTER, Alignment::LEFT, Alignment::CENTER]; + yield [Alignment::RIGHT, Alignment::LEFT, Alignment::RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::LEFT, Alignment::LEFT]; + yield [Alignment::BOTTOM, Alignment::LEFT, Alignment::CENTER]; + yield [Alignment::BOTTOM_RIGHT, Alignment::LEFT, Alignment::RIGHT]; + + yield [Alignment::TOP_LEFT, Alignment::CENTER, Alignment::LEFT]; + yield [Alignment::TOP, Alignment::CENTER, Alignment::CENTER]; + yield [Alignment::TOP_RIGHT, Alignment::CENTER, Alignment::RIGHT]; + yield [Alignment::LEFT, Alignment::CENTER, Alignment::LEFT]; + yield [Alignment::CENTER, Alignment::CENTER, Alignment::CENTER]; + yield [Alignment::RIGHT, Alignment::CENTER, Alignment::RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::CENTER, Alignment::LEFT]; + yield [Alignment::BOTTOM, Alignment::CENTER, Alignment::CENTER]; + yield [Alignment::BOTTOM_RIGHT, Alignment::CENTER, Alignment::RIGHT]; + + yield [Alignment::TOP_LEFT, Alignment::RIGHT, Alignment::LEFT]; + yield [Alignment::TOP, Alignment::RIGHT, Alignment::CENTER]; + yield [Alignment::TOP_RIGHT, Alignment::RIGHT, Alignment::RIGHT]; + yield [Alignment::LEFT, Alignment::RIGHT, Alignment::LEFT]; + yield [Alignment::CENTER, Alignment::RIGHT, Alignment::CENTER]; + yield [Alignment::RIGHT, Alignment::RIGHT, Alignment::RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::RIGHT, Alignment::LEFT]; + yield [Alignment::BOTTOM, Alignment::RIGHT, Alignment::CENTER]; + yield [Alignment::BOTTOM_RIGHT, Alignment::RIGHT, Alignment::RIGHT]; + + yield [Alignment::TOP_LEFT, Alignment::BOTTOM_LEFT, Alignment::BOTTOM_LEFT]; + yield [Alignment::TOP, Alignment::BOTTOM_LEFT, Alignment::BOTTOM]; + yield [Alignment::TOP_RIGHT, Alignment::BOTTOM_LEFT, Alignment::BOTTOM_RIGHT]; + yield [Alignment::LEFT, Alignment::BOTTOM_LEFT, Alignment::BOTTOM_LEFT]; + yield [Alignment::CENTER, Alignment::BOTTOM_LEFT, Alignment::BOTTOM]; + yield [Alignment::RIGHT, Alignment::BOTTOM_LEFT, Alignment::BOTTOM_RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::BOTTOM_LEFT, Alignment::BOTTOM_LEFT]; + yield [Alignment::BOTTOM, Alignment::BOTTOM_LEFT, Alignment::BOTTOM]; + yield [Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_LEFT, Alignment::BOTTOM_RIGHT]; + + yield [Alignment::TOP_LEFT, Alignment::BOTTOM, Alignment::BOTTOM_LEFT]; + yield [Alignment::TOP, Alignment::BOTTOM, Alignment::BOTTOM]; + yield [Alignment::TOP_RIGHT, Alignment::BOTTOM, Alignment::BOTTOM_RIGHT]; + yield [Alignment::LEFT, Alignment::BOTTOM, Alignment::BOTTOM_LEFT]; + yield [Alignment::CENTER, Alignment::BOTTOM, Alignment::BOTTOM]; + yield [Alignment::RIGHT, Alignment::BOTTOM, Alignment::BOTTOM_RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::BOTTOM, Alignment::BOTTOM_LEFT]; + yield [Alignment::BOTTOM, Alignment::BOTTOM, Alignment::BOTTOM]; + yield [Alignment::BOTTOM_RIGHT, Alignment::BOTTOM, Alignment::BOTTOM_RIGHT]; + + yield [Alignment::TOP_LEFT, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_LEFT]; + yield [Alignment::TOP, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM]; + yield [Alignment::TOP_RIGHT, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_RIGHT]; + yield [Alignment::LEFT, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_LEFT]; + yield [Alignment::CENTER, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM]; + yield [Alignment::RIGHT, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_RIGHT]; + yield [Alignment::BOTTOM_LEFT, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_LEFT]; + yield [Alignment::BOTTOM, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM]; + yield [Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_RIGHT, Alignment::BOTTOM_RIGHT]; + } +} diff --git a/tests/Unit/AnimationFactoryTest.php b/tests/Unit/AnimationFactoryTest.php new file mode 100644 index 000000000..7fc806103 --- /dev/null +++ b/tests/Unit/AnimationFactoryTest.php @@ -0,0 +1,106 @@ +add(Resource::create('red.gif')->path(), .2); + $animation->add(Resource::create('green.gif')->path(), .2); + $animation->add(Resource::create('blue.gif')->path(), .2); + }))->image($driver); + + $this->assertEquals(12, $image->width()); + $this->assertEquals(4, $image->height()); + $this->assertEquals(3, $image->count()); + $this->assertEquals(0, $image->loops()); + foreach ($image as $frame) { + $this->assertEquals(.2, $frame->delay()); + } + } + + #[DataProviderExternal(DriverProvider::class, 'drivers')] + public function testAnimationEmptyCallback(DriverInterface $driver): void + { + $image = (new AnimationFactory(12, 4, function (): void { + // + }))->image($driver); + + $this->assertEquals(12, $image->width()); + $this->assertEquals(4, $image->height()); + $this->assertEquals(1, $image->count()); + $this->assertEquals(0, $image->loops()); + $this->assertColor(255, 255, 255, 0, $image->colorAt(0, 0)); + } + + #[DataProviderExternal(DriverProvider::class, 'drivers')] + public function testAnimationEmptyFactory(DriverInterface $driver): void + { + $image = (new AnimationFactory(12, 4))->image($driver); + + $this->assertEquals(12, $image->width()); + $this->assertEquals(4, $image->height()); + $this->assertEquals(1, $image->count()); + $this->assertEquals(0, $image->loops()); + $this->assertColor(255, 255, 255, 0, $image->colorAt(0, 0)); + } + + #[DataProviderExternal(DriverProvider::class, 'drivers')] + public function testBuild(DriverInterface $driver): void + { + $image = AnimationFactory::build(12, 4, fn($animation) => $animation, $driver); + + $this->assertEquals(12, $image->width()); + $this->assertEquals(4, $image->height()); + $this->assertEquals(1, $image->count()); + $this->assertEquals(0, $image->loops()); + $this->assertColor(255, 255, 255, 0, $image->colorAt(0, 0)); + } + + #[DataProviderExternal(DriverProvider::class, 'drivers')] + public function testCallMagicMethodOnFrame(DriverInterface $driver): void + { + $image = (new AnimationFactory(12, 4, function (AnimationFactory $animation): void { + $animation->add(Resource::create('red.gif')->path(), .2)->grayscale(); + }))->image($driver); + + $this->assertEquals(12, $image->width()); + $this->assertEquals(4, $image->height()); + $this->assertEquals(1, $image->count()); + } + + public function testCallMagicMethodWithInvalidMethod(): void + { + $this->expectException(\Error::class); + $factory = new AnimationFactory(12, 4); + $factory->add('test', .2); + $factory->nonExistentMethod(); + } + + #[DataProviderExternal(DriverProvider::class, 'drivers')] + public function testBuildFrameWithColorSource(DriverInterface $driver): void + { + // Use a color string as source — triggers DecoderException catch path in buildFrame + $image = (new AnimationFactory(12, 4, function (AnimationFactory $animation): void { + $animation->add('ff0000', .5); + }))->image($driver); + + $this->assertEquals(12, $image->width()); + $this->assertEquals(4, $image->height()); + $this->assertEquals(1, $image->count()); + } +} diff --git a/tests/Unit/CollectionTest.php b/tests/Unit/CollectionTest.php new file mode 100644 index 000000000..11525afee --- /dev/null +++ b/tests/Unit/CollectionTest.php @@ -0,0 +1,194 @@ +assertInstanceOf(Collection::class, $collection); + + $collection = Collection::create(['foo', 'bar', 'baz']); + $this->assertInstanceOf(Collection::class, $collection); + } + + public function testIterator(): void + { + $collection = new Collection(['foo', 'bar', 'baz']); + foreach ($collection as $key => $item) { + switch ($key) { + case 0: + $this->assertEquals('foo', $item); + break; + + case 1: + $this->assertEquals('bar', $item); + break; + + case 2: + $this->assertEquals('baz', $item); + break; + } + } + } + + public function testCount(): void + { + $collection = new Collection(['foo', 'bar', 'baz']); + $this->assertEquals(3, $collection->count()); + $this->assertEquals(3, count($collection)); + } + + public function testFilter(): void + { + $collection = new Collection(['foo', 'bar', 'baz']); + $this->assertEquals(3, $collection->count()); + $collection = $collection->filter(function ($text): bool { + return substr($text, 0, 1) == 'b'; + }); + $this->assertEquals(2, $collection->count()); + } + + public function testFirstLast(): void + { + $collection = new Collection(['foo', 'bar', 'baz']); + $this->assertEquals('foo', $collection->first()); + $this->assertEquals('baz', $collection->last()); + + $collection = new Collection(); + $this->assertNull($collection->first()); + $this->assertNull($collection->last()); + } + + public function testPush(): void + { + $collection = new Collection(['foo', 'bar', 'baz']); + $this->assertEquals(3, $collection->count()); + $result = $collection->push('test'); + $this->assertEquals(4, $collection->count()); + $this->assertInstanceOf(Collection::class, $result); + } + + public function testToArray(): void + { + $collection = new Collection(['foo', 'bar', 'baz']); + $this->assertEquals(['foo', 'bar', 'baz'], $collection->toArray()); + } + + public function testMap(): void + { + $collection = new Collection(['FOO', 'BAR', 'BAZ']); + $mapped = $collection->map(function ($item) { + return strtolower($item); + }); + $this->assertInstanceOf(Collection::class, $collection); + $this->assertInstanceOf(Collection::class, $mapped); + $this->assertEquals(['FOO', 'BAR', 'BAZ'], $collection->toArray()); + $this->assertEquals(['foo', 'bar', 'baz'], $mapped->toArray()); + } + + public function testSetGet(): void + { + $collection = new Collection(); + $collection->set('foo', 1); + $collection->set('bar', true); + $collection->set('baz', new stdClass()); + $this->assertEquals(1, $collection->get('foo')); + $this->assertTrue($collection->get('bar')); + $this->assertInstanceOf(stdClass::class, $collection->get('baz')); + } + + public function testGet(): void + { + // phpcs:ignore SlevomatCodingStandard.Arrays.DisallowPartiallyKeyed + $collection = new Collection([ + 'first', + 'second', + ['testx' => 'x'], + 'foo' => 'foo_value', + 'bar' => 'bar_value', + 'baz' => [ + 'test1' => '1', + 'test2' => '2', + 'test3' => [ + 'example' => 'value' + ] + ] + ]); + + $this->assertEquals('first', $collection->get(0)); + $this->assertEquals('second', $collection->get(1)); + $this->assertEquals('first', $collection->get('0')); + $this->assertEquals('second', $collection->get('1')); + $this->assertEquals('x', $collection->get('2.testx')); + $this->assertEquals('foo_value', $collection->get('foo')); + $this->assertEquals('bar_value', $collection->get('bar')); + $this->assertEquals('1', $collection->get('baz.test1')); + $this->assertEquals('2', $collection->get('baz.test2')); + $this->assertEquals('value', $collection->get('baz.test3.example')); + $this->assertEquals('value', $collection->get('baz.test3.example', 'default')); + $this->assertEquals('default', $collection->get('baz.test3.no', 'default')); + $this->assertEquals(['example' => 'value'], $collection->get('baz.test3')); + } + + public function testGetAtPosition(): void + { + // phpcs:ignore SlevomatCodingStandard.Arrays.DisallowPartiallyKeyed + $collection = new Collection([1, 2, 'foo' => 'bar']); + $this->assertEquals(1, $collection->at(0)); + $this->assertEquals(2, $collection->at(1)); + $this->assertEquals('bar', $collection->at(2)); + $this->assertNull($collection->at(3)); + $this->assertEquals('default', $collection->at(3, 'default')); + } + + public function testGetAtPositionEmpty(): void + { + $collection = new Collection(); + $this->assertNull($collection->at()); + $this->assertEquals('default', $collection->at(3, 'default')); + } + + public function testEmpty(): void + { + $collection = new Collection([1, 2, 3]); + $this->assertEquals(3, $collection->count()); + $result = $collection->clear(); + $this->assertEquals(0, $collection->count()); + $this->assertEquals(0, $result->count()); + } + + public function testSlice(): void + { + $collection = new Collection(['a', 'b', 'c', 'd', 'e', 'f']); + $this->assertEquals(6, $collection->count()); + $result = $collection->slice(0, 3); + $this->assertEquals(['a', 'b', 'c'], $collection->toArray()); + $this->assertEquals(['a', 'b', 'c'], $result->toArray()); + $this->assertEquals('a', $result->get(0)); + $this->assertEquals('b', $result->get(1)); + $this->assertEquals('c', $result->get(2)); + + $result = $collection->slice(2, 1); + $this->assertEquals(['c'], $collection->toArray()); + $this->assertEquals(['c'], $result->toArray()); + $this->assertEquals('c', $result->get(0)); + } + + public function testSliceOutOfBounds(): void + { + $collection = new Collection(['a', 'b', 'c']); + $result = $collection->slice(6); + $this->assertEquals(0, $result->count()); + $this->assertEquals([], $result->toArray()); + } +} diff --git a/tests/Unit/ColorTest.php b/tests/Unit/ColorTest.php new file mode 100644 index 000000000..ea065ad01 --- /dev/null +++ b/tests/Unit/ColorTest.php @@ -0,0 +1,117 @@ + + */ + #[DataProviderExternal(ColorDataProvider::class, 'rgbString')] + #[DataProviderExternal(ColorDataProvider::class, 'rgbHex')] + #[DataProviderExternal(ColorDataProvider::class, 'rgbNamedColor')] + #[DataProviderExternal(ColorDataProvider::class, 'cmykString')] + #[DataProviderExternal(ColorDataProvider::class, 'hslString')] + #[DataProviderExternal(ColorDataProvider::class, 'hsvString')] + #[DataProviderExternal(ColorDataProvider::class, 'oklabString')] + #[DataProviderExternal(ColorDataProvider::class, 'oklchString')] + public function testParse(mixed $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::parse(...$input)->channels()), + ); + } + + /** + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'rgbArray')] + public function testRgb(mixed $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::rgb(...$input)->channels()), + ); + } + + /** + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'cmykArray')] + public function testCmyk(mixed $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::cmyk(...$input)->channels()), + ); + } + + /** + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'hslArray')] + public function testHsl(mixed $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::hsl(...$input)->channels()), + ); + } + + /** + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'hsvArray')] + public function testHsv(mixed $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::hsv(...$input)->channels()), + ); + } + + /** + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'oklabArray')] + public function testOklab(mixed $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::oklab(...$input)->channels()), + ); + } + + /** + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'oklchArray')] + public function testOklch(mixed $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::oklch(...$input)->channels()), + ); + } + + public function testTransparent(): void + { + $this->assertTrue(Color::transparent()->isClear()); + } +} diff --git a/tests/Unit/Colors/Cmyk/ChannelTest.php b/tests/Unit/Colors/Cmyk/ChannelTest.php new file mode 100644 index 000000000..222c97b5d --- /dev/null +++ b/tests/Unit/Colors/Cmyk/ChannelTest.php @@ -0,0 +1,76 @@ +assertInstanceOf(Cyan::class, $channel); + + $channel = new Cyan(value: 0); + $this->assertInstanceOf(Cyan::class, $channel); + + $channel = Cyan::fromNormalized(0); + $this->assertInstanceOf(Cyan::class, $channel); + } + + public function testConstructorFailInvalidArgument(): void + { + $this->expectException(InvalidArgumentException::class); + new Cyan(200); + } + + public function testConstructorFailInvalidArgumentNormalized(): void + { + $this->expectException(InvalidArgumentException::class); + Cyan::fromNormalized(2); + } + + public function testToString(): void + { + $channel = new Cyan(10); + $this->assertEquals("10", $channel->toString()); + $this->assertEquals("10", (string) $channel); + } + + public function testValue(): void + { + $channel = new Cyan(10); + $this->assertEquals(10, $channel->value()); + } + + public function testNormalize(): void + { + $channel = new Cyan(100); + $this->assertEquals(1, $channel->normalized()); + $channel = new Cyan(0); + $this->assertEquals(0, $channel->normalized()); + $channel = new Cyan(20); + $this->assertEquals(.2, $channel->normalized()); + } + + public function testValidate(): void + { + $this->expectException(InvalidArgumentException::class); + new Cyan(101); + + $this->expectException(InvalidArgumentException::class); + new Cyan(-1); + } +} diff --git a/tests/Unit/Colors/Cmyk/ColorTest.php b/tests/Unit/Colors/Cmyk/ColorTest.php new file mode 100644 index 000000000..9187688d4 --- /dev/null +++ b/tests/Unit/Colors/Cmyk/ColorTest.php @@ -0,0 +1,338 @@ +assertInstanceOf(Color::class, $color); + } + + public function testCreate(): void + { + $color = Color::create(10, 20, 30, 40); + $this->assertInstanceOf(Color::class, $color); + } + + /** + * @param $input array + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'cmykString')] + public function testParse(array $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::parse(...$input)->channels()), + ); + } + + public function testColorspace(): void + { + $color = new Color(0, 0, 0, 0); + $this->assertInstanceOf(Colorspace::class, $color->colorspace()); + } + + public function testChannels(): void + { + $color = new Color(10, 20, 30, 40); + $this->assertIsArray($color->channels()); + $this->assertCount(5, $color->channels()); + } + + public function testChannel(): void + { + $color = new Color(10, 20, 30, 40); + $channel = $color->channel(Cyan::class); + $this->assertInstanceOf(Cyan::class, $channel); + $this->assertEquals(10, $channel->value()); + } + + public function testChannelNotFound(): void + { + $color = new Color(10, 20, 30, 30); + $this->expectException(InvalidArgumentException::class); + $color->channel('none'); + } + + public function testCyanMagentaYellowKey(): void + { + $color = new Color(10, 20, 30, 40); + $this->assertInstanceOf(Cyan::class, $color->cyan()); + $this->assertInstanceOf(Magenta::class, $color->magenta()); + $this->assertInstanceOf(Yellow::class, $color->yellow()); + $this->assertInstanceOf(Key::class, $color->key()); + $this->assertEquals(10, $color->cyan()->value()); + $this->assertEquals(20, $color->magenta()->value()); + $this->assertEquals(30, $color->yellow()->value()); + $this->assertEquals(40, $color->key()->value()); + } + + public function testToHex(): void + { + $color = new Color(0, 73, 100, 0); + $this->assertEquals('ff4400', $color->toHex()); + $this->assertEquals('#ff4400', $color->toHex(true)); + } + + public function testIsGrayscale(): void + { + $color = new Color(0, 73, 100, 0); + $this->assertFalse($color->isGrayscale()); + + $color = new Color(0, 0, 0, 50); + $this->assertTrue($color->isGrayscale()); + } + + public function testNormalize(): void + { + $color = new Color(100, 50, 20, 0); + $this->assertEquals( + [1.0, 0.5, 0.2, 0.0, 1], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + } + + public function testToString(): void + { + $color = new Color(100, 50, 20, 0); + $this->assertEquals('cmyk(100 50 20 0)', (string) $color); + } + + public function testIsTransparent(): void + { + $color = new Color(100, 50, 50, 0); + $this->assertFalse($color->isTransparent()); + } + + public function testIsClear(): void + { + $color = new Color(0, 0, 0, 0); + $this->assertFalse($color->isClear()); + } + + public function testSetTransparency(): void + { + $color = new Color(0, 0, 0, 1); + $result = $color->withTransparency(.2); + $this->assertEquals(255, $color->channel(Alpha::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testDebugInfo(): void + { + $info = (new Color(10, 20, 30, 40))->__debugInfo(); + $this->assertEquals(10, $info['cyan']); + $this->assertEquals(20, $info['magenta']); + $this->assertEquals(30, $info['yellow']); + $this->assertEquals(40, $info['key']); + } + + public function testCreateFailsInvalidArguments(): void + { + $this->expectException(InvalidArgumentException::class); + Color::create(1000, 10, 20, 30); + } + + public function testCreateWithFiveArgs(): void + { + $color = Color::create(10, 20, 30, 40, .5); + $this->assertInstanceOf(Color::class, $color); + $this->assertEquals(10, $color->cyan()->value()); + $this->assertEquals(20, $color->magenta()->value()); + $this->assertEquals(30, $color->yellow()->value()); + $this->assertEquals(40, $color->key()->value()); + $this->assertEquals(128, $color->alpha()->value()); + } + + public function testToStringWithAlpha(): void + { + $color = new Color(100, 50, 20, 0, .5); + $this->assertEquals('cmyk(100 50 20 0 / 0.5)', (string) $color); + } + + public function testIsTransparentTrue(): void + { + $color = new Color(100, 50, 50, 0, .5); + $this->assertTrue($color->isTransparent()); + } + + public function testIsClearTrue(): void + { + $color = new Color(0, 0, 0, 0, 0); + $this->assertTrue($color->isClear()); + } + + public function testConstructorWithChannelObjects(): void + { + $color = new Color(new Cyan(10), new Magenta(20), new Yellow(30), new Key(40), new Alpha(.5)); + $this->assertEquals(10, $color->cyan()->value()); + $this->assertEquals(20, $color->magenta()->value()); + $this->assertEquals(30, $color->yellow()->value()); + $this->assertEquals(40, $color->key()->value()); + $this->assertEquals(128, $color->alpha()->value()); + } + + public function testCloneDeepCopiesChannels(): void + { + $original = new Color(10, 20, 30, 40); + $cloned = clone $original; + + $this->assertEquals(10, $original->cyan()->value()); + $this->assertEquals(10, $cloned->cyan()->value()); + + // Verify they are separate objects (deep clone) + $this->assertNotSame($original->cyan(), $cloned->cyan()); + $this->assertNotSame($original->magenta(), $cloned->magenta()); + $this->assertNotSame($original->yellow(), $cloned->yellow()); + $this->assertNotSame($original->key(), $cloned->key()); + } + + public function testToColorspace(): void + { + $color = new Color(0, 73, 100, 0); + $result = $color->toColorspace(RgbColorspace::class); + $this->assertInstanceOf(RgbColor::class, $result); + + /** @var RgbColor $result */ + $this->assertEquals(255, $result->red()->value()); + $this->assertEquals(68, $result->green()->value()); + $this->assertEquals(0, $result->blue()->value()); + } + + public function testToColorspaceWithObject(): void + { + $color = new Color(0, 73, 100, 0); + $result = $color->toColorspace(new RgbColorspace()); + $this->assertInstanceOf(RgbColor::class, $result); + } + + public function testToColorspaceFailsInvalidClass(): void + { + $color = new Color(0, 0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->toColorspace('NonExistentClass'); + } + + public function testToColorspaceFailsNonColorspaceClass(): void + { + $color = new Color(0, 0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->toColorspace(\stdClass::class); + } + + public function testWithBrightnessPositive(): void + { + $color = new Color(0, 100, 100, 0); + $result = $color->withBrightness(20); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + } + + public function testWithBrightnessInvalidLevelAbove(): void + { + $color = new Color(0, 0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->withBrightness(101); + } + + public function testWithBrightnessInvalidLevelBelow(): void + { + $color = new Color(0, 0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->withBrightness(-101); + } + + public function testWithBrightnessNegative(): void + { + $color = new Color(0, 100, 100, 0); + $result = $color->withBrightness(-20); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + } + + public function testWithSaturationPositive(): void + { + $color = new Color(50, 50, 50, 0); + $result = $color->withSaturation(20); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + } + + public function testWithSaturationInvalidLevelAbove(): void + { + $color = new Color(0, 0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->withSaturation(101); + } + + public function testWithSaturationInvalidLevelBelow(): void + { + $color = new Color(0, 0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->withSaturation(-101); + } + + public function testWithSaturationNegative(): void + { + $color = new Color(0, 100, 100, 0); + $result = $color->withSaturation(-50); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + } + + public function testInvert(): void + { + $color = new Color(0, 100, 100, 0); + $result = $color->withInversion(); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + } + + public function testWithBrightnessPreservesAlpha(): void + { + $color = new Color(0, 100, 100, 0, .5); + $result = $color->withBrightness(20); + $this->assertEquals( + $color->channel(Alpha::class)->value(), + $result->channel(Alpha::class)->value(), + ); + } + + public function testInvertPreservesAlpha(): void + { + $color = new Color(0, 100, 100, 0, .5); + $result = $color->withInversion(); + $this->assertEquals( + $color->channel(Alpha::class)->value(), + $result->channel(Alpha::class)->value(), + ); + } +} diff --git a/tests/Unit/Colors/Cmyk/ColorspaceTest.php b/tests/Unit/Colors/Cmyk/ColorspaceTest.php new file mode 100644 index 000000000..f266a014f --- /dev/null +++ b/tests/Unit/Colors/Cmyk/ColorspaceTest.php @@ -0,0 +1,185 @@ +colorFromNormalized([0, 1, 0, 1]); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertEquals(0, $result->channel(Cyan::class)->value()); + $this->assertEquals(100, $result->channel(Magenta::class)->value()); + $this->assertEquals(0, $result->channel(Yellow::class)->value()); + $this->assertEquals(100, $result->channel(Key::class)->value()); + $this->assertEquals(255, $result->channel(Alpha::class)->value()); + + $result = $colorspace->colorFromNormalized([0, 1, 0, 1, .2]); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertEquals(0, $result->channel(Cyan::class)->value()); + $this->assertEquals(100, $result->channel(Magenta::class)->value()); + $this->assertEquals(0, $result->channel(Yellow::class)->value()); + $this->assertEquals(100, $result->channel(Key::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testImportRgbColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new RgbColor(255, 0, 255)); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertEquals(0, $result->channel(Cyan::class)->value()); + $this->assertEquals(100, $result->channel(Magenta::class)->value()); + $this->assertEquals(0, $result->channel(Yellow::class)->value()); + $this->assertEquals(0, $result->channel(Key::class)->value()); + + $result = $colorspace->importColor(new RgbColor(127, 127, 127)); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertEquals(0, $result->channel(Cyan::class)->value()); + $this->assertEquals(0, $result->channel(Magenta::class)->value()); + $this->assertEquals(0, $result->channel(Yellow::class)->value()); + $this->assertEquals(50, $result->channel(Key::class)->value()); + + $result = $colorspace->importColor(new RgbColor(127, 127, 127, 0.3333333333)); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertEquals(0, $result->channel(Cyan::class)->value()); + $this->assertEquals(0, $result->channel(Magenta::class)->value()); + $this->assertEquals(0, $result->channel(Yellow::class)->value()); + $this->assertEquals(50, $result->channel(Key::class)->value()); + $this->assertEquals(85, $result->channel(Alpha::class)->value()); + } + + public function testImportHsvColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new HsvColor(0, 0, 50)); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertEquals(0, $result->channel(Cyan::class)->value()); + $this->assertEquals(0, $result->channel(Magenta::class)->value()); + $this->assertEquals(0, $result->channel(Yellow::class)->value()); + $this->assertEquals(50, $result->channel(Key::class)->value()); + } + + public function testImportHslColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new HslColor(300, 100, 50)); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertEquals(0, $result->channel(Cyan::class)->value()); + $this->assertEquals(100, $result->channel(Magenta::class)->value()); + $this->assertEquals(0, $result->channel(Yellow::class)->value()); + $this->assertEquals(0, $result->channel(Key::class)->value()); + + $result = $colorspace->importColor(new HslColor(0, 0, 50)); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertEquals(0, $result->channel(Cyan::class)->value()); + $this->assertEquals(0, $result->channel(Magenta::class)->value()); + $this->assertEquals(0, $result->channel(Yellow::class)->value()); + $this->assertEquals(50, $result->channel(Key::class)->value()); + } + + public function testImportNamedColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(NamedColor::BLACK); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertEquals(0, $result->channel(Cyan::class)->value()); + $this->assertEquals(0, $result->channel(Magenta::class)->value()); + $this->assertEquals(0, $result->channel(Yellow::class)->value()); + $this->assertEquals(100, $result->channel(Key::class)->value()); + } + + public function testImportOklabColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new OklabColor(0.68, 0.17, 0.14)); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertEquals(0, $result->channel(Cyan::class)->value()); + $this->assertEqualsWithDelta(67, $result->channel(Magenta::class)->value(), 2); + $this->assertEqualsWithDelta(100, $result->channel(Yellow::class)->value(), 1); + $this->assertEquals(0, $result->channel(Key::class)->value()); + } + + public function testImportOklchColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new OklchColor(0.68, 0.22, 38.8)); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertEquals(0, $result->channel(Cyan::class)->value()); + $this->assertEqualsWithDelta(67, $result->channel(Magenta::class)->value(), 2); + $this->assertEqualsWithDelta(100, $result->channel(Yellow::class)->value(), 1); + $this->assertEquals(0, $result->channel(Key::class)->value()); + } + + public function testImportCmykColorPassthrough(): void + { + $colorspace = new Colorspace(); + + $color = new CmykColor(10, 20, 30, 40); + $result = $colorspace->importColor($color); + $this->assertInstanceOf(CmykColor::class, $result); + $this->assertSame($color, $result); + } + + public function testColorFromNormalizedInvalidChannelCount(): void + { + $colorspace = new Colorspace(); + $this->expectException(InvalidArgumentException::class); + $colorspace->colorFromNormalized([0.5, 0.5]); + } + + public function testColorFromNormalizedWithNullValue(): void + { + $colorspace = new Colorspace(); + $this->expectException(InvalidArgumentException::class); + $colorspace->colorFromNormalized([0.5, null, 0.5, 0.5]); + } + + public function testImportUnsupportedColor(): void + { + $colorspace = new Colorspace(); + $color = Mockery::mock(\Intervention\Image\Interfaces\ColorInterface::class); + $this->expectException(ColorException::class); + $colorspace->importColor($color); + } + + public function testChannels(): void + { + $channels = Colorspace::channels(); + $this->assertIsArray($channels); + $this->assertCount(5, $channels); + $this->assertEquals(Cyan::class, $channels[0]); + $this->assertEquals(Magenta::class, $channels[1]); + $this->assertEquals(Yellow::class, $channels[2]); + $this->assertEquals(Key::class, $channels[3]); + $this->assertEquals(Alpha::class, $channels[4]); + } +} diff --git a/tests/Unit/Colors/Cmyk/Decoders/StringColorDecoderTest.php b/tests/Unit/Colors/Cmyk/Decoders/StringColorDecoderTest.php new file mode 100644 index 000000000..d2efc19c8 --- /dev/null +++ b/tests/Unit/Colors/Cmyk/Decoders/StringColorDecoderTest.php @@ -0,0 +1,51 @@ + + */ + #[DataProviderExternal(ColorDataProvider::class, 'cmykString')] + public function testDecode(mixed $input, array $channelValues): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode($input[0]); + $this->assertEquals( + $channelValues, + array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $result->channels(), + ), + ); + } + + public function testSupportsString(): void + { + $decoder = new StringColorDecoder(); + $this->assertTrue($decoder->supports('cmyk(0, 0, 0, 0)')); + $this->assertTrue($decoder->supports('CMYK(100, 50, 25, 10)')); + $this->assertFalse($decoder->supports('rgb(0, 0, 0)')); + $this->assertFalse($decoder->supports(123)); + $this->assertFalse($decoder->supports(null)); + } + + public function testDecodeInvalid(): void + { + $decoder = new StringColorDecoder(); + $this->expectException(InvalidArgumentException::class); + $decoder->decode('cmyk(invalid)'); + } +} diff --git a/tests/Unit/Colors/Hsl/ChannelTest.php b/tests/Unit/Colors/Hsl/ChannelTest.php new file mode 100644 index 000000000..90083f15d --- /dev/null +++ b/tests/Unit/Colors/Hsl/ChannelTest.php @@ -0,0 +1,88 @@ +assertInstanceOf(Hue::class, $channel); + + $channel = new Hue(value: 0); + $this->assertInstanceOf(Hue::class, $channel); + + $channel = Hue::fromNormalized(0); + $this->assertInstanceOf(Hue::class, $channel); + } + + public function testConstructorFailInvalidArgument(): void + { + $this->expectException(InvalidArgumentException::class); + new Hue(400); + } + + public function testConstructorFailInvalidArgumentNormalized(): void + { + $this->expectException(InvalidArgumentException::class); + Hue::fromNormalized(2); + } + + public function testToString(): void + { + $channel = new Hue(10); + $this->assertEquals("10", $channel->toString()); + $this->assertEquals("10", (string) $channel); + } + + public function testValue(): void + { + $channel = new Hue(10); + $this->assertEquals(10, $channel->value()); + } + + public function testNormalize(): void + { + $channel = new Hue(360); + $this->assertEquals(1, $channel->normalized()); + $channel = new Hue(180); + $this->assertEquals(0.5, $channel->normalized()); + $channel = new Hue(0); + $this->assertEquals(0, $channel->normalized()); + $channel = new Luminance(90); + $this->assertEquals(.9, $channel->normalized()); + } + + public function testValidate(): void + { + $this->expectException(InvalidArgumentException::class); + new Hue(361); + + $this->expectException(InvalidArgumentException::class); + new Hue(-1); + + $this->expectException(InvalidArgumentException::class); + new Saturation(101); + + $this->expectException(InvalidArgumentException::class); + new Saturation(-1); + + $this->expectException(InvalidArgumentException::class); + new Luminance(101); + + $this->expectException(InvalidArgumentException::class); + new Luminance(-1); + } +} diff --git a/tests/Unit/Colors/Hsl/Channels/SaturationTest.php b/tests/Unit/Colors/Hsl/Channels/SaturationTest.php new file mode 100644 index 000000000..8ff4564c2 --- /dev/null +++ b/tests/Unit/Colors/Hsl/Channels/SaturationTest.php @@ -0,0 +1,20 @@ +assertEquals(0, $saturation->min()); + $this->assertEquals(100, $saturation->max()); + } +} diff --git a/tests/Unit/Colors/Hsl/ColorTest.php b/tests/Unit/Colors/Hsl/ColorTest.php new file mode 100644 index 000000000..247a8ab2f --- /dev/null +++ b/tests/Unit/Colors/Hsl/ColorTest.php @@ -0,0 +1,354 @@ +assertInstanceOf(Color::class, $color); + } + + public function testCreate(): void + { + $color = Color::create(10, 20, 30); + $this->assertInstanceOf(Color::class, $color); + } + + /** + * @param $input array + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'hslString')] + public function testParse(array $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::parse(...$input)->channels()), + ); + } + + public function testColorspace(): void + { + $color = new Color(0, 0, 0); + $this->assertInstanceOf(Colorspace::class, $color->colorspace()); + } + + public function testChannels(): void + { + $color = new Color(10, 20, 30); + $this->assertIsArray($color->channels()); + $this->assertCount(4, $color->channels()); + } + + public function testChannel(): void + { + $color = new Color(10, 20, 30); + $channel = $color->channel(Hue::class); + $this->assertInstanceOf(Hue::class, $channel); + $this->assertEquals(10, $channel->value()); + } + + public function testChannelNotFound(): void + { + $color = new Color(10, 20, 30); + $this->expectException(InvalidArgumentException::class); + $color->channel('none'); + } + + public function testHueSaturationLuminanceKey(): void + { + $color = new Color(10, 20, 30); + $this->assertInstanceOf(Hue::class, $color->hue()); + $this->assertInstanceOf(Saturation::class, $color->saturation()); + $this->assertInstanceOf(Luminance::class, $color->luminance()); + $this->assertInstanceOf(Alpha::class, $color->alpha()); + $this->assertEquals(10, $color->hue()->value()); + $this->assertEquals(20, $color->saturation()->value()); + $this->assertEquals(30, $color->luminance()->value()); + $this->assertEquals(255, $color->alpha()->value()); + } + + public function testToHex(): void + { + $color = new Color(16, 100, 50); + $this->assertEquals('ff4400', $color->toHex()); + } + + public function testNormalize(): void + { + $color = new Color(180, 50, 25); + $this->assertEquals( + [.5, 0.5, 0.25, 1], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + } + + public function testToString(): void + { + $color = new Color(100, 50, 20); + $this->assertEquals('hsl(100 50% 20%)', (string) $color); + + $color = new Color(100, 50, 20, .2); + $this->assertEquals('hsl(100 50% 20% / 0.2)', (string) $color); + } + + public function testIsGrayscale(): void + { + $color = new Color(0, 1, 0); + $this->assertFalse($color->isGrayscale()); + + $color = new Color(1, 0, 0); + $this->assertTrue($color->isGrayscale()); + + $color = new Color(0, 0, 1); + $this->assertTrue($color->isGrayscale()); + } + + public function testIsTransparent(): void + { + $color = new Color(0, 1, 0); + $this->assertFalse($color->isTransparent()); + } + + public function testIsClear(): void + { + $color = new Color(0, 1, 0); + $this->assertFalse($color->isClear()); + } + + public function testSetTransparency(): void + { + $color = new Color(0, 0, 0, 1); + $result = $color->withTransparency(.2); + $this->assertEquals(255, $color->channel(Alpha::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testDebugInfo(): void + { + $info = (new Color(10, 20, 30))->__debugInfo(); + $this->assertEquals('10', $info['hue']); + $this->assertEquals('20', $info['saturation']); + $this->assertEquals('30', $info['luminance']); + $this->assertEquals('1', $info['alpha']); + } + + public function testCreateFailsInvalidArguments(): void + { + $this->expectException(InvalidArgumentException::class); + Color::create(1000, 2000, 1000); + } + + public function testCreateWithFourArgs(): void + { + $color = Color::create(180, 50, 50, .5); + $this->assertInstanceOf(Color::class, $color); + $this->assertEquals(180, $color->hue()->value()); + $this->assertEquals(50, $color->saturation()->value()); + $this->assertEquals(50, $color->luminance()->value()); + $this->assertEquals(128, $color->alpha()->value()); + } + + public function testIsTransparentTrue(): void + { + $color = new Color(0, 50, 50, .5); + $this->assertTrue($color->isTransparent()); + } + + public function testIsClearTrue(): void + { + $color = new Color(0, 50, 50, 0); + $this->assertTrue($color->isClear()); + } + + public function testConstructorWithChannelObjects(): void + { + $color = new Color(new Hue(180), new Saturation(50), new Luminance(50), new Alpha(.5)); + $this->assertEquals(180, $color->hue()->value()); + $this->assertEquals(50, $color->saturation()->value()); + $this->assertEquals(50, $color->luminance()->value()); + $this->assertEquals(128, $color->alpha()->value()); + } + + public function testCloneDeepCopiesChannels(): void + { + $original = new Color(180, 50, 50); + $cloned = clone $original; + + $this->assertEquals(180, $original->hue()->value()); + $this->assertEquals(180, $cloned->hue()->value()); + + // Verify they are separate objects (deep clone) + $this->assertNotSame($original->hue(), $cloned->hue()); + $this->assertNotSame($original->saturation(), $cloned->saturation()); + $this->assertNotSame($original->luminance(), $cloned->luminance()); + } + + public function testToColorspace(): void + { + $color = new Color(16, 100, 50); + $result = $color->toColorspace(RgbColorspace::class); + $this->assertInstanceOf(RgbColor::class, $result); + } + + public function testToColorspaceWithObject(): void + { + $color = new Color(16, 100, 50); + $result = $color->toColorspace(new RgbColorspace()); + $this->assertInstanceOf(RgbColor::class, $result); + } + + public function testToColorspaceFailsInvalidClass(): void + { + $color = new Color(0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->toColorspace('NonExistentClass'); + } + + public function testToColorspaceFailsNonColorspaceClass(): void + { + $color = new Color(0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->toColorspace(\stdClass::class); + } + + public function testWithBrightnessPositive(): void + { + $color = new Color(0, 100, 50); + $result = $color->withBrightness(20); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + + // Luminance should increase, hue and saturation should stay the same + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertGreaterThan(50, $result->channel(Luminance::class)->value()); + } + + public function testWithBrightnessZero(): void + { + $color = new Color(180, 50, 50); + $result = $color->withBrightness(0); + $this->assertInstanceOf(Color::class, $result); + $this->assertEquals(180, $result->channel(Hue::class)->value()); + $this->assertEquals(50, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Luminance::class)->value()); + } + + public function testWithBrightnessPreservesAlpha(): void + { + $color = new Color(0, 100, 50, .5); + $result = $color->withBrightness(20); + $this->assertEquals($color->channel(Alpha::class)->value(), $result->channel(Alpha::class)->value()); + } + + public function testWithBrightnessInvalidLevelAbove(): void + { + $color = new Color(0, 100, 50); + $this->expectException(InvalidArgumentException::class); + $color->withBrightness(101); + } + + public function testWithBrightnessInvalidLevelBelow(): void + { + $color = new Color(0, 100, 50); + $this->expectException(InvalidArgumentException::class); + $color->withBrightness(-101); + } + + public function testWithBrightnessNegative(): void + { + $color = new Color(0, 100, 50); + $result = $color->withBrightness(-20); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + + // Luminance should decrease + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertLessThan(50, $result->channel(Luminance::class)->value()); + } + + public function testWithSaturationPositive(): void + { + $color = new Color(0, 50, 50); + $result = $color->withSaturation(20); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + + // Saturation should increase + $this->assertGreaterThan(50, $result->channel(Saturation::class)->value()); + } + + public function testWithSaturationInvalidLevelAbove(): void + { + $color = new Color(0, 100, 50); + $this->expectException(InvalidArgumentException::class); + $color->withSaturation(101); + } + + public function testWithSaturationInvalidLevelBelow(): void + { + $color = new Color(0, 100, 50); + $this->expectException(InvalidArgumentException::class); + $color->withSaturation(-101); + } + + public function testWithSaturationNegative(): void + { + $color = new Color(0, 100, 50); + $result = $color->withSaturation(-20); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + + // Saturation should decrease + $this->assertLessThan(100, $result->channel(Saturation::class)->value()); + } + + public function testWithSaturationFullNegative(): void + { + $color = new Color(0, 100, 50); + $result = $color->withSaturation(-100); + $this->assertInstanceOf(Color::class, $result); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + } + + public function testInvert(): void + { + $color = new Color(0, 100, 50); + $result = $color->withInversion(); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + } + + public function testInvertPreservesAlpha(): void + { + $color = new Color(0, 100, 50, .5); + $result = $color->withInversion(); + $this->assertEquals($color->channel(Alpha::class)->value(), $result->channel(Alpha::class)->value()); + } +} diff --git a/tests/Unit/Colors/Hsl/ColorspaceTest.php b/tests/Unit/Colors/Hsl/ColorspaceTest.php new file mode 100644 index 000000000..96ef527a5 --- /dev/null +++ b/tests/Unit/Colors/Hsl/ColorspaceTest.php @@ -0,0 +1,246 @@ +colorFromNormalized([1, 0, 1]); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(360, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Luminance::class)->value()); + $this->assertEquals(255, $result->channel(Alpha::class)->value()); + + $colorspace = new Colorspace(); + $result = $colorspace->colorFromNormalized([1, 0, 1, .2]); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(360, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Luminance::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testImportRgbColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new RgbColor(255, 0, 255)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(300, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Luminance::class)->value()); + + $result = $colorspace->importColor(new RgbColor(127, 127, 127)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Luminance::class)->value()); + + $result = $colorspace->importColor(new RgbColor(255, 0, 0, 0.3333333333)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Luminance::class)->value()); + } + + public function testImportCmykColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new CmykColor(0, 100, 0, 0)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(300, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Luminance::class)->value()); + + $result = $colorspace->importColor(new CmykColor(0, 0, 0, 50)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Luminance::class)->value()); + } + + public function testImportHsvColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new HsvColor(300, 100, 100)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(300, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Luminance::class)->value()); + + $result = $colorspace->importColor(new HsvColor(0, 0, 50)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Luminance::class)->value()); + } + + public function testImportNamedColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(NamedColor::WHITE); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Luminance::class)->value()); + } + + public function testImportOklabColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new OklabColor(0.68, 0.17, 0.14)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEqualsWithDelta(19, $result->channel(Hue::class)->value(), 2); + $this->assertEqualsWithDelta(100, $result->channel(Saturation::class)->value(), 1); + $this->assertEqualsWithDelta(50, $result->channel(Luminance::class)->value(), 2); + } + + public function testImportOklchColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new OklchColor(0.68, 0.22, 38.8)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEqualsWithDelta(19, $result->channel(Hue::class)->value(), 2); + $this->assertEqualsWithDelta(100, $result->channel(Saturation::class)->value(), 1); + $this->assertEqualsWithDelta(50, $result->channel(Luminance::class)->value(), 2); + } + + public function testImportHslColorPassthrough(): void + { + $colorspace = new Colorspace(); + + $color = new HslColor(200, 50, 60); + $result = $colorspace->importColor($color); + $this->assertSame($color, $result); + } + + public function testColorFromNormalizedInvalidChannelCount(): void + { + $colorspace = new Colorspace(); + $this->expectException(InvalidArgumentException::class); + $colorspace->colorFromNormalized([0.5, 0.5]); + } + + public function testColorFromNormalizedWithNullValue(): void + { + $colorspace = new Colorspace(); + $this->expectException(InvalidArgumentException::class); + $colorspace->colorFromNormalized([0.5, null, 0.5]); + } + + public function testImportUnsupportedColor(): void + { + $colorspace = new Colorspace(); + $color = Mockery::mock(\Intervention\Image\Interfaces\ColorInterface::class); + $this->expectException(ColorException::class); + $colorspace->importColor($color); + } + + public function testChannels(): void + { + $channels = Colorspace::channels(); + $this->assertIsArray($channels); + $this->assertCount(4, $channels); + $this->assertEquals(Hue::class, $channels[0]); + $this->assertEquals(Saturation::class, $channels[1]); + $this->assertEquals(Luminance::class, $channels[2]); + $this->assertEquals(Alpha::class, $channels[3]); + } + + public function testImportHsvColorLuminanceZero(): void + { + $colorspace = new Colorspace(); + // HSV with saturation=100, value=0 -> luminance=0 + $result = $colorspace->importColor(new HsvColor(0, 100, 0)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(0, $result->channel(Luminance::class)->value()); + } + + public function testImportHsvColorLuminanceOne(): void + { + $colorspace = new Colorspace(); + // HSV with saturation=0, value=100 -> luminance=100 (=1 normalized) + $result = $colorspace->importColor(new HsvColor(0, 0, 100)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(100, $result->channel(Luminance::class)->value()); + } + + public function testImportRgbColorGreenDominant(): void + { + $colorspace = new Colorspace(); + + // RGB(0, 255, 0) => max=G => hits ($max == $g) branch in hue calculation + $result = $colorspace->importColor(new RgbColor(0, 255, 0)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(120, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Luminance::class)->value()); + } + + public function testImportRgbColorBlueDominant(): void + { + $colorspace = new Colorspace(); + + // RGB(0, 0, 255) => max=B => hits ($max == $b) branch in hue calculation + $result = $colorspace->importColor(new RgbColor(0, 0, 255)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(240, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Luminance::class)->value()); + } + + public function testImportHsvColorLuminanceBelowHalf(): void + { + $colorspace = new Colorspace(); + + // HSV(120, 100, 40) => s=1.0, v=0.4 => luminance=(2-1)*0.4/2=0.2 + // hits $luminance < .5 branch: saturation = s*v/(luminance*2) = 1*0.4/(0.2*2) = 1.0 + $result = $colorspace->importColor(new HsvColor(120, 100, 40)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(120, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(20, $result->channel(Luminance::class)->value()); + } + + public function testImportHsvColorLuminanceAboveHalf(): void + { + $colorspace = new Colorspace(); + + // HSV(120, 50, 80) => s=0.5, v=0.8 => luminance=(2-0.5)*0.8/2=0.6 + // hits default branch: saturation = s*v/(2-luminance*2) = 0.5*0.8/(2-1.2) = 0.5 + $result = $colorspace->importColor(new HsvColor(120, 50, 80)); + $this->assertInstanceOf(HslColor::class, $result); + $this->assertEquals(120, $result->channel(Hue::class)->value()); + $this->assertEquals(50, $result->channel(Saturation::class)->value()); + $this->assertEquals(60, $result->channel(Luminance::class)->value()); + } +} diff --git a/tests/Unit/Colors/Hsl/Decoders/StringColorDecoderTest.php b/tests/Unit/Colors/Hsl/Decoders/StringColorDecoderTest.php new file mode 100644 index 000000000..b7af6b99b --- /dev/null +++ b/tests/Unit/Colors/Hsl/Decoders/StringColorDecoderTest.php @@ -0,0 +1,52 @@ + + */ + #[DataProviderExternal(ColorDataProvider::class, 'hslString')] + public function testDecode(mixed $input, array $channelValues): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode($input[0]); + $this->assertEquals( + $channelValues, + array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $result->channels(), + ), + ); + } + + public function testSupportsString(): void + { + $decoder = new StringColorDecoder(); + $this->assertTrue($decoder->supports('hsl(0, 0%, 0%)')); + $this->assertTrue($decoder->supports('HSL(360, 100%, 100%)')); + $this->assertTrue($decoder->supports('hsla(0, 0%, 0%, 1)')); + $this->assertFalse($decoder->supports('rgb(0, 0, 0)')); + $this->assertFalse($decoder->supports(123)); + $this->assertFalse($decoder->supports(null)); + } + + public function testDecodeInvalid(): void + { + $decoder = new StringColorDecoder(); + $this->expectException(InvalidArgumentException::class); + $decoder->decode('hsl(invalid)'); + } +} diff --git a/tests/Unit/Colors/Hsv/ChannelTest.php b/tests/Unit/Colors/Hsv/ChannelTest.php new file mode 100644 index 000000000..ac36ec623 --- /dev/null +++ b/tests/Unit/Colors/Hsv/ChannelTest.php @@ -0,0 +1,88 @@ +assertInstanceOf(Hue::class, $channel); + + $channel = new Hue(value: 0); + $this->assertInstanceOf(Hue::class, $channel); + + $channel = Hue::fromNormalized(0); + $this->assertInstanceOf(Hue::class, $channel); + } + + public function testConstructorFailInvalidArgument(): void + { + $this->expectException(InvalidArgumentException::class); + new Hue(400); + } + + public function testConstructorFailInvalidArgumentNormalized(): void + { + $this->expectException(InvalidArgumentException::class); + Hue::fromNormalized(2); + } + + public function testToString(): void + { + $channel = new Hue(10); + $this->assertEquals("10", $channel->toString()); + $this->assertEquals("10", (string) $channel); + } + + public function testValue(): void + { + $channel = new Hue(10); + $this->assertEquals(10, $channel->value()); + } + + public function testNormalize(): void + { + $channel = new Hue(360); + $this->assertEquals(1, $channel->normalized()); + $channel = new Hue(180); + $this->assertEquals(0.5, $channel->normalized()); + $channel = new Hue(0); + $this->assertEquals(0, $channel->normalized()); + $channel = new Hue(90); + $this->assertEquals(.25, $channel->normalized()); + } + + public function testValidate(): void + { + $this->expectException(InvalidArgumentException::class); + new Hue(361); + + $this->expectException(InvalidArgumentException::class); + new Hue(-1); + + $this->expectException(InvalidArgumentException::class); + new Saturation(101); + + $this->expectException(InvalidArgumentException::class); + new Saturation(-1); + + $this->expectException(InvalidArgumentException::class); + new Value(101); + + $this->expectException(InvalidArgumentException::class); + new Value(-1); + } +} diff --git a/tests/Unit/Colors/Hsv/Channels/SaturationTest.php b/tests/Unit/Colors/Hsv/Channels/SaturationTest.php new file mode 100644 index 000000000..a1ff904f4 --- /dev/null +++ b/tests/Unit/Colors/Hsv/Channels/SaturationTest.php @@ -0,0 +1,20 @@ +assertEquals(0, $saturation->min()); + $this->assertEquals(100, $saturation->max()); + } +} diff --git a/tests/Unit/Colors/Hsv/Channels/ValueTest.php b/tests/Unit/Colors/Hsv/Channels/ValueTest.php new file mode 100644 index 000000000..2278aabc5 --- /dev/null +++ b/tests/Unit/Colors/Hsv/Channels/ValueTest.php @@ -0,0 +1,20 @@ +assertEquals(0, $saturation->min()); + $this->assertEquals(100, $saturation->max()); + } +} diff --git a/tests/Unit/Colors/Hsv/ColorTest.php b/tests/Unit/Colors/Hsv/ColorTest.php new file mode 100644 index 000000000..0bdadef69 --- /dev/null +++ b/tests/Unit/Colors/Hsv/ColorTest.php @@ -0,0 +1,254 @@ +assertInstanceOf(Color::class, $color); + } + + public function testCreate(): void + { + $color = Color::create(10, 20, 30); + $this->assertInstanceOf(Color::class, $color); + } + + /** + * @param $input array + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'hsvString')] + public function testParse(array $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::parse(...$input)->channels()), + ); + } + + public function testColorspace(): void + { + $color = new Color(0, 0, 0); + $this->assertInstanceOf(Colorspace::class, $color->colorspace()); + } + + public function testChannels(): void + { + $color = new Color(10, 20, 30); + $this->assertIsArray($color->channels()); + $this->assertCount(4, $color->channels()); + } + + public function testChannel(): void + { + $color = new Color(10, 20, 30); + $channel = $color->channel(Hue::class); + $this->assertInstanceOf(Hue::class, $channel); + $this->assertEquals(10, $channel->value()); + } + + public function testChannelNotFound(): void + { + $color = new Color(10, 20, 30); + $this->expectException(InvalidArgumentException::class); + $color->channel('none'); + } + + public function testHueSaturationValueKey(): void + { + $color = new Color(10, 20, 30); + $this->assertInstanceOf(Hue::class, $color->hue()); + $this->assertInstanceOf(Saturation::class, $color->saturation()); + $this->assertInstanceOf(Value::class, $color->value()); + $this->assertEquals(10, $color->hue()->value()); + $this->assertEquals(20, $color->saturation()->value()); + $this->assertEquals(30, $color->value()->value()); + } + + public function testToHex(): void + { + $color = new Color(16, 100, 100); + $this->assertEquals('ff4400', $color->toHex()); + } + + public function testNormalize(): void + { + $color = new Color(180, 50, 25); + $this->assertEquals( + [.5, 0.5, 0.25, 1], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + + $color = new Color(180, 50, 25, .2); + $this->assertEquals( + [.5, 0.5, 0.25, .2], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + } + + public function testToString(): void + { + $color = new Color(100, 50, 20); + $this->assertEquals('hsv(100 50% 20%)', (string) $color); + + $color = new Color(100, 50, 20, 1); + $this->assertEquals('hsv(100 50% 20%)', (string) $color); + + $color = new Color(100, 50, 20, .2); + $this->assertEquals('hsv(100 50% 20% / 0.2)', (string) $color); + } + + public function testIsGrayscale(): void + { + $color = new Color(0, 1, 0); + $this->assertFalse($color->isGrayscale()); + + $color = new Color(1, 0, 0); + $this->assertTrue($color->isGrayscale()); + + $color = new Color(0, 0, 1); + $this->assertTrue($color->isGrayscale()); + } + + public function testIsTransparent(): void + { + $color = new Color(1, 0, 0); + $this->assertFalse($color->isTransparent()); + + $color = new Color(1, 0, 0, 1); + $this->assertFalse($color->isTransparent()); + + $color = new Color(1, 0, 0, .2); + $this->assertTrue($color->isTransparent()); + + $color = new Color(1, 0, 0, 0); + $this->assertTrue($color->isTransparent()); + } + + public function testIsClear(): void + { + $color = new Color(0, 1, 0); + $this->assertFalse($color->isClear()); + + $color = new Color(0, 1, 0, 1); + $this->assertFalse($color->isClear()); + + $color = new Color(0, 1, 0, .2); + $this->assertFalse($color->isClear()); + + $color = new Color(0, 1, 0, 0); + $this->assertTrue($color->isClear()); + } + + public function testSetTransparency(): void + { + $color = new Color(0, 0, 0, 1); + $result = $color->withTransparency(.2); + $this->assertEquals(255, $color->channel(Alpha::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testDebugInfo(): void + { + $info = (new Color(10, 20, 30))->__debugInfo(); + $this->assertEquals('10', $info['hue']); + $this->assertEquals('20', $info['saturation']); + $this->assertEquals('30', $info['value']); + $this->assertEquals('1', $info['alpha']); + } + + public function testCreateFailsInvalidArguments(): void + { + $this->expectException(InvalidArgumentException::class); + Color::create(1000, 2000, 1000); + } + + public function testCreateWithFourArgs(): void + { + $color = Color::create(180, 50, 50, .5); + $this->assertInstanceOf(Color::class, $color); + $this->assertEquals(180, $color->hue()->value()); + $this->assertEquals(50, $color->saturation()->value()); + $this->assertEquals(50, $color->value()->value()); + $this->assertEquals(128, $color->alpha()->value()); + } + + public function testConstructorWithChannelObjects(): void + { + $color = new Color(new Hue(180), new Saturation(50), new Value(50), new Alpha(.5)); + $this->assertEquals(180, $color->hue()->value()); + $this->assertEquals(50, $color->saturation()->value()); + $this->assertEquals(50, $color->value()->value()); + $this->assertEquals(128, $color->alpha()->value()); + } + + public function testCloneDeepCopiesChannels(): void + { + $original = new Color(180, 50, 50); + $cloned = clone $original; + + $this->assertEquals(180, $original->hue()->value()); + $this->assertEquals(180, $cloned->hue()->value()); + + // Verify they are separate objects (deep clone) + $this->assertNotSame($original->hue(), $cloned->hue()); + $this->assertNotSame($original->saturation(), $cloned->saturation()); + $this->assertNotSame($original->value(), $cloned->value()); + } + + public function testToColorspace(): void + { + $color = new Color(16, 100, 100); + $result = $color->toColorspace(RgbColorspace::class); + $this->assertInstanceOf(RgbColor::class, $result); + } + + public function testToColorspaceWithObject(): void + { + $color = new Color(16, 100, 100); + $result = $color->toColorspace(new RgbColorspace()); + $this->assertInstanceOf(RgbColor::class, $result); + } + + public function testToColorspaceFailsInvalidClass(): void + { + $color = new Color(0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->toColorspace('NonExistentClass'); + } + + public function testToColorspaceFailsNonColorspaceClass(): void + { + $color = new Color(0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->toColorspace(\stdClass::class); + } +} diff --git a/tests/Unit/Colors/Hsv/ColorspaceTest.php b/tests/Unit/Colors/Hsv/ColorspaceTest.php new file mode 100644 index 000000000..eb2e0a457 --- /dev/null +++ b/tests/Unit/Colors/Hsv/ColorspaceTest.php @@ -0,0 +1,262 @@ +colorFromNormalized([1, 0, 1]); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(360, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Value::class)->value()); + + $colorspace = new Colorspace(); + $result = $colorspace->colorFromNormalized([1, 0, 1, .2]); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(360, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Value::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testImportRgbColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new RgbColor(255, 0, 255)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(300, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Value::class)->value()); + + $result = $colorspace->importColor(new RgbColor(127, 127, 127)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Value::class)->value()); + + $result = $colorspace->importColor(new RgbColor(127, 127, 127, .3333333333)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Value::class)->value()); + $this->assertEquals(85, $result->channel(Alpha::class)->value()); + } + + public function testImportCmykColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new CmykColor(0, 100, 0, 0)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(300, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Value::class)->value()); + + $result = $colorspace->importColor(new CmykColor(0, 0, 0, 50)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Value::class)->value()); + } + + public function testImportHslColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new HslColor(300, 100, 50)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(300, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Value::class)->value()); + + $result = $colorspace->importColor(new HslColor(0, 0, 50)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(50, $result->channel(Value::class)->value()); + } + + public function testImportNamedColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(NamedColor::WHITE); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Value::class)->value()); + } + + public function testImportOklabColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new OklabColor(0.68, 0.17, 0.14)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEqualsWithDelta(19, $result->channel(Hue::class)->value(), 2); + $this->assertEqualsWithDelta(100, $result->channel(Saturation::class)->value(), 1); + $this->assertEqualsWithDelta(100, $result->channel(Value::class)->value(), 1); + } + + public function testImportOklchColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new OklchColor(0.68, 0.22, 38.8)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEqualsWithDelta(19, $result->channel(Hue::class)->value(), 2); + $this->assertEqualsWithDelta(100, $result->channel(Saturation::class)->value(), 1); + $this->assertEqualsWithDelta(100, $result->channel(Value::class)->value(), 1); + } + + public function testImportHsvColorPassthrough(): void + { + $colorspace = new Colorspace(); + + $color = new HsvColor(200, 50, 80); + $result = $colorspace->importColor($color); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertSame($color, $result); + } + + public function testColorFromNormalizedInvalidChannelCount(): void + { + $this->expectException(InvalidArgumentException::class); + Colorspace::colorFromNormalized([0.5, 0.5]); + } + + public function testColorFromNormalizedWithNullValue(): void + { + $this->expectException(InvalidArgumentException::class); + Colorspace::colorFromNormalized([0.5, null, 0.5]); + } + + public function testImportUnsupportedColor(): void + { + $this->expectException(ColorException::class); + $colorspace = new Colorspace(); + $colorspace->importColor(Mockery::mock(ColorInterface::class)); + } + + public function testChannels(): void + { + $channels = Colorspace::channels(); + $this->assertIsArray($channels); + $this->assertCount(4, $channels); + $this->assertEquals(Hue::class, $channels[0]); + $this->assertEquals(Saturation::class, $channels[1]); + $this->assertEquals(Value::class, $channels[2]); + $this->assertEquals(Alpha::class, $channels[3]); + } + + public function testImportRgbColorHueBranchMaxIsGreen(): void + { + $colorspace = new Colorspace(); + + // RGB(0, 255, 0) => max=G, min=R => hits default branch ($g == $min is false) + $result = $colorspace->importColor(new RgbColor(0, 255, 0)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(120, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Value::class)->value()); + } + + public function testImportRgbColorHueBranchMaxIsBlue(): void + { + $colorspace = new Colorspace(); + + // RGB(0, 0, 255) => max=B, min=R => hits ($r == $min) branch + $result = $colorspace->importColor(new RgbColor(0, 0, 255)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(240, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Value::class)->value()); + } + + public function testImportRgbColorHueBranchMaxIsRed(): void + { + $colorspace = new Colorspace(); + + // RGB(255, 0, 0) => max=R, min=G=B => hits ($b == $min) branch + $result = $colorspace->importColor(new RgbColor(255, 0, 0)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(100, $result->channel(Saturation::class)->value()); + $this->assertEquals(100, $result->channel(Value::class)->value()); + } + + public function testImportRgbColorGrayscale(): void + { + $colorspace = new Colorspace(); + + // Grayscale color => chroma == 0 => early return + $result = $colorspace->importColor(new RgbColor(100, 100, 100)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(39, $result->channel(Value::class)->value()); + } + + public function testImportRgbColorGrayscalePreservesAlpha(): void + { + $colorspace = new Colorspace(); + + // Semi-transparent grayscale => chroma == 0 => early return must preserve alpha + $result = $colorspace->importColor(new RgbColor(100, 100, 100, .5)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(39, $result->channel(Value::class)->value()); + $this->assertEquals(128, $result->channel(Alpha::class)->value()); + } + + public function testImportHslColorBlack(): void + { + $colorspace = new Colorspace(); + + // HslColor(0, 0, 0) = black => v = l + s*min(l, 1-l) = 0 + // hits ($v == 0) ? 0 : ... branch for saturation calculation + $result = $colorspace->importColor(new HslColor(0, 0, 0)); + $this->assertInstanceOf(HsvColor::class, $result); + $this->assertEquals(0, $result->channel(Hue::class)->value()); + $this->assertEquals(0, $result->channel(Saturation::class)->value()); + $this->assertEquals(0, $result->channel(Value::class)->value()); + } + + public function testImportHslColorTypeCheck(): void + { + $colorspace = new Colorspace(); + + // Call protected importHslColor() with a non-HslColor to trigger type check + $method = new \ReflectionMethod($colorspace, 'importHslColor'); + + $this->expectException(InvalidArgumentException::class); + $method->invoke($colorspace, new RgbColor(255, 0, 0)); + } +} diff --git a/tests/Unit/Colors/Hsv/Decoders/StringColorDecoderTest.php b/tests/Unit/Colors/Hsv/Decoders/StringColorDecoderTest.php new file mode 100644 index 000000000..f150a3fb4 --- /dev/null +++ b/tests/Unit/Colors/Hsv/Decoders/StringColorDecoderTest.php @@ -0,0 +1,53 @@ + + */ + #[DataProviderExternal(ColorDataProvider::class, 'hsvString')] + public function testDecode(mixed $input, array $channelValues): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode($input[0]); + $this->assertEquals( + $channelValues, + array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $result->channels(), + ), + ); + } + + public function testSupportsString(): void + { + $decoder = new StringColorDecoder(); + $this->assertTrue($decoder->supports('hsv(0, 0%, 0%)')); + $this->assertTrue($decoder->supports('HSV(360, 100%, 100%)')); + $this->assertTrue($decoder->supports('hsb(0, 0%, 0%)')); + $this->assertTrue($decoder->supports('HSB(360, 100%, 100%)')); + $this->assertFalse($decoder->supports('rgb(0, 0, 0)')); + $this->assertFalse($decoder->supports(123)); + $this->assertFalse($decoder->supports(null)); + } + + public function testDecodeInvalid(): void + { + $decoder = new StringColorDecoder(); + $this->expectException(InvalidArgumentException::class); + $decoder->decode('hsv(invalid)'); + } +} diff --git a/tests/Unit/Colors/Oklab/ChannelTest.php b/tests/Unit/Colors/Oklab/ChannelTest.php new file mode 100644 index 000000000..94ad5c9e0 --- /dev/null +++ b/tests/Unit/Colors/Oklab/ChannelTest.php @@ -0,0 +1,94 @@ +assertInstanceOf(Lightness::class, $channel); + + $channel = Lightness::fromNormalized(1); + $this->assertInstanceOf(Lightness::class, $channel); + } + + public function testConstructorFailInvalidArgument(): void + { + $this->expectException(InvalidArgumentException::class); + new Lightness(300); + } + + public function testConstructorFailInvalidArgumentNormalized(): void + { + $this->expectException(InvalidArgumentException::class); + Lightness::fromNormalized(2); + } + + public function testToString(): void + { + $channel = new Lightness(1); + $this->assertEquals("1", $channel->toString()); + $this->assertEquals("1", (string) $channel); + } + + public function testValue(): void + { + $this->assertEquals(1, (new Lightness(1))->value()); + } + + public function testNormalize(): void + { + $channel = new Lightness(1); + $this->assertEquals(1, $channel->normalized()); + $channel = new Lightness(0); + $this->assertEquals(0, $channel->normalized()); + $channel = new Lightness(.5); + $this->assertEquals(.5, $channel->normalized()); + } + + public function testFromNormalizedOnFloatChannel(): void + { + // A channel: min=-0.4, max=0.4 — fromNormalized(0.5) = -0.4 + 0.5*(0.4-(-0.4)) = -0.4 + 0.4 = 0 + $channel = A::fromNormalized(0.5); + $this->assertEqualsWithDelta(0.0, $channel->value(), 0.0001); + + $channel = A::fromNormalized(0); + $this->assertEqualsWithDelta(-0.4, $channel->value(), 0.0001); + + $channel = A::fromNormalized(1); + $this->assertEqualsWithDelta(0.4, $channel->value(), 0.0001); + } + + public function testFromNormalizedFailsNegative(): void + { + $this->expectException(InvalidArgumentException::class); + Lightness::fromNormalized(-0.1); + } + + public function testValidateA(): void + { + $this->expectException(InvalidArgumentException::class); + new A(0.5); + } + + public function testValidateB(): void + { + $this->expectException(InvalidArgumentException::class); + new B(0.5); + } +} diff --git a/tests/Unit/Colors/Oklab/ColorTest.php b/tests/Unit/Colors/Oklab/ColorTest.php new file mode 100644 index 000000000..737b653e7 --- /dev/null +++ b/tests/Unit/Colors/Oklab/ColorTest.php @@ -0,0 +1,249 @@ +assertInstanceOf(Color::class, $color); + } + + public function testCreate(): void + { + $color = Color::create(.51, .1, -.2); + $this->assertInstanceOf(Color::class, $color); + $this->assertEquals( + [.51, .1, -.2, 255], + array_map( + fn(ColorChannelInterface $channel): int|float => $channel->value(), + $color->channels() + ) + ); + } + + /** + * @param $input array + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'oklabString')] + public function testParse(array $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::parse(...$input)->channels()), + ); + } + + public function testColorspace(): void + { + $color = new Color(0, 0, 0); + $this->assertInstanceOf(OklabColorspace::class, $color->colorspace()); + } + + public function testChannels(): void + { + $color = new Color(0, .1, .2); + $this->assertIsArray($color->channels()); + $this->assertCount(4, $color->channels()); + } + + public function testChannel(): void + { + $color = new Color(0, .1, .2); + + $channel = $color->channel(Lightness::class); + $this->assertInstanceOf(Lightness::class, $channel); + $this->assertEquals(0, $channel->value()); + + $channel = $color->channel(A::class); + $this->assertInstanceOf(A::class, $channel); + $this->assertEquals(.1, $channel->value()); + + $channel = $color->channel(B::class); + $this->assertInstanceOf(B::class, $channel); + $this->assertEquals(.2, $channel->value()); + } + + public function testChannelNotFound(): void + { + $color = new Color(0, .1, .2); + $this->expectException(InvalidArgumentException::class); + $color->channel('none'); + } + + public function testLightnessAB(): void + { + $color = new Color(0, .1, .2); + $this->assertInstanceOf(Lightness::class, $color->lightness()); + $this->assertInstanceOf(A::class, $color->a()); + $this->assertInstanceOf(B::class, $color->b()); + $this->assertEquals(0, $color->lightness()->value()); + $this->assertEquals(.1, $color->a()->value()); + $this->assertEquals(.2, $color->b()->value()); + } + + public function testToHex(): void + { + $color = new Color(0.64905124115, 0.19974263609074, 0.13044605841927); + $this->assertEquals('ff3700', $color->toHex()); + + $color = new Color(0.64905124115, 0.19974263609074, 0.13044605841927, .2); + $this->assertEquals('ff370033', $color->toHex()); + } + + public function testNormalize(): void + { + $color = new Color(1, .2, -0.4); + $this->assertEquals( + [1.0, 0.7500000000000001, 0.0, 1], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + + $color = new Color(0, 0, 0); + $this->assertEquals( + [0, 0.5, 0.5, 1], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + + $color = new Color(0, 0, 0, .5); + $this->assertEquals( + [0, 0.5, 0.5, 0.5019607843137255], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + } + + public function testToString(): void + { + $color = new Color(0, .1, -0.2); + $this->assertEquals('oklab(0 0.1 -0.2)', (string) $color); + } + + public function testToColorspace(): void + { + $color = new Color(0.64905124115, 0.19974263609074, 0.13044605841927); + $converted = $color->toColorspace(Rgb::class); + $this->assertInstanceOf(RgbColor::class, $converted); + $this->assertEquals( + [255, 55, 0, 255], + array_map( + fn(ColorChannelInterface $channel): int|float => $channel->value(), + $converted->channels() + ) + ); + } + + public function testIsGrayscale(): void + { + $color = new Color(0, 0, 0); + $this->assertTrue($color->isGrayscale()); + + $color = new Color(.5, .10, -0.2); + $this->assertFalse($color->isGrayscale()); + } + + public function testSetTransparency(): void + { + $color = new Color(0, 0, 0, 1); + $result = $color->withTransparency(.2); + $this->assertEquals(255, $color->channel(Alpha::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testDebugInfo(): void + { + $info = (new Color(0, .1, .2))->__debugInfo(); + $this->assertEquals('0', $info['lightness']); + $this->assertEquals('0.1', $info['a']); + $this->assertEquals('0.2', $info['b']); + $this->assertEquals('1', $info['alpha']); + } + + public function testIsTransparent(): void + { + $color = new Color(0, 0, 0); + $this->assertFalse($color->isTransparent()); + + $color = new Color(0, 0, 0, 1); + $this->assertFalse($color->isTransparent()); + + $color = new Color(0, 0, 0, .5); + $this->assertTrue($color->isTransparent()); + + $color = new Color(0, 0, 0, 0); + $this->assertTrue($color->isTransparent()); + } + + public function testIsClear(): void + { + $color = new Color(0, 0, 0); + $this->assertFalse($color->isClear()); + + $color = new Color(0, 0, 0, 1); + $this->assertFalse($color->isClear()); + + $color = new Color(0, 0, 0, .2); + $this->assertFalse($color->isClear()); + + $color = new Color(0, 0, 0, 0); + $this->assertTrue($color->isClear()); + } + + public function testCreateFailsInvalidArguments(): void + { + $this->expectException(InvalidArgumentException::class); + Color::create(100, 2000, 1000); + } + + public function testConstructorWithChannelObjects(): void + { + $color = new Color(new Lightness(.5), new A(.1), new B(-.2), new Alpha(.5)); + $this->assertEquals(.5, $color->lightness()->value()); + $this->assertEquals(.1, $color->a()->value()); + $this->assertEquals(-.2, $color->b()->value()); + $this->assertEquals(128, $color->alpha()->value()); + } + + public function testCloneDeepCopiesChannels(): void + { + $original = new Color(.5, .1, -.2); + $cloned = clone $original; + + $this->assertEquals(.5, $original->lightness()->value()); + $this->assertEquals(.5, $cloned->lightness()->value()); + + // Verify they are separate objects (deep clone) + $this->assertNotSame($original->lightness(), $cloned->lightness()); + $this->assertNotSame($original->a(), $cloned->a()); + $this->assertNotSame($original->b(), $cloned->b()); + } +} diff --git a/tests/Unit/Colors/Oklab/ColorspaceTest.php b/tests/Unit/Colors/Oklab/ColorspaceTest.php new file mode 100644 index 000000000..93f1787df --- /dev/null +++ b/tests/Unit/Colors/Oklab/ColorspaceTest.php @@ -0,0 +1,151 @@ +colorFromNormalized([1.0, 0.5, 0]); + $this->assertInstanceOf(OklabColor::class, $result); + $this->assertEquals(1.0, $result->channel(Lightness::class)->value()); + $this->assertEquals(0.0, $result->channel(A::class)->value()); + $this->assertEquals(-0.4, $result->channel(B::class)->value()); + + $result = $colorspace->colorFromNormalized([1.0, 1, .25]); + $this->assertInstanceOf(OklabColor::class, $result); + $this->assertEquals(1.0, $result->channel(Lightness::class)->value()); + $this->assertEquals(0.4, $result->channel(A::class)->value()); + $this->assertEquals(-0.2, $result->channel(B::class)->value()); + + $result = $colorspace->colorFromNormalized([1.0, 1, .25, .2]); + $this->assertInstanceOf(OklabColor::class, $result); + $this->assertEquals(1.0, $result->channel(Lightness::class)->value()); + $this->assertEquals(0.4, $result->channel(A::class)->value()); + $this->assertEquals(-0.2, $result->channel(B::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testImportRgbColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new RgbColor(255, 85, 0)); + $this->assertInstanceOf(OklabColor::class, $result); + $this->assertEquals(0.68, round($result->channel(Lightness::class)->value(), 2)); + $this->assertEquals(0.17, round($result->channel(A::class)->value(), 2)); + $this->assertEquals(0.14, round($result->channel(B::class)->value(), 2)); + } + + public function testImportCmykColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new CmykColor(0, 67, 100, 0)); + $this->assertInstanceOf(OklabColor::class, $result); + $this->assertEquals(0.7, round($result->channel(Lightness::class)->value(), 1)); + $this->assertEquals(0.2, round($result->channel(A::class)->value(), 1)); + $this->assertEquals(0.1, round($result->channel(B::class)->value(), 1)); + } + + public function testImportHsvColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new HsvColor(300, 100, 100)); + $this->assertInstanceOf(OklabColor::class, $result); + $this->assertEquals(0.7, round($result->channel(Lightness::class)->value(), 1)); + $this->assertEquals(0.3, round($result->channel(A::class)->value(), 1)); + $this->assertEquals(-0.2, round($result->channel(B::class)->value(), 1)); + } + + public function testImportHslColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new HslColor(300, 100, 50)); + $this->assertInstanceOf(OklabColor::class, $result); + $this->assertEquals(0.7, round($result->channel(Lightness::class)->value(), 1)); + $this->assertEquals(0.3, round($result->channel(A::class)->value(), 1)); + $this->assertEquals(-0.2, round($result->channel(B::class)->value(), 1)); + } + + public function testImportNamedColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(NamedColor::WHITE); + $this->assertInstanceOf(OklabColor::class, $result); + $this->assertEquals(1.0, round($result->channel(Lightness::class)->value(), 1)); + $this->assertEquals(0.0, round($result->channel(A::class)->value(), 1)); + $this->assertEquals(0.0, round($result->channel(B::class)->value(), 1)); + } + + public function testImportOklchColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new OklchColor(0.65, 0.24, 33.15)); + $this->assertInstanceOf(OklabColor::class, $result); + $this->assertEquals(0.65, round($result->channel(Lightness::class)->value(), 2)); + $this->assertEqualsWithDelta(0.20, round($result->channel(A::class)->value(), 2), 0.01); + $this->assertEqualsWithDelta(0.13, round($result->channel(B::class)->value(), 2), 0.01); + } + + public function testImportOklabColorPassthrough(): void + { + $colorspace = new Colorspace(); + $color = new OklabColor(0.5, 0.1, -0.1); + $result = $colorspace->importColor($color); + $this->assertInstanceOf(OklabColor::class, $result); + $this->assertSame($color, $result); + } + + public function testColorFromNormalizedInvalidChannelCount(): void + { + $this->expectException(InvalidArgumentException::class); + Colorspace::colorFromNormalized([0.5, 0.5]); + } + + public function testColorFromNormalizedWithNullValue(): void + { + $this->expectException(InvalidArgumentException::class); + Colorspace::colorFromNormalized([0.5, null, 0.5]); + } + + public function testImportUnsupportedColor(): void + { + $this->expectException(ColorException::class); + $colorspace = new Colorspace(); + $colorspace->importColor(Mockery::mock(ColorInterface::class)); + } + + public function testChannels(): void + { + $channels = Colorspace::channels(); + $this->assertIsArray($channels); + $this->assertCount(4, $channels); + $this->assertEquals(Lightness::class, $channels[0]); + $this->assertEquals(A::class, $channels[1]); + $this->assertEquals(B::class, $channels[2]); + $this->assertEquals(Alpha::class, $channels[3]); + } +} diff --git a/tests/Unit/Colors/Oklab/Decoders/StringColorDecoderTest.php b/tests/Unit/Colors/Oklab/Decoders/StringColorDecoderTest.php new file mode 100644 index 000000000..7ee7735c4 --- /dev/null +++ b/tests/Unit/Colors/Oklab/Decoders/StringColorDecoderTest.php @@ -0,0 +1,94 @@ + + */ + #[DataProviderExternal(ColorDataProvider::class, 'oklabString')] + public function testDecode(mixed $input, array $channelValues): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode($input[0]); + $this->assertEquals( + $channelValues, + array_map( + fn(ColorChannelInterface $channel): int|float => $channel->value(), + $result->channels(), + ), + ); + } + + public function testSupportsOklabString(): void + { + $decoder = new StringColorDecoder(); + $this->assertTrue($decoder->supports('oklab(0.5 0.1 -0.1)')); + $this->assertTrue($decoder->supports('OKLAB(0.5 0.1 -0.1)')); + $this->assertTrue($decoder->supports('oklab(0.5, 0.1, -0.1)')); + } + + public function testSupportsNonString(): void + { + $decoder = new StringColorDecoder(); + $this->assertFalse($decoder->supports(123)); + $this->assertFalse($decoder->supports(null)); + $this->assertFalse($decoder->supports([])); + } + + public function testSupportsInvalidString(): void + { + $decoder = new StringColorDecoder(); + $this->assertFalse($decoder->supports('rgb(255, 0, 0)')); + $this->assertFalse($decoder->supports('hsl(0, 100%, 50%)')); + $this->assertFalse($decoder->supports('not a color')); + } + + public function testDecodeInvalid(): void + { + $decoder = new StringColorDecoder(); + $this->expectException(InvalidArgumentException::class); + $decoder->decode('oklab(invalid)'); + } + + public function testDecodeWithPercentageValues(): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode('oklab(50% 10% -10%)'); + $channels = $result->channels(); + // 50% of lightness max (1.0) = 0.5 + $this->assertEqualsWithDelta(0.5, $channels[0]->value(), 0.01); + } + + public function testDecodeWithAlpha(): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode('oklab(0.5 0.1 -0.1 / 0.5)'); + $channels = $result->channels(); + $this->assertCount(4, $channels); + // Alpha value() returns internal int (0-255 range), 0.5 * 255 = 128 + $this->assertEqualsWithDelta(128, $channels[3]->value(), 1); + } + + public function testDecodeWithAlphaPercentage(): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode('oklab(0.5 0.1 -0.1 / 50%)'); + $channels = $result->channels(); + $this->assertCount(4, $channels); + // Alpha value() returns internal int (0-255 range), 50% = 0.5 * 255 = 128 + $this->assertEqualsWithDelta(128, $channels[3]->value(), 1); + } +} diff --git a/tests/Unit/Colors/Oklch/ChannelTest.php b/tests/Unit/Colors/Oklch/ChannelTest.php new file mode 100644 index 000000000..eb850c910 --- /dev/null +++ b/tests/Unit/Colors/Oklch/ChannelTest.php @@ -0,0 +1,61 @@ +assertInstanceOf(Lightness::class, $channel); + + $channel = Lightness::fromNormalized(1); + $this->assertInstanceOf(Lightness::class, $channel); + } + + public function testConstructorFailInvalidArgument(): void + { + $this->expectException(InvalidArgumentException::class); + new Lightness(300); + } + + public function testConstructorFailInvalidArgumentNormalized(): void + { + $this->expectException(InvalidArgumentException::class); + Lightness::fromNormalized(2); + } + + public function testToString(): void + { + $channel = new Lightness(1); + $this->assertEquals("1", $channel->toString()); + $this->assertEquals("1", (string) $channel); + } + + public function testValue(): void + { + $this->assertEquals(1, (new Lightness(1))->value()); + } + + public function testNormalize(): void + { + $channel = new Lightness(1); + $this->assertEquals(1, $channel->normalized()); + $channel = new Lightness(0); + $this->assertEquals(0, $channel->normalized()); + $channel = new Lightness(.5); + $this->assertEquals(.5, $channel->normalized()); + } +} diff --git a/tests/Unit/Colors/Oklch/ColorTest.php b/tests/Unit/Colors/Oklch/ColorTest.php new file mode 100644 index 000000000..8aac98bdc --- /dev/null +++ b/tests/Unit/Colors/Oklch/ColorTest.php @@ -0,0 +1,270 @@ +assertInstanceOf(Color::class, $color); + } + + public function testCreate(): void + { + $color = Color::create(0, 0.123, 180); + $this->assertInstanceOf(Color::class, $color); + $this->assertEquals( + [0.0, .123, 180, 255], + array_map( + fn(ColorChannelInterface $channel): int|float => $channel->value(), + $color->channels() + ) + ); + + $color = Color::create(.51, -0.1, 2); + $this->assertInstanceOf(Color::class, $color); + $this->assertEquals( + [.51, -0.1, 2, 255], + array_map( + fn(ColorChannelInterface $channel): int|float => $channel->value(), + $color->channels() + ) + ); + + $color = Color::create(.51, -0.1, 2, .2); + $this->assertInstanceOf(Color::class, $color); + $this->assertEquals( + [.51, -0.1, 2, 51], + array_map( + fn(ColorChannelInterface $channel): int|float => $channel->value(), + $color->channels() + ) + ); + } + + /** + * @param $input array + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'oklchString')] + public function testParse(array $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::parse(...$input)->channels()), + ); + } + + public function testColorspace(): void + { + $color = new Color(0, 0, 0); + $this->assertInstanceOf(OklchColorspace::class, $color->colorspace()); + } + + public function testChannels(): void + { + $color = new Color(0, 0, 0); + $this->assertIsArray($color->channels()); + $this->assertCount(4, $color->channels()); + } + + public function testChannel(): void + { + $color = new Color(0, .1, 2); + + $channel = $color->channel(Lightness::class); + $this->assertInstanceOf(Lightness::class, $channel); + $this->assertEquals(0, $channel->value()); + + $channel = $color->channel(Chroma::class); + $this->assertInstanceOf(Chroma::class, $channel); + $this->assertEquals(.1, $channel->value()); + + $channel = $color->channel(Hue::class); + $this->assertInstanceOf(Hue::class, $channel); + $this->assertEquals(2, $channel->value()); + + $channel = $color->channel(Alpha::class); + $this->assertInstanceOf(Alpha::class, $channel); + $this->assertEquals(255, $channel->value()); + } + + public function testChannelNotFound(): void + { + $color = new Color(0, .1, 2); + $this->expectException(InvalidArgumentException::class); + $color->channel('none'); + } + + public function testLightnessChromaHue(): void + { + $color = new Color(0, .1, 2); + $this->assertInstanceOf(Lightness::class, $color->lightness()); + $this->assertInstanceOf(Hue::class, $color->hue()); + $this->assertInstanceOf(Chroma::class, $color->chroma()); + $this->assertEquals(0, $color->lightness()->value()); + $this->assertEquals(.1, $color->chroma()->value()); + $this->assertEquals(2, $color->hue()->value()); + } + + public function testToHex(): void + { + $color = new Color(0.6759, 0.21747, 38.8022); + $this->assertEquals('ff5500', $color->toHex()); + + $color = new Color(0.6759, 0.21747, 38.8022, .2); + $this->assertEquals('ff550033', $color->toHex()); + } + + public function testNormalize(): void + { + $color = new Color(1, -0.4, 180); + $this->assertEquals( + [1.0, 0, .5, 1], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + + $color = new Color(1, 0.4, 90); + $this->assertEquals( + [1.0, 1.0, .25, 1], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + } + + public function testToString(): void + { + $color = new Color(0, .1, 2); + $this->assertEquals('oklch(0 0.1 2)', (string) $color); + } + + public function testToColorspace(): void + { + $color = new Color(0.6759, 0.21747, 38.8022); + $converted = $color->toColorspace(Rgb::class); + $this->assertInstanceOf(RgbColor::class, $converted); + $this->assertEquals( + [255, 85, 0, 255], + array_map( + fn(ColorChannelInterface $channel): int|float => $channel->value(), + $converted->channels() + ) + ); + } + + public function testIsGrayscale(): void + { + $color = new Color(0, 0, 0); + $this->assertTrue($color->isGrayscale()); + + $color = new Color(0, .1, 2); + $this->assertFalse($color->isGrayscale()); + } + + public function testSetTransparency(): void + { + $color = new Color(0, 0, 0, 1); + $result = $color->withTransparency(.2); + $this->assertEquals(255, $color->channel(Alpha::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testDebugInfo(): void + { + $info = (new Color(0, .1, 2))->__debugInfo(); + $this->assertEquals('0', $info['lightness']); + $this->assertEquals('0.1', $info['chroma']); + $this->assertEquals('2', $info['hue']); + $this->assertEquals('1', $info['alpha']); + } + + public function testIsTransparent(): void + { + $color = new Color(0, 0, 0); + $this->assertFalse($color->isTransparent()); + + $color = new Color(0, 0, 0, 1); + $this->assertFalse($color->isTransparent()); + + $color = new Color(0, 0, 0, .5); + $this->assertTrue($color->isTransparent()); + + $color = new Color(0, 0, 0, 0); + $this->assertTrue($color->isTransparent()); + } + + public function testIsClear(): void + { + $color = new Color(0, 0, 0); + $this->assertFalse($color->isClear()); + + $color = new Color(0, 0, 0, 1); + $this->assertFalse($color->isClear()); + + $color = new Color(0, 0, 0, .2); + $this->assertFalse($color->isClear()); + + $color = new Color(0, 0, 0, 0); + $this->assertTrue($color->isClear()); + } + + public function testCreateFailsInvalidArguments(): void + { + $this->expectException(InvalidArgumentException::class); + Color::create(1000, 2000, 1000); + } + + public function testToStringWithAlpha(): void + { + $color = new Color(.5, .1, 180, .5); + $this->assertEquals('oklch(0.5 0.1 180 / 0.5)', (string) $color); + } + + public function testConstructorWithChannelObjects(): void + { + $color = new Color(new Lightness(.5), new Chroma(.1), new Hue(180), new Alpha(.5)); + $this->assertEquals(.5, $color->lightness()->value()); + $this->assertEquals(.1, $color->chroma()->value()); + $this->assertEquals(180, $color->hue()->value()); + $this->assertEquals(128, $color->alpha()->value()); + } + + public function testCloneDeepCopiesChannels(): void + { + $original = new Color(.5, .1, 180); + $cloned = clone $original; + + $this->assertEquals(.5, $original->lightness()->value()); + $this->assertEquals(.5, $cloned->lightness()->value()); + + // Verify they are separate objects (deep clone) + $this->assertNotSame($original->lightness(), $cloned->lightness()); + $this->assertNotSame($original->chroma(), $cloned->chroma()); + $this->assertNotSame($original->hue(), $cloned->hue()); + } +} diff --git a/tests/Unit/Colors/Oklch/ColorspaceTest.php b/tests/Unit/Colors/Oklch/ColorspaceTest.php new file mode 100644 index 000000000..14ff03986 --- /dev/null +++ b/tests/Unit/Colors/Oklch/ColorspaceTest.php @@ -0,0 +1,151 @@ +colorFromNormalized([1.0, 0, 1]); + $this->assertInstanceOf(OklchColor::class, $result); + $this->assertEquals(1.0, $result->channel(Lightness::class)->value()); + $this->assertEquals(-0.4, $result->channel(Chroma::class)->value()); + $this->assertEquals(360, $result->channel(Hue::class)->value()); + + $result = $colorspace->colorFromNormalized([0.5, 0.5, 0.25]); + $this->assertInstanceOf(OklchColor::class, $result); + $this->assertEquals(0.5, $result->channel(Lightness::class)->value()); + $this->assertEquals(0.0, $result->channel(Chroma::class)->value()); + $this->assertEquals(90.0, $result->channel(Hue::class)->value()); + + $result = $colorspace->colorFromNormalized([0.5, 0.5, 0.25, .2]); + $this->assertInstanceOf(OklchColor::class, $result); + $this->assertEquals(0.5, $result->channel(Lightness::class)->value()); + $this->assertEquals(0.0, $result->channel(Chroma::class)->value()); + $this->assertEquals(90.0, $result->channel(Hue::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testImportRgbColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new RgbColor(255, 85, 0)); + $this->assertInstanceOf(OklchColor::class, $result); + $this->assertEquals(0.68, round($result->channel(Lightness::class)->value(), 2)); + $this->assertEquals(0.22, round($result->channel(Chroma::class)->value(), 2)); + $this->assertEquals(38.8, round($result->channel(Hue::class)->value(), 2)); + } + + public function testImportOklabColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new OklabColor(.64905124115, .19974263609074, .13044605841927)); + $this->assertInstanceOf(OklchColor::class, $result); + $this->assertEquals(.64905124115, $result->channel(Lightness::class)->value()); + $this->assertEquals(.23856507462242119, $result->channel(Chroma::class)->value()); + $this->assertEquals(33.14737546449335, $result->channel(Hue::class)->value()); + } + + public function testImportCmykColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new CmykColor(0, 67, 100, 0)); // ff5500, rgb(255, 85, 0) + $this->assertInstanceOf(OklchColor::class, $result); + $this->assertEquals(0.674822658543154, $result->channel(Lightness::class)->value()); + $this->assertEquals(0.2182398312296448, $result->channel(Chroma::class)->value()); + $this->assertEquals(38.56205123589542, $result->channel(Hue::class)->value()); + } + + public function testImportHsvColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new HsvColor(20, 100, 100)); + $this->assertInstanceOf(OklchColor::class, $result); + $this->assertEquals(0.68, round($result->channel(Lightness::class)->value(), 2)); + $this->assertEquals(0.22, round($result->channel(Chroma::class)->value(), 2)); + $this->assertEquals(38.8, round($result->channel(Hue::class)->value(), 2)); + } + + public function testImportHslColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new HslColor(20, 100, 50)); + $this->assertInstanceOf(OklchColor::class, $result); + $this->assertEquals(0.68, round($result->channel(Lightness::class)->value(), 2)); + $this->assertEquals(0.22, round($result->channel(Chroma::class)->value(), 2)); + $this->assertEquals(38.8, round($result->channel(Hue::class)->value(), 2)); + } + + public function testImportNamedColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(NamedColor::WHITE); + $this->assertInstanceOf(OklchColor::class, $result); + $this->assertEquals(1.0, round($result->channel(Lightness::class)->value(), 2)); + $this->assertEquals(0, round($result->channel(Chroma::class)->value(), 2)); + $this->assertEquals(89.88, round($result->channel(Hue::class)->value(), 2)); + } + + public function testImportOklchColorPassthrough(): void + { + $colorspace = new Colorspace(); + $color = new OklchColor(0.5, 0.15, 180.0); + $result = $colorspace->importColor($color); + $this->assertInstanceOf(OklchColor::class, $result); + $this->assertSame($color, $result); + } + + public function testColorFromNormalizedInvalidChannelCount(): void + { + $this->expectException(InvalidArgumentException::class); + Colorspace::colorFromNormalized([0.5, 0.5]); + } + + public function testColorFromNormalizedWithNullValue(): void + { + $this->expectException(InvalidArgumentException::class); + Colorspace::colorFromNormalized([0.5, null, 0.5]); + } + + public function testImportUnsupportedColor(): void + { + $this->expectException(ColorException::class); + $colorspace = new Colorspace(); + $colorspace->importColor(Mockery::mock(ColorInterface::class)); + } + + public function testChannels(): void + { + $channels = Colorspace::channels(); + $this->assertIsArray($channels); + $this->assertCount(4, $channels); + $this->assertEquals(Lightness::class, $channels[0]); + $this->assertEquals(Chroma::class, $channels[1]); + $this->assertEquals(Hue::class, $channels[2]); + $this->assertEquals(Alpha::class, $channels[3]); + } +} diff --git a/tests/Unit/Colors/Oklch/Decoders/StringColorDecoderTest.php b/tests/Unit/Colors/Oklch/Decoders/StringColorDecoderTest.php new file mode 100644 index 000000000..d561a1b47 --- /dev/null +++ b/tests/Unit/Colors/Oklch/Decoders/StringColorDecoderTest.php @@ -0,0 +1,94 @@ + + */ + #[DataProviderExternal(ColorDataProvider::class, 'oklchString')] + public function testDecode(mixed $input, array $channelValues): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode($input[0]); + $this->assertEquals( + $channelValues, + array_map( + fn(ColorChannelInterface $channel): int|float => $channel->value(), + $result->channels(), + ), + ); + } + + public function testSupportsOklchString(): void + { + $decoder = new StringColorDecoder(); + $this->assertTrue($decoder->supports('oklch(0.5 0.1 120)')); + $this->assertTrue($decoder->supports('OKLCH(0.5 0.1 120)')); + $this->assertTrue($decoder->supports('oklch(0.5, 0.1, 120)')); + } + + public function testSupportsNonString(): void + { + $decoder = new StringColorDecoder(); + $this->assertFalse($decoder->supports(123)); + $this->assertFalse($decoder->supports(null)); + $this->assertFalse($decoder->supports([])); + } + + public function testSupportsInvalidString(): void + { + $decoder = new StringColorDecoder(); + $this->assertFalse($decoder->supports('rgb(255, 0, 0)')); + $this->assertFalse($decoder->supports('oklab(0.5 0.1 -0.1)')); + $this->assertFalse($decoder->supports('not a color')); + } + + public function testDecodeInvalid(): void + { + $decoder = new StringColorDecoder(); + $this->expectException(InvalidArgumentException::class); + $decoder->decode('oklch(invalid)'); + } + + public function testDecodeWithPercentageValues(): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode('oklch(50% 10% 120)'); + $channels = $result->channels(); + // 50% of lightness max (1.0) = 0.5 + $this->assertEqualsWithDelta(0.5, $channels[0]->value(), 0.01); + } + + public function testDecodeWithAlpha(): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode('oklch(0.5 0.1 120 / 0.5)'); + $channels = $result->channels(); + $this->assertCount(4, $channels); + // Alpha value() returns internal int (0-255 range), 0.5 * 255 = 128 + $this->assertEqualsWithDelta(128, $channels[3]->value(), 1); + } + + public function testDecodeWithAlphaPercentage(): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode('oklch(0.5 0.1 120 / 50%)'); + $channels = $result->channels(); + $this->assertCount(4, $channels); + // Alpha value() returns internal int (0-255 range), 50% = 0.5 * 255 = 128 + $this->assertEqualsWithDelta(128, $channels[3]->value(), 1); + } +} diff --git a/tests/Unit/Colors/ProfileTest.php b/tests/Unit/Colors/ProfileTest.php new file mode 100644 index 000000000..76ee465bd --- /dev/null +++ b/tests/Unit/Colors/ProfileTest.php @@ -0,0 +1,90 @@ +assertInstanceOf(Profile::class, $profile); + } + + public function testConstructorFromResource(): void + { + $profile = new Profile(fopen('php://temp', 'r')); + $this->assertInstanceOf(Profile::class, $profile); + } + + public function testFromPath(): void + { + $profile = Profile::fromPath(Resource::create()->path()); + $this->assertInstanceOf(Profile::class, $profile); + $this->assertTrue($profile->size() > 0); + } + + public function testFromPathReturnsNonEmptyData(): void + { + $profile = Profile::fromPath(Resource::create()->path()); + $this->assertNotEmpty($profile->toString()); + } + + public function testFromPathNotFound(): void + { + $this->expectException(FileNotFoundException::class); + Profile::fromPath('/tmp/nonexistent_profile_' . hrtime(true) . '.icc'); + } + + public function testToString(): void + { + $profile = new Profile('foo'); + $this->assertEquals('foo', $profile->toString()); + } + + public function testCastToString(): void + { + $profile = new Profile('foo'); + $this->assertEquals('foo', (string) $profile); + } + + public function testToStream(): void + { + $profile = new Profile('foo'); + $fp = $profile->toStream(); + $this->assertIsResource($fp); + } + + public function testSize(): void + { + $profile = new Profile(); + $this->assertEquals(0, $profile->size()); + + $profile = new Profile('foo'); + $this->assertEquals(3, $profile->size()); + } + + public function testSave(): void + { + $profile = new Profile('foo'); + $path = __DIR__ . '/profile_' . strval(hrtime(true)) . '.test'; + + try { + $profile->save($path); + $this->assertFileExists($path); + $this->assertEquals('foo', file_get_contents($path)); + } finally { + if (file_exists($path)) { + unlink($path); + } + } + } +} diff --git a/tests/Unit/Colors/Rgb/ChannelTest.php b/tests/Unit/Colors/Rgb/ChannelTest.php new file mode 100644 index 000000000..b2e618b0c --- /dev/null +++ b/tests/Unit/Colors/Rgb/ChannelTest.php @@ -0,0 +1,154 @@ +assertInstanceOf(Channel::class, $channel); + + $channel = new Channel(value: 0); + $this->assertInstanceOf(Channel::class, $channel); + + $channel = Channel::fromNormalized(0); + $this->assertInstanceOf(Channel::class, $channel); + } + + public function testConstructorFailInvalidArgument(): void + { + $this->expectException(InvalidArgumentException::class); + new Channel(300); + } + + public function testConstructorFailInvalidArgumentNormalized(): void + { + $this->expectException(InvalidArgumentException::class); + Channel::fromNormalized(2); + } + + public function testToString(): void + { + $channel = new Channel(10); + $this->assertEquals("10", $channel->toString()); + $this->assertEquals("10", (string) $channel); + } + + public function testValue(): void + { + $channel = new Channel(10); + $this->assertEquals(10, $channel->value()); + } + + public function testNormalize(): void + { + $channel = new Channel(255); + $this->assertEquals(1, $channel->normalized()); + $channel = new Channel(0); + $this->assertEquals(0, $channel->normalized()); + $channel = new Channel(51); + $this->assertEquals(.2, $channel->normalized()); + } + + public function testValidate(): void + { + $this->expectException(InvalidArgumentException::class); + new Channel(256); + } + + public function testValidateNegative(): void + { + $this->expectException(InvalidArgumentException::class); + new Channel(-1); + } + + public function testScalePositive(): void + { + $channel = new Channel(100); + $result = $channel->scale(50); + $this->assertSame($channel, $result); + // base = (255 - 100) = 155, delta = round(155/100*50) = 78, value = 100+78 = 178 + $this->assertEquals(178, $channel->value()); + } + + public function testScaleNegative(): void + { + $channel = new Channel(200); + $result = $channel->scale(-50); + $this->assertSame($channel, $result); + // base = 200, delta = round(200/100*-50) = -100, value = 200-100 = 100 + $this->assertEquals(100, $channel->value()); + } + + public function testScaleZero(): void + { + $channel = new Channel(100); + $result = $channel->scale(0); + $this->assertSame($channel, $result); + $this->assertEquals(100, $channel->value()); + } + + public function testScaleMax(): void + { + $channel = new Channel(100); + $channel->scale(100); + // base = (255 - 100) = 155, delta = round(155/100*100) = 155, value = 100+155 = 255 + $this->assertEquals(255, $channel->value()); + } + + public function testScaleMinNegative(): void + { + $channel = new Channel(100); + $channel->scale(-100); + // base = 100, delta = round(100/100*-100) = -100, value = 100-100 = 0 + $this->assertEquals(0, $channel->value()); + } + + public function testScaleFailsOverRange(): void + { + $channel = new Channel(100); + $this->expectException(InvalidArgumentException::class); + $channel->scale(101); + } + + public function testScaleFailsUnderRange(): void + { + $channel = new Channel(100); + $this->expectException(InvalidArgumentException::class); + $channel->scale(-101); + } + + public function testFromNormalizedBoundaries(): void + { + $channel = Channel::fromNormalized(0); + $this->assertEquals(0, $channel->value()); + + $channel = Channel::fromNormalized(1); + $this->assertEquals(255, $channel->value()); + + $channel = Channel::fromNormalized(0.5); + $this->assertEquals(128, $channel->value()); + } + + public function testFromNormalizedFailsNegative(): void + { + $this->expectException(InvalidArgumentException::class); + Channel::fromNormalized(-0.1); + } +} diff --git a/tests/Unit/Colors/Rgb/Channels/AlphaTest.php b/tests/Unit/Colors/Rgb/Channels/AlphaTest.php new file mode 100644 index 000000000..d2a5ce353 --- /dev/null +++ b/tests/Unit/Colors/Rgb/Channels/AlphaTest.php @@ -0,0 +1,99 @@ +assertEquals('0.33', $alpha->toString()); + $this->assertEquals('0.33', (string) $alpha); + } + + public function testConstructorDefault(): void + { + $alpha = new Alpha(); + $this->assertEquals(255, $alpha->value()); + } + + public function testConstructorFullyOpaque(): void + { + $alpha = new Alpha(1); + $this->assertEquals(255, $alpha->value()); + } + + public function testConstructorFullyTransparent(): void + { + $alpha = new Alpha(0); + $this->assertEquals(0, $alpha->value()); + } + + public function testConstructorHalfTransparent(): void + { + $alpha = new Alpha(0.5); + $this->assertEquals(128, $alpha->value()); + } + + public function testConstructorFailsAboveOne(): void + { + $this->expectException(InvalidArgumentException::class); + new Alpha(1.1); + } + + public function testConstructorFailsBelowZero(): void + { + $this->expectException(InvalidArgumentException::class); + new Alpha(-0.1); + } + + public function testFromNormalized(): void + { + $alpha = Alpha::fromNormalized(0.5); + $this->assertEquals(128, $alpha->value()); + + $alpha = Alpha::fromNormalized(0); + $this->assertEquals(0, $alpha->value()); + + $alpha = Alpha::fromNormalized(1); + $this->assertEquals(255, $alpha->value()); + } + + public function testValue(): void + { + $alpha = new Alpha(0.2); + $this->assertEquals(51, $alpha->value()); + } + + public function testMin(): void + { + $this->assertEquals(0, Alpha::min()); + } + + public function testMax(): void + { + $this->assertEquals(255, Alpha::max()); + } + + public function testNormalizedValue(): void + { + $alpha = new Alpha(1); + $this->assertEquals(1.0, $alpha->normalized()); + + $alpha = new Alpha(0); + $this->assertEquals(0.0, $alpha->normalized()); + + $alpha = new Alpha(0.5); + $this->assertEquals(0.5, $alpha->normalized(1)); + } +} diff --git a/tests/Unit/Colors/Rgb/ColorTest.php b/tests/Unit/Colors/Rgb/ColorTest.php new file mode 100644 index 000000000..cb6f4b119 --- /dev/null +++ b/tests/Unit/Colors/Rgb/ColorTest.php @@ -0,0 +1,497 @@ +assertInstanceOf(Color::class, $color); + + $color = new Color(0, 0, 0, 0); + $this->assertInstanceOf(Color::class, $color); + } + + public function testCreate(): void + { + $color = Color::create(10, 20, 30, .2); + $this->assertInstanceOf(Color::class, $color); + $this->assertEquals( + [10, 20, 30, 51], + array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $color->channels(), + ), + ); + } + + /** + * @param $input array + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'rgbString')] + #[DataProviderExternal(ColorDataProvider::class, 'rgbHex')] + #[DataProviderExternal(ColorDataProvider::class, 'rgbNamedColor')] + public function testParse(array $input, array $channels): void + { + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), Color::parse(...$input)->channels()), + ); + } + + public function testColorspace(): void + { + $color = new Color(0, 0, 0); + $this->assertInstanceOf(RgbColorspace::class, $color->colorspace()); + } + + public function testChannels(): void + { + $color = new Color(10, 20, 30); + $this->assertIsArray($color->channels()); + $this->assertCount(4, $color->channels()); + } + + public function testChannel(): void + { + $color = new Color(10, 20, 30); + $channel = $color->channel(Red::class); + $this->assertInstanceOf(Red::class, $channel); + $this->assertEquals(10, $channel->value()); + } + + public function testChannelNotFound(): void + { + $color = new Color(10, 20, 30); + $this->expectException(InvalidArgumentException::class); + $color->channel('none'); + } + + public function testRedGreenBlue(): void + { + $color = new Color(10, 20, 30); + $this->assertInstanceOf(Red::class, $color->red()); + $this->assertInstanceOf(Green::class, $color->green()); + $this->assertInstanceOf(Blue::class, $color->blue()); + $this->assertEquals(10, $color->red()->value()); + $this->assertEquals(20, $color->green()->value()); + $this->assertEquals(30, $color->blue()->value()); + } + + public function testToHex(): void + { + $color = new Color(181, 55, 23); + $this->assertEquals('b53717', $color->toHex()); + $this->assertEquals('#b53717', $color->toHex(true)); + + $color = new Color(181, 55, 23, .2); + $this->assertEquals('b5371733', $color->toHex()); + } + + public function testNormalize(): void + { + $color = new Color(255, 0, 51); + $this->assertEquals( + [1.0, 0.0, 0.2, 1.0], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + + $color = new Color(255, 0, 51, 1); + $this->assertEquals( + [1.0, 0.0, 0.2, 1.0], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + + $color = new Color(255, 0, 51, .2); + $this->assertEquals( + [1.0, 0.0, 0.2, .2], + array_map( + fn(ColorChannelInterface $channel): float => $channel->normalized(), + $color->channels(), + ) + ); + } + + public function testToString(): void + { + $color = new Color(181, 55, 23); + $this->assertEquals('rgb(181 55 23)', (string) $color); + + $color = new Color(181, 55, 23, 1); + $this->assertEquals('rgb(181 55 23)', (string) $color); + + $color = new Color(181, 55, 23, .2); + $this->assertEquals('rgb(181 55 23 / 0.2)', (string) $color); + + $color = new Color(181, 55, 23, 0); + $this->assertEquals('rgb(181 55 23 / 0)', (string) $color); + } + + public function testToColorspace(): void + { + $color = new Color(0, 0, 0); + $converted = $color->toColorspace(CmykColorspace::class); + $this->assertInstanceOf(CmykColor::class, $converted); + $this->assertEquals( + [0, 0, 0, 100, 255], + array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $converted->channels() + ) + ); + + $color = new Color(255, 255, 255); + $converted = $color->toColorspace(CmykColorspace::class); + $this->assertInstanceOf(CmykColor::class, $converted); + $this->assertEquals( + [0, 0, 0, 0, 255], + array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $converted->channels() + ) + ); + + $color = new Color(255, 0, 0); + $converted = $color->toColorspace(CmykColorspace::class); + $this->assertInstanceOf(CmykColor::class, $converted); + $this->assertEquals( + [0, 100, 100, 0, 255], + array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $converted->channels() + ) + ); + + $color = new Color(255, 0, 255); + $converted = $color->toColorspace(CmykColorspace::class); + $this->assertInstanceOf(CmykColor::class, $converted); + $this->assertEquals( + [0, 100, 0, 0, 255], + array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $converted->channels() + ) + ); + + $color = new Color(255, 255, 0); + $converted = $color->toColorspace(CmykColorspace::class); + $this->assertInstanceOf(CmykColor::class, $converted); + $this->assertEquals( + [0, 0, 100, 0, 255], + array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $converted->channels() + ) + ); + + $color = new Color(255, 204, 204); + $converted = $color->toColorspace(CmykColorspace::class); + $this->assertInstanceOf(CmykColor::class, $converted); + $this->assertEquals( + [0, 20, 20, 0, 255], + array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $converted->channels() + ) + ); + } + + public function testIsGrayscale(): void + { + $color = new Color(255, 0, 100); + $this->assertFalse($color->isGrayscale()); + + $color = new Color(50, 50, 50); + $this->assertTrue($color->isGrayscale()); + } + + public function testIsTransparent(): void + { + $color = new Color(255, 255, 255); + $this->assertFalse($color->isTransparent()); + + $color = new Color(255, 255, 255, 1); + $this->assertFalse($color->isTransparent()); + + $color = new Color(255, 255, 255, .5); + $this->assertTrue($color->isTransparent()); + + $color = new Color(255, 255, 255, 0); + $this->assertTrue($color->isTransparent()); + } + + public function testIsClear(): void + { + $color = new Color(255, 255, 255); + $this->assertFalse($color->isClear()); + + $color = new Color(255, 255, 255, 1); + $this->assertFalse($color->isClear()); + + $color = new Color(255, 255, 255, .1); + $this->assertFalse($color->isClear()); + + $color = new Color(255, 255, 255, 0); + $this->assertTrue($color->isClear()); + } + + public function testSetTransparency(): void + { + $color = new Color(0, 0, 0, 1); + $result = $color->withTransparency(.2); + $this->assertEquals(255, $color->channel(Alpha::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testDebugInfo(): void + { + $info = (new Color(10, 20, 30, .2))->__debugInfo(); + $this->assertEquals('10', $info['red']); + $this->assertEquals('20', $info['green']); + $this->assertEquals('30', $info['blue']); + $this->assertEquals('0.2', $info['alpha']); + } + + public function testCreateWithFourArgs(): void + { + $color = Color::create(10, 20, 30, .5); + $this->assertInstanceOf(Color::class, $color); + $this->assertEquals(10, $color->red()->value()); + $this->assertEquals(20, $color->green()->value()); + $this->assertEquals(30, $color->blue()->value()); + $this->assertEquals(128, $color->alpha()->value()); + } + + public function testToColorspaceFailsInvalidClass(): void + { + $color = new Color(0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->toColorspace('NonExistentColorspace'); + } + + public function testToColorspaceFailsNonColorspaceClass(): void + { + $color = new Color(0, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->toColorspace(\stdClass::class); + } + + public function testCloneDeepCopiesChannels(): void + { + $original = new Color(100, 150, 200); + $cloned = clone $original; + + $this->assertEquals(100, $original->red()->value()); + $this->assertEquals(100, $cloned->red()->value()); + + // Verify they are separate objects (deep clone) + $this->assertNotSame($original->red(), $cloned->red()); + } + + public function testConstructorWithChannelObjects(): void + { + $color = new Color(new Red(10), new Green(20), new Blue(30), new Alpha(.5)); + $this->assertEquals(10, $color->red()->value()); + $this->assertEquals(20, $color->green()->value()); + $this->assertEquals(30, $color->blue()->value()); + $this->assertEquals(128, $color->alpha()->value()); + } + + public function testWithBrightnessPositive(): void + { + $color = new Color(255, 0, 0); + $result = $color->withBrightness(50); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + + // Original should be unchanged (immutability) + $this->assertEquals(255, $color->channel(Red::class)->value()); + $this->assertEquals(0, $color->channel(Green::class)->value()); + $this->assertEquals(0, $color->channel(Blue::class)->value()); + + // Lightened result should have higher channel values overall + $originalChannels = array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $color->channels(), + ); + $resultChannels = array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $result->channels(), + ); + $this->assertGreaterThan( + $originalChannels[1] + $originalChannels[2], + $resultChannels[1] + $resultChannels[2], + ); + } + + public function testWithBrightnessZero(): void + { + $color = new Color(100, 150, 200); + $result = $color->withBrightness(0); + $this->assertInstanceOf(Color::class, $result); + + // Allow small rounding differences from colorspace roundtrip + $this->assertEqualsWithDelta(100, $result->channel(Red::class)->value(), 1); + $this->assertEqualsWithDelta(150, $result->channel(Green::class)->value(), 1); + $this->assertEqualsWithDelta(200, $result->channel(Blue::class)->value(), 1); + } + + public function testWithBrightnessPreservesAlpha(): void + { + $color = new Color(255, 0, 0, .5); + $result = $color->withBrightness(20); + $this->assertInstanceOf(Color::class, $result); + $this->assertEquals($color->channel(Alpha::class)->value(), $result->channel(Alpha::class)->value()); + } + + public function testWithBrightnessInvalidLevelAbove(): void + { + $color = new Color(255, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->withBrightness(101); + } + + public function testWithBrightnessInvalidLevelBelow(): void + { + $color = new Color(255, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->withBrightness(-101); + } + + public function testWithBrightnessNegative(): void + { + $color = new Color(255, 0, 0); + $result = $color->withBrightness(-50); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + + // Darkened red: red channel should be lower + $this->assertLessThan( + $color->channel(Red::class)->value(), + $result->channel(Red::class)->value(), + ); + } + + public function testWithSaturationPositive(): void + { + // Start with a desaturated color + $color = new Color(150, 100, 100); + $result = $color->withSaturation(50); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + + // Saturating should increase difference between max and min channels + $origDiff = $color->channel(Red::class)->value() - $color->channel(Green::class)->value(); + $resultDiff = $result->channel(Red::class)->value() - $result->channel(Green::class)->value(); + $this->assertGreaterThan($origDiff, $resultDiff); + } + + public function testWithSaturationInvalidLevelAbove(): void + { + $color = new Color(255, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->withSaturation(101); + } + + public function testWithSaturationInvalidLevelBelow(): void + { + $color = new Color(255, 0, 0); + $this->expectException(InvalidArgumentException::class); + $color->withSaturation(-101); + } + + public function testWithSaturationNegative(): void + { + $color = new Color(255, 0, 0); + $result = $color->withSaturation(-50); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + + // Desaturating should reduce difference between channels + $origDiff = $color->channel(Red::class)->value() - $color->channel(Green::class)->value(); + $resultDiff = $result->channel(Red::class)->value() - $result->channel(Green::class)->value(); + $this->assertLessThan($origDiff, $resultDiff); + } + + public function testWithSaturationFullNegative(): void + { + $color = new Color(255, 0, 0); + $result = $color->withSaturation(-100); + $this->assertInstanceOf(Color::class, $result); + + // Fully desaturated should be grayscale (R=G=B) + $this->assertEquals($result->channel(Red::class)->value(), $result->channel(Green::class)->value()); + $this->assertEquals($result->channel(Green::class)->value(), $result->channel(Blue::class)->value()); + } + + public function testCreateFailsInvalidArguments(): void + { + $this->expectException(InvalidArgumentException::class); + Color::create(1000, 2000, 1000); + } + + public function testInvert(): void + { + $color = new Color(255, 0, 0); + $result = $color->withInversion(); + $this->assertInstanceOf(Color::class, $result); + $this->assertNotSame($color, $result); + $this->assertEquals(0, $result->channel(Red::class)->value()); + $this->assertEquals(255, $result->channel(Green::class)->value()); + $this->assertEquals(255, $result->channel(Blue::class)->value()); + } + + public function testInvertBlack(): void + { + $color = new Color(0, 0, 0); + $result = $color->withInversion(); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(255, $result->channel(Green::class)->value()); + $this->assertEquals(255, $result->channel(Blue::class)->value()); + } + + public function testInvertWhite(): void + { + $color = new Color(255, 255, 255); + $result = $color->withInversion(); + $this->assertEquals(0, $result->channel(Red::class)->value()); + $this->assertEquals(0, $result->channel(Green::class)->value()); + $this->assertEquals(0, $result->channel(Blue::class)->value()); + } + + public function testInvertPreservesAlpha(): void + { + $color = new Color(255, 0, 0, .5); + $result = $color->withInversion(); + $this->assertEquals($color->channel(Alpha::class)->value(), $result->channel(Alpha::class)->value()); + } +} diff --git a/tests/Unit/Colors/Rgb/ColorspaceTest.php b/tests/Unit/Colors/Rgb/ColorspaceTest.php new file mode 100644 index 000000000..53bf841e0 --- /dev/null +++ b/tests/Unit/Colors/Rgb/ColorspaceTest.php @@ -0,0 +1,296 @@ +colorFromNormalized([1, 0, 1]); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(0, $result->channel(Green::class)->value()); + $this->assertEquals(255, $result->channel(Blue::class)->value()); + $this->assertEquals(255, $result->channel(Alpha::class)->value()); + + $result = $colorspace->colorFromNormalized([1, 0, 1, .2]); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(0, $result->channel(Green::class)->value()); + $this->assertEquals(255, $result->channel(Blue::class)->value()); + $this->assertEquals(51, $result->channel(Alpha::class)->value()); + } + + public function testImportCmykColor(): void + { + $colorspace = new Colorspace(); + $result = $colorspace->importColor(new CmykColor(0, 100, 0, 0)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(0, $result->channel(Green::class)->value()); + $this->assertEquals(255, $result->channel(Blue::class)->value()); + + $result = $colorspace->importColor(new CmykColor(0, 0, 0, 50)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(127, $result->channel(Red::class)->value()); + $this->assertEquals(127, $result->channel(Green::class)->value()); + $this->assertEquals(127, $result->channel(Blue::class)->value()); + } + + public function testImportHsvColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new HsvColor(300, 100, 100)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(0, $result->channel(Green::class)->value()); + $this->assertEquals(255, $result->channel(Blue::class)->value()); + + $result = $colorspace->importColor(new HsvColor(0, 0, 50)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(128, $result->channel(Red::class)->value()); + $this->assertEquals(128, $result->channel(Green::class)->value()); + $this->assertEquals(128, $result->channel(Blue::class)->value()); + } + + public function testImportHsvColorHueBranch1(): void + { + $colorspace = new Colorspace(); + + // hue=30 => normalized*6 < 1 => first match branch [chroma, x, 0] + $result = $colorspace->importColor(new HsvColor(30, 100, 100)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(128, $result->channel(Green::class)->value()); + $this->assertEquals(0, $result->channel(Blue::class)->value()); + } + + public function testImportHsvColorHueBranch2(): void + { + $colorspace = new Colorspace(); + + // hue=90 => normalized*6 < 2 => second match branch [x, chroma, 0] + $result = $colorspace->importColor(new HsvColor(90, 100, 100)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(128, $result->channel(Red::class)->value()); + $this->assertEquals(255, $result->channel(Green::class)->value()); + $this->assertEquals(0, $result->channel(Blue::class)->value()); + } + + public function testImportHsvColorHueBranch3(): void + { + $colorspace = new Colorspace(); + + // hue=150 => normalized*6 < 3 => third match branch [0, chroma, x] + $result = $colorspace->importColor(new HsvColor(150, 100, 100)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(0, $result->channel(Red::class)->value()); + $this->assertEquals(255, $result->channel(Green::class)->value()); + $this->assertEquals(128, $result->channel(Blue::class)->value()); + } + + public function testImportHsvColorHueBranch4(): void + { + $colorspace = new Colorspace(); + + // hue=210 => normalized*6 < 4 => fourth match branch [0, x, chroma] + $result = $colorspace->importColor(new HsvColor(210, 100, 100)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(0, $result->channel(Red::class)->value()); + $this->assertEquals(128, $result->channel(Green::class)->value()); + $this->assertEquals(255, $result->channel(Blue::class)->value()); + } + + public function testImportHsvColorHueBranch5(): void + { + $colorspace = new Colorspace(); + + // hue=270 => normalized*6 < 5 => fifth match branch [x, 0, chroma] + $result = $colorspace->importColor(new HsvColor(270, 100, 100)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(128, $result->channel(Red::class)->value()); + $this->assertEquals(0, $result->channel(Green::class)->value()); + $this->assertEquals(255, $result->channel(Blue::class)->value()); + } + + public function testImportHslColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new HslColor(300, 100, 50)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(0, $result->channel(Green::class)->value()); + $this->assertEquals(255, $result->channel(Blue::class)->value()); + + $result = $colorspace->importColor(new HslColor(0, 0, 50)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(128, $result->channel(Red::class)->value()); + $this->assertEquals(128, $result->channel(Green::class)->value()); + $this->assertEquals(128, $result->channel(Blue::class)->value()); + + $result = $colorspace->importColor(new HslColor(346, 100, 15)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(77, $result->channel(Red::class)->value()); + $this->assertEquals(0, $result->channel(Green::class)->value()); + $this->assertEquals(18, $result->channel(Blue::class)->value()); + } + + public function testImportHslColorHueBranch1(): void + { + $colorspace = new Colorspace(); + + // hue=30 => h < 1/6 => first match branch [c, x, 0] + $result = $colorspace->importColor(new HslColor(30, 100, 50)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(128, $result->channel(Green::class)->value()); + $this->assertEquals(0, $result->channel(Blue::class)->value()); + } + + public function testImportHslColorHueBranch2(): void + { + $colorspace = new Colorspace(); + + // hue=90 => h < 2/6 => second match branch [x, c, 0] + $result = $colorspace->importColor(new HslColor(90, 100, 50)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(128, $result->channel(Red::class)->value()); + $this->assertEquals(255, $result->channel(Green::class)->value()); + $this->assertEquals(0, $result->channel(Blue::class)->value()); + } + + public function testImportHslColorHueBranch3(): void + { + $colorspace = new Colorspace(); + + // hue=150 => h < 3/6 => third match branch [0, c, x] + $result = $colorspace->importColor(new HslColor(150, 100, 50)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(0, $result->channel(Red::class)->value()); + $this->assertEquals(255, $result->channel(Green::class)->value()); + $this->assertEquals(128, $result->channel(Blue::class)->value()); + } + + public function testImportHslColorHueBranch4(): void + { + $colorspace = new Colorspace(); + + // hue=210 => h < 4/6 => fourth match branch [0, x, c] + $result = $colorspace->importColor(new HslColor(210, 100, 50)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(0, $result->channel(Red::class)->value()); + $this->assertEquals(128, $result->channel(Green::class)->value()); + $this->assertEquals(255, $result->channel(Blue::class)->value()); + } + + public function testImportHslColorHueBranch5(): void + { + $colorspace = new Colorspace(); + + // hue=270 => h < 5/6 => fifth match branch [x, 0, c] + $result = $colorspace->importColor(new HslColor(270, 100, 50)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(128, $result->channel(Red::class)->value()); + $this->assertEquals(0, $result->channel(Green::class)->value()); + $this->assertEquals(255, $result->channel(Blue::class)->value()); + } + + public function testImportNamedColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(NamedColor::STEELBLUE); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEquals(70, $result->channel(Red::class)->value()); + $this->assertEquals(130, $result->channel(Green::class)->value()); + $this->assertEquals(180, $result->channel(Blue::class)->value()); + } + + public function testImportOklabColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new OklabColor(0.68, 0.17, 0.14)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEqualsWithDelta(255, $result->channel(Red::class)->value(), 2); + $this->assertEqualsWithDelta(85, $result->channel(Green::class)->value(), 2); + $this->assertEqualsWithDelta(0, $result->channel(Blue::class)->value(), 2); + } + + public function testImportOklchColor(): void + { + $colorspace = new Colorspace(); + + $result = $colorspace->importColor(new OklchColor(0.68, 0.22, 38.8)); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertEqualsWithDelta(255, $result->channel(Red::class)->value(), 2); + $this->assertEqualsWithDelta(83, $result->channel(Green::class)->value(), 2); + $this->assertEqualsWithDelta(0, $result->channel(Blue::class)->value(), 2); + } + + public function testImportRgbColorPassthrough(): void + { + $colorspace = new Colorspace(); + + $color = new RgbColor(100, 150, 200); + $result = $colorspace->importColor($color); + $this->assertInstanceOf(RgbColor::class, $result); + $this->assertSame($color, $result); + } + + public function testColorFromNormalizedInvalidChannelCount(): void + { + $this->expectException(InvalidArgumentException::class); + Colorspace::colorFromNormalized([0.5, 0.5]); + } + + public function testColorFromNormalizedWithNullValue(): void + { + $this->expectException(InvalidArgumentException::class); + Colorspace::colorFromNormalized([0.5, null, 0.5]); + } + + public function testImportUnsupportedColor(): void + { + $this->expectException(ColorException::class); + $colorspace = new Colorspace(); + $colorspace->importColor(Mockery::mock(ColorInterface::class)); + } + + public function testChannels(): void + { + $channels = Colorspace::channels(); + $this->assertIsArray($channels); + $this->assertCount(4, $channels); + $this->assertEquals(Red::class, $channels[0]); + $this->assertEquals(Green::class, $channels[1]); + $this->assertEquals(Blue::class, $channels[2]); + $this->assertEquals(Alpha::class, $channels[3]); + } +} diff --git a/tests/Unit/Colors/Rgb/Decoders/HexColorDecoderTest.php b/tests/Unit/Colors/Rgb/Decoders/HexColorDecoderTest.php new file mode 100644 index 000000000..43333be6d --- /dev/null +++ b/tests/Unit/Colors/Rgb/Decoders/HexColorDecoderTest.php @@ -0,0 +1,208 @@ + + */ + #[DataProviderExternal(ColorDataProvider::class, 'rgbHex')] + public function testDecode(mixed $input, array $channelValues): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode($input[0]); + $this->assertEquals( + $channelValues, + array_map(fn(ColorChannelInterface $channel): int => $channel->value(), $result->channels()), + ); + } + + public function testSupportsString(): void + { + $decoder = new HexColorDecoder(); + $this->assertTrue($decoder->supports('#fff')); + $this->assertTrue($decoder->supports('#ffffff')); + $this->assertTrue($decoder->supports('#ffff')); + $this->assertTrue($decoder->supports('#ffffffff')); + $this->assertTrue($decoder->supports('fff')); + $this->assertTrue($decoder->supports('ffffff')); + $this->assertTrue($decoder->supports('FF0000')); + $this->assertTrue($decoder->supports('#FF0000')); + } + + public function testSupportsNonString(): void + { + $decoder = new HexColorDecoder(); + $this->assertFalse($decoder->supports(123)); + $this->assertFalse($decoder->supports(null)); + $this->assertFalse($decoder->supports([])); + $this->assertFalse($decoder->supports(1.5)); + $this->assertFalse($decoder->supports(true)); + $this->assertFalse($decoder->supports(false)); + $this->assertFalse($decoder->supports(new \stdClass())); + } + + public function testSupportsEmptyString(): void + { + $decoder = new HexColorDecoder(); + $this->assertFalse($decoder->supports('')); + } + + public function testSupportsInvalidStrings(): void + { + $decoder = new HexColorDecoder(); + $this->assertFalse($decoder->supports('xyz')); + $this->assertFalse($decoder->supports('not-a-color')); + $this->assertFalse($decoder->supports('gggggg')); + $this->assertFalse($decoder->supports('aabbccdde')); + $this->assertFalse($decoder->supports('aabbccddee')); + } + + public function testDecodeThreeCharHex(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('#f00'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([255, 0, 0, 255], $channels); + } + + public function testDecodeFourCharHex(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('#f008'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([255, 0, 0, 136], $channels); + } + + public function testDecodeSixCharHex(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('#ff0000'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([255, 0, 0, 255], $channels); + } + + public function testDecodeEightCharHex(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('#ff000080'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([255, 0, 0, 128], $channels); + } + + public function testDecodeWithoutHash(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('ff0000'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([255, 0, 0, 255], $channels); + } + + public function testDecodeThreeCharWithoutHash(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('f00'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([255, 0, 0, 255], $channels); + } + + public function testDecodeFourCharWithoutHash(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('f008'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([255, 0, 0, 136], $channels); + } + + public function testDecodeEightCharWithoutHash(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('ff000080'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([255, 0, 0, 128], $channels); + } + + public function testDecodeCaseInsensitive(): void + { + $decoder = new HexColorDecoder(); + $lower = $decoder->decode('#ff0000'); + $upper = $decoder->decode('#FF0000'); + $lowerChannels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $lower->channels()); + $upperChannels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $upper->channels()); + $this->assertEquals($lowerChannels, $upperChannels); + } + + public function testDecodeBlack(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('#000000'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([0, 0, 0, 255], $channels); + } + + public function testDecodeWhite(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('#ffffff'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([255, 255, 255, 255], $channels); + } + + public function testDecodeFullTransparent(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('#00000000'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([0, 0, 0, 0], $channels); + } + + public function testDecodeShorthandBlack(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('#000'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([0, 0, 0, 255], $channels); + } + + public function testDecodeShorthandWhite(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('#fff'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([255, 255, 255, 255], $channels); + } + + public function testDecodeShorthandWithAlpha(): void + { + $decoder = new HexColorDecoder(); + $result = $decoder->decode('#0000'); + $channels = array_map(fn(ColorChannelInterface $c): int => $c->value(), $result->channels()); + $this->assertEquals([0, 0, 0, 0], $channels); + } + + public function testDecodeInvalid(): void + { + $decoder = new HexColorDecoder(); + $this->expectException(InvalidArgumentException::class); + $decoder->decode('xyz-not-hex'); + } + + public function testDecodeInvalidShortString(): void + { + $decoder = new HexColorDecoder(); + $this->expectException(InvalidArgumentException::class); + $decoder->decode('zz'); + } +} diff --git a/tests/Unit/Colors/Rgb/Decoders/NamedColorDecoderTest.php b/tests/Unit/Colors/Rgb/Decoders/NamedColorDecoderTest.php new file mode 100644 index 000000000..502475476 --- /dev/null +++ b/tests/Unit/Colors/Rgb/Decoders/NamedColorDecoderTest.php @@ -0,0 +1,61 @@ + + */ + #[DataProviderExternal(ColorDataProvider::class, 'rgbNamedColor')] + public function testDecode(mixed $input, array $channelValues): void + { + $decoder = new NamedColorDecoder(); + $result = $decoder->decode($input[0]); + $this->assertEquals( + $channelValues, + array_map( + fn(ColorChannelInterface $channel): int|float => $channel->value(), + $result->channels() + ) + ); + } + + public function testSupportsString(): void + { + $decoder = new NamedColorDecoder(); + $this->assertTrue($decoder->supports('red')); + $this->assertTrue($decoder->supports('blue')); + $this->assertTrue($decoder->supports('green')); + $this->assertTrue($decoder->supports('white')); + $this->assertTrue($decoder->supports('black')); + $this->assertTrue($decoder->supports('RED')); + $this->assertTrue($decoder->supports('Red')); + } + + public function testSupportsNonString(): void + { + $decoder = new NamedColorDecoder(); + $this->assertFalse($decoder->supports(123)); + $this->assertFalse($decoder->supports(null)); + $this->assertFalse($decoder->supports([])); + } + + public function testSupportsInvalidString(): void + { + $decoder = new NamedColorDecoder(); + $this->assertFalse($decoder->supports('not-a-color-name')); + $this->assertFalse($decoder->supports('#fff')); + $this->assertFalse($decoder->supports('rgb(0,0,0)')); + } +} diff --git a/tests/Unit/Colors/Rgb/Decoders/StringColorDecoderTest.php b/tests/Unit/Colors/Rgb/Decoders/StringColorDecoderTest.php new file mode 100644 index 000000000..feb57633c --- /dev/null +++ b/tests/Unit/Colors/Rgb/Decoders/StringColorDecoderTest.php @@ -0,0 +1,71 @@ + + */ + #[DataProviderExternal(ColorDataProvider::class, 'rgbString')] + public function testDecode(mixed $input, array $channelValues): void + { + $decoder = new StringColorDecoder(); + $result = $decoder->decode($input[0]); + $this->assertEquals( + $channelValues, + array_map( + fn(ColorChannelInterface $channel): int => $channel->value(), + $result->channels(), + ), + ); + } + + /** + * @param $channelValues array + */ + #[DataProviderExternal(ColorDataProvider::class, 'rgbStringInvalid')] + public function testDecodeInvalid(string $input): void + { + $decoder = new StringColorDecoder(); + $this->expectException(InvalidArgumentException::class); + $decoder->decode($input); + } + + public function testSupportsString(): void + { + $decoder = new StringColorDecoder(); + $this->assertTrue($decoder->supports('rgb(255, 0, 0)')); + $this->assertTrue($decoder->supports('rgba(255, 0, 0, 1)')); + $this->assertTrue($decoder->supports('srgb(255, 0, 0)')); + $this->assertTrue($decoder->supports('srgba(255, 0, 0, 1)')); + $this->assertTrue($decoder->supports('RGB(255, 0, 0)')); + } + + public function testSupportsNonString(): void + { + $decoder = new StringColorDecoder(); + $this->assertFalse($decoder->supports(123)); + $this->assertFalse($decoder->supports(null)); + $this->assertFalse($decoder->supports([])); + } + + public function testSupportsInvalidString(): void + { + $decoder = new StringColorDecoder(); + $this->assertFalse($decoder->supports('not-a-color')); + $this->assertFalse($decoder->supports('#fff')); + $this->assertFalse($decoder->supports('hsl(0, 100%, 50%)')); + } +} diff --git a/tests/Unit/Colors/Rgb/NamedColorTest.php b/tests/Unit/Colors/Rgb/NamedColorTest.php new file mode 100644 index 000000000..68cebbe77 --- /dev/null +++ b/tests/Unit/Colors/Rgb/NamedColorTest.php @@ -0,0 +1,143 @@ + + */ + #[DataProviderExternal(ColorDataProvider::class, 'rgbNamedColor')] + public function testCreate(mixed $input, array $channels): void + { + $color = NamedColor::create(...$input); + $this->assertInstanceOf(NamedColor::class, $color); + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), $color->channels()), + ); + } + + /** + * @param $channels array + */ + #[DataProviderExternal(ColorDataProvider::class, 'rgbNamedColor')] + public function testTryCreate(mixed $input, array $channels): void + { + $color = NamedColor::tryCreate(...$input); + $this->assertInstanceOf(NamedColor::class, $color); + $this->assertEquals( + $channels, + array_map(fn(ColorChannelInterface $channel): int|float => + $channel->value(), $color->channels()), + ); + } + + public function testTryCreateInvalid(): void + { + $this->assertNull(NamedColor::tryCreate('foo')); + } + + public function testColorspace(): void + { + $this->assertInstanceOf(Rgb::class, NamedColor::STEELBLUE->colorspace()); + } + + public function testToString(): void + { + $this->assertEquals('steelblue', NamedColor::STEELBLUE->toString()); + $this->assertEquals('aliceblue', NamedColor::ALICEBLUE->toString()); + } + + public function testToHex(): void + { + $this->assertEquals('4682b4', NamedColor::STEELBLUE->toHex()); + $this->assertEquals('f0f8ff', NamedColor::ALICEBLUE->toHex()); + $this->assertEquals('#4682b4', NamedColor::STEELBLUE->toHex(true)); + $this->assertEquals('#f0f8ff', NamedColor::ALICEBLUE->toHex(true)); + } + + public function testChannels(): void + { + $this->assertIsArray(NamedColor::STEELBLUE->channels()); + foreach (NamedColor::STEELBLUE->channels() as $channel) { + $this->assertInstanceOf(ColorChannelInterface::class, $channel); + } + } + + public function testChannel(): void + { + $this->assertInstanceOf(Red::class, NamedColor::STEELBLUE->channel(Red::class)); + $this->assertInstanceOf(Green::class, NamedColor::STEELBLUE->channel(Green::class)); + $this->assertInstanceOf(Blue::class, NamedColor::STEELBLUE->channel(Blue::class)); + $this->assertInstanceOf(Alpha::class, NamedColor::STEELBLUE->channel(Alpha::class)); + + $this->assertEquals(70, NamedColor::STEELBLUE->channel(Red::class)->value()); + $this->assertEquals(130, NamedColor::STEELBLUE->channel(Green::class)->value()); + $this->assertEquals(180, NamedColor::STEELBLUE->channel(Blue::class)->value()); + $this->assertEquals(255, NamedColor::STEELBLUE->channel(Alpha::class)->value()); + } + + public function testAlpha(): void + { + $this->assertInstanceOf(Alpha::class, NamedColor::STEELBLUE->alpha()); + $this->assertEquals(255, NamedColor::STEELBLUE->alpha()->value()); + } + + public function testToColorspace(): void + { + $this->assertInstanceOf(CmykColor::class, NamedColor::STEELBLUE->toColorspace(Cmyk::class)); + $this->assertInstanceOf(HslColor::class, NamedColor::STEELBLUE->toColorspace(Hsl::class)); + } + + public function testIsGrayscale(): void + { + $this->assertFalse(NamedColor::STEELBLUE->isGrayscale()); + $this->assertFalse(NamedColor::FUCHSIA->isGrayscale()); + $this->assertTrue(NamedColor::DIMGRAY->isGrayscale()); + $this->assertTrue(NamedColor::GRAY->isGrayscale()); + } + + public function testIsTransparent(): void + { + $this->assertFalse(NamedColor::STEELBLUE->isTransparent()); + $this->assertFalse(NamedColor::FUCHSIA->isTransparent()); + $this->assertFalse(NamedColor::DIMGRAY->isTransparent()); + $this->assertFalse(NamedColor::GRAY->isTransparent()); + } + + public function testIsClear(): void + { + $this->assertFalse(NamedColor::STEELBLUE->isClear()); + $this->assertFalse(NamedColor::FUCHSIA->isClear()); + $this->assertFalse(NamedColor::DIMGRAY->isClear()); + $this->assertFalse(NamedColor::GRAY->isClear()); + } + + public function testWithTransparency(): void + { + $color = NamedColor::STEELBLUE; + $this->assertEquals(255, $color->alpha()->value()); + $this->assertEquals(51, $color->withTransparency(.2)->alpha()->value()); + } +} diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php new file mode 100644 index 000000000..313eb756d --- /dev/null +++ b/tests/Unit/ConfigTest.php @@ -0,0 +1,117 @@ +assertInstanceOf(Config::class, $config); + + $this->assertTrue($config->autoOrientation); + $this->assertTrue($config->decodeAnimation); + $this->assertEquals('ffffff', $config->backgroundColor); + + $config = new Config( + autoOrientation: false, + decodeAnimation: false, + backgroundColor: 'f00', + strip: true, + ); + $this->assertInstanceOf(Config::class, $config); + + $this->assertFalse($config->autoOrientation); + $this->assertFalse($config->decodeAnimation); + $this->assertTrue($config->strip); + $this->assertEquals('f00', $config->backgroundColor); + } + + public function testGetSetOptions(): void + { + $config = new Config(); + $this->assertTrue($config->autoOrientation); + $this->assertTrue($config->decodeAnimation); + $this->assertFalse($config->strip); + $this->assertEquals('ffffff', $config->backgroundColor); + + $result = $config->setOptions( + autoOrientation: false, + decodeAnimation: false, + backgroundColor: 'f00', + strip: true, + ); + + $this->assertFalse($config->autoOrientation); + $this->assertFalse($config->decodeAnimation); + $this->assertEquals('f00', $config->backgroundColor); + + $this->assertFalse($result->autoOrientation); + $this->assertFalse($result->decodeAnimation); + $this->assertTrue($result->strip); + $this->assertEquals('f00', $result->backgroundColor); + + $result = $config->setOptions(backgroundColor: '000'); + + $this->assertFalse($config->autoOrientation); + $this->assertFalse($config->decodeAnimation); + $this->assertTrue($config->strip); + $this->assertEquals('000', $config->backgroundColor); + + $this->assertFalse($result->autoOrientation); + $this->assertFalse($result->decodeAnimation); + $this->assertTrue($result->strip); + $this->assertEquals('000', $result->backgroundColor); + } + + public function testSetOptionsWithArray(): void + { + $config = new Config(); + $result = $config->setOptions([ + 'autoOrientation' => false, + 'decodeAnimation' => false, + 'backgroundColor' => 'f00', + 'strip' => true, + ]); + + $this->assertFalse($config->autoOrientation); + $this->assertFalse($config->decodeAnimation); + $this->assertTrue($config->strip); + $this->assertEquals('f00', $config->backgroundColor); + $this->assertFalse($result->autoOrientation); + $this->assertFalse($result->decodeAnimation); + $this->assertTrue($result->strip); + $this->assertEquals('f00', $result->backgroundColor); + } + + public function testSetOptionsInvalidProperty(): void + { + $config = new Config(); + $this->expectException(InvalidArgumentException::class); + $config->setOptions(nonExistent: 'value'); + } + + public function testSetOptionsEmpty(): void + { + $config = new Config(); + $result = $config->setOptions(); + $this->assertSame($config, $result); + $this->assertTrue($config->autoOrientation); + $this->assertTrue($config->decodeAnimation); + } + + public function testSetOptionsWithSingleNonArrayPositionalArg(): void + { + $config = new Config(); + $this->expectException(\TypeError::class); + $config->setOptions('someValue'); + } +} diff --git a/tests/Unit/DataUriTest.php b/tests/Unit/DataUriTest.php new file mode 100644 index 000000000..756bc941b --- /dev/null +++ b/tests/Unit/DataUriTest.php @@ -0,0 +1,241 @@ +assertEquals('test', $datauri->data()); + $datauri->setData('foo'); + $this->assertEquals('foo', $datauri->data()); + } + + #[DataProvider('getSetMediaTypeDataProvider')] + public function testGetSetMediaType(mixed $inputMediaType, ?string $resultMediaType): void + { + $datauri = new DataUri(mediaType: $inputMediaType); + $this->assertEquals($resultMediaType, $datauri->mediaType()); + $datauri->setMediaType(null); + $this->assertNull($datauri->mediaType()); + } + + public static function getSetMediaTypeDataProvider(): Generator + { + yield [null, null]; + yield ['', null]; + yield ['image/jpeg', 'image/jpeg']; + yield ['image/gif', 'image/gif']; + yield [MediaType::IMAGE_AVIF, 'image/avif']; + } + + public function testSetGetParameters(): void + { + $datauri = new DataUri(); + $this->assertEquals([], $datauri->parameters()); + $datauri->setParameters(['foo' => 'bar']); + $this->assertEquals(['foo' => 'bar'], $datauri->parameters()); + $datauri->setParameters(['bar' => 'baz', 'test' => 123]); + $this->assertEquals(['bar' => 'baz', 'test' => '123'], $datauri->parameters()); + $datauri->setParameter('test', '456'); + $this->assertEquals(['bar' => 'baz', 'test' => '456'], $datauri->parameters()); + $datauri->appendParameters(['bar' => 'foobar', 'append' => 'ok']); + $this->assertEquals(['bar' => 'foobar', 'test' => '456', 'append' => 'ok'], $datauri->parameters()); + $this->assertEquals('foobar', $datauri->parameter('bar')); + $this->assertEquals('456', $datauri->parameter('test')); + $this->assertEquals('ok', $datauri->parameter('append')); + $this->assertEquals(null, $datauri->parameter('none')); + $datauri->setCharset('utf-8'); + $this->assertEquals('utf-8', $datauri->charset()); + $this->assertEquals([ + 'bar' => 'foobar', + 'test' => '456', + 'append' => 'ok', + 'charset' => 'utf-8', + ], $datauri->parameters()); + } + + /** + * @param array $parameters + */ + #[DataProvider('toStringDataProvider')] + public function testToString( + string $data, + null|string|MediaType $mediaType, + array $parameters, + bool $base64, + string $result, + ): void { + $datauri = new DataUri($data, $mediaType, $parameters, $base64); + $this->assertEquals($result, $datauri->toString()); + $this->assertEquals($result, (string) $datauri); + } + + /** + * @param array $parameters + */ + #[DataProvider('toStringDataProvider')] + public function testCreateStaticFactory( + string $data, + ?string $mediaType, + array $parameters, + bool $base64, + string $result, + ): void { + $datauri = DataUri::create($data, $mediaType, $parameters, $base64); + $this->assertInstanceOf(DataUri::class, $datauri); + $this->assertEquals($data, $datauri->data()); + $this->assertEquals($mediaType, $datauri->mediaType()); + $this->assertEquals($parameters, $datauri->parameters()); + $this->assertEquals($result, (string) $datauri); + } + + /** + * @param array $parameters + */ + #[DataProvider('toStringDataProvider')] + public function testCreateParse( + string $data, + ?string $mediaType, + array $parameters, + bool $base64, + string $result, + ): void { + $this->assertEquals($result, DataUri::create($data, $mediaType, $parameters, $base64)->toString()); + } + + public static function toStringDataProvider(): Generator + { + yield [ + '', + null, + [], + false, + 'data:,' + ]; + + yield [ + 'foo', + null, + [], + false, + 'data:,foo' + ]; + + yield [ + 'foo', + 'text/plain', + [], + false, + 'data:text/plain,foo' + ]; + + yield [ + 'foo', + 'text/plain', + ['charset' => 'utf-8'], + false, + 'data:text/plain;charset=utf-8,foo' + ]; + + yield [ + 'foo', + 'text/plain', + ['charset' => 'utf-8'], + true, + 'data:text/plain;charset=utf-8;base64,Zm9v' + ]; + + yield [ + 'hello', + 'text/plain', + ['charset' => 'utf-8'], + true, + 'data:text/plain;charset=utf-8;base64,aGVsbG8=', + ]; + + yield [ + 'hello', + 'text/plain', + [], + false, + 'data:text/plain,hello', + ]; + } + + #[DataProviderExternal(DataUriDataProvider::class, 'validDataUris')] + public function testParse(string $dataUriScheme, string $resultData): void + { + $datauri = DataUri::parse($dataUriScheme); + $this->assertInstanceOf(DataUri::class, $datauri); + $this->assertEquals($resultData, $datauri->data()); + } + + #[DataProviderExternal(DataUriDataProvider::class, 'invalidDataUris')] + public function testParseInvalid(string $input, string $exception): void + { + $this->expectException($exception); + DataUri::parse($input); + } + + public function testCreateStaticFactoryMinimal(): void + { + $datauri = DataUri::create('data'); + $this->assertEquals('data', $datauri->data()); + $this->assertNull($datauri->mediaType()); + $this->assertEquals([], $datauri->parameters()); + } + + public function testCreateStaticFactoryWithMediaTypeEnum(): void + { + $datauri = DataUri::create('data', MediaType::IMAGE_PNG); + $this->assertEquals('image/png', $datauri->mediaType()); + } + + public function testCreateBase64EncodedStaticFactory(): void + { + $datauri = DataUri::create('hello', 'text/plain', ['charset' => 'utf-8'], base64: true); + $this->assertInstanceOf(DataUri::class, $datauri); + $this->assertEquals('data:text/plain;charset=utf-8;base64,aGVsbG8=', $datauri->toString()); + $this->assertEquals('hello', $datauri->data()); + $this->assertEquals('text/plain', $datauri->mediaType()); + $this->assertEquals(['charset' => 'utf-8'], $datauri->parameters()); + } + + public function testCreateBase64EncodedMinimal(): void + { + $datauri = DataUri::create('test123', base64: true); + $this->assertEquals('data:;base64,dGVzdDEyMw==', $datauri->toString()); + $this->assertEquals('test123', $datauri->data()); + $this->assertNull($datauri->mediaType()); + } + + public function testDebugInfo(): void + { + $datauri = new DataUri('test-data', 'image/jpeg'); + $debug = $datauri->__debugInfo(); + $this->assertArrayHasKey('mediaType', $debug); + $this->assertArrayHasKey('size', $debug); + $this->assertEquals('image/jpeg', $debug['mediaType']); + $this->assertEquals(9, $debug['size']); + } + + public function testDebugInfoWithoutMediaType(): void + { + $datauri = new DataUri('abc'); + $debug = $datauri->__debugInfo(); + $this->assertNull($debug['mediaType']); + $this->assertEquals(3, $debug['size']); + } +} diff --git a/tests/Unit/Decoders/ColorObjectDecoderTest.php b/tests/Unit/Decoders/ColorObjectDecoderTest.php new file mode 100644 index 000000000..e5a03cc1f --- /dev/null +++ b/tests/Unit/Decoders/ColorObjectDecoderTest.php @@ -0,0 +1,62 @@ +assertTrue($decoder->supports($color)); + } + + public function testSupportsNonColorObject(): void + { + $decoder = new ColorObjectDecoder(); + $this->assertFalse($decoder->supports('not a color')); + $this->assertFalse($decoder->supports(123)); + $this->assertFalse($decoder->supports(null)); + $this->assertFalse($decoder->supports(new \stdClass())); + } + + public function testDecode(): void + { + $decoder = new ColorObjectDecoder(); + $color = new RgbColor( + new Red(255), + new Green(0), + new Blue(0), + new Alpha(1) + ); + $result = $decoder->decode($color); + $this->assertInstanceOf(ColorInterface::class, $result); + $this->assertSame($color, $result); + } + + public function testDecodeInvalid(): void + { + $decoder = new ColorObjectDecoder(); + $this->expectException(InvalidArgumentException::class); + $decoder->decode('not a color object'); + } +} diff --git a/tests/Unit/Drivers/AbstractDecoderTest.php b/tests/Unit/Drivers/AbstractDecoderTest.php new file mode 100644 index 000000000..709620681 --- /dev/null +++ b/tests/Unit/Drivers/AbstractDecoderTest.php @@ -0,0 +1,173 @@ +assertFalse($decoder->isGifFormat(Resource::create('exif.jpg')->data())); + $this->assertTrue($decoder->isGifFormat(Resource::create('red.gif')->data())); + } + + public function testExtractExifDataFromBinary(): void + { + $resource = Resource::create('exif.jpg'); + $source = $resource->data(); + $stream = $resource->stream(); + $decoder = Mockery::mock(AbstractDecoder::class); + $decoder->shouldReceive('buildStreamOrFail')->with($source)->andReturn($stream); + $result = $decoder->extractExifData($source); + $this->assertInstanceOf(CollectionInterface::class, $result); + $this->assertEquals('Oliver Vogel', $result->get('IFD0.Artist')); + } + + public function testExtractExifDataFromPath(): void + { + $decoder = Mockery::mock(AbstractDecoder::class); + $result = $decoder->extractExifData(Resource::create('exif.jpg')->path()); + $this->assertInstanceOf(CollectionInterface::class, $result); + $this->assertEquals('Oliver Vogel', $result->get('IFD0.Artist')); + } + + public function testIsValidBase64(): void + { + $decoder = new class () extends AbstractDecoder + { + public function supports(mixed $input): bool + { + return true; + } + + public function isValid(mixed $input): bool + { + try { + parent::decodeBase64Data($input); + } catch (Throwable) { + return false; + } + return true; + } + + public function decode(mixed $input): ImageInterface|ColorInterface + { + throw new Exception(''); + } + }; + + $this->assertTrue( + $decoder->isValid('R0lGODdhAwADAKIAAAQyrKTy/ByS7AQytLT2/AAAAAAAAAAAACwAAAAAAwADAAADBhgU0gMgAQA7') + ); + + $this->assertFalse( + $decoder->isValid('foo') + ); + + $this->assertFalse( + $decoder->isValid(new stdClass()) + ); + } + + public function testExtractExifDataFromInvalidInput(): void + { + $decoder = Mockery::mock(AbstractDecoder::class); + // Input that fails both readableFilePathOrFail and buildStreamOrFail + // This triggers the RuntimeException catch, returning empty Collection + $decoder->shouldReceive('buildStreamOrFail') + ->andThrow(new FilesystemException('not valid')); + $result = $decoder->extractExifData('not-a-file-and-not-binary'); + $this->assertInstanceOf(CollectionInterface::class, $result); + $this->assertEquals(0, $result->count()); + } + + public function testExtractExifDataFromNonExifImage(): void + { + // Use a file that exists but has no EXIF data (GIF) + $decoder = Mockery::mock(AbstractDecoder::class); + $result = $decoder->extractExifData(Resource::create('red.gif')->path()); + $this->assertInstanceOf(CollectionInterface::class, $result); + } + + public function testDecodeBase64DataWithStrictFalse(): void + { + // Test where base64_decode returns false with strict mode + // Characters like "!" are not valid base64 with strict=true + $decoder = new class () extends AbstractDecoder + { + public function supports(mixed $input): bool + { + return true; + } + + public function callDecodeBase64Data(mixed $input): string + { + return parent::decodeBase64Data($input); + } + + public function decode(mixed $input): ImageInterface|ColorInterface + { + throw new Exception(''); + } + }; + + $this->expectException(DecoderException::class); + $decoder->callDecodeBase64Data('!!!invalid-base64!!!'); + } + + #[DataProvider('pathDataProvider')] + public function testResolveFilePath(bool $valid, string $path): void + { + $decoder = new class () extends AbstractDecoder + { + public function supports(mixed $input): bool + { + return true; + } + + public function decode(mixed $input): ImageInterface|ColorInterface + { + throw new Exception(''); + } + + public function checkValidityResult(string $path, bool $result): bool + { + try { + self::readableFilePathOrFail($path); + } catch (Throwable) { + return $result === false; + } + + return $result === true; + } + }; + + $this->assertTrue($decoder->checkValidityResult($path, $valid)); + } + + public static function pathDataProvider(): Generator + { + yield [true, Resource::create()->path()]; + yield [false, 'foo']; + yield [false, 'foo/bar']; + } +} diff --git a/tests/Unit/Drivers/AbstractDriverTest.php b/tests/Unit/Drivers/AbstractDriverTest.php new file mode 100644 index 000000000..9291b7d0c --- /dev/null +++ b/tests/Unit/Drivers/AbstractDriverTest.php @@ -0,0 +1,126 @@ +assertSame($config, $driver->config()); + } + + public function testConfigDefault(): void + { + $driver = new GdDriver(); + $this->assertInstanceOf(Config::class, $driver->config()); + } + + public function testHandleImageInputFailsWithEmptyDecoders(): void + { + $driver = new GdDriver(); + $this->expectException(InvalidArgumentException::class); + $driver->decodeImage('test', []); + } + + public function testHandleImageInputFailsWithUnsupportedInput(): void + { + $driver = new GdDriver(); + $this->expectException(InvalidArgumentException::class); + $driver->decodeImage(12345); + } + + public function testHandleColorInputFailsWithEmptyDecoders(): void + { + $driver = new GdDriver(); + $this->expectException(InvalidArgumentException::class); + $driver->decodeColor('test', []); + } + + public function testHandleColorInputFailsWithUnsupportedInput(): void + { + $driver = new GdDriver(); + $this->expectException(ColorDecoderException::class); + $driver->decodeColor(new \stdClass()); + } + + public function testSpecializeModifier(): void + { + $driver = new GdDriver(); + $modifier = new BlurModifier(5); + $result = $driver->specializeModifier($modifier); + $this->assertInstanceOf(ModifierInterface::class, $result); + } + + public function testSpecializeAnalyzer(): void + { + $driver = new GdDriver(); + $analyzer = new WidthAnalyzer(); + $result = $driver->specializeAnalyzer($analyzer); + $this->assertNotNull($result); + } + + public function testSpecializeEncoder(): void + { + $driver = new GdDriver(); + $encoder = new PngEncoder(); + $result = $driver->specializeEncoder($encoder); + $this->assertNotNull($result); + } + + public function testSpecializeDecoder(): void + { + $driver = new GdDriver(); + $decoder = new BinaryImageDecoder(); + $result = $driver->specializeDecoder($decoder); + $this->assertNotNull($result); + } + + public function testSpecializeModifierNotSupported(): void + { + $driver = new GdDriver(); + $modifier = new class () extends \Intervention\Image\Drivers\SpecializableModifier { + }; + $this->expectException(NotSupportedException::class); + $driver->specializeModifier($modifier); + } + + public function testSpecializeNonSpecializableModifier(): void + { + $driver = new GdDriver(); + $modifier = new class () implements ModifierInterface { + public function apply(ImageInterface $image): ImageInterface + { + return $image; + } + }; + $result = $driver->specializeModifier($modifier); + $this->assertSame($modifier, $result); + } + + public function testHandleColorInput(): void + { + $driver = new GdDriver(); + $result = $driver->decodeColor('ff0000'); + $this->assertNotNull($result); + } +} diff --git a/tests/Unit/Drivers/AbstractEncoderTest.php b/tests/Unit/Drivers/AbstractEncoderTest.php new file mode 100644 index 000000000..56c0944ca --- /dev/null +++ b/tests/Unit/Drivers/AbstractEncoderTest.php @@ -0,0 +1,117 @@ +makePartial(); + $image = Mockery::mock(ImageInterface::class); + $encoded = Mockery::mock(EncodedImage::class); + $image->shouldReceive('encode')->andReturn($encoded); + $result = $encoder->encode($image); + $this->assertInstanceOf(EncodedImage::class, $result); + } + + public function testEncodeThrowsWhenSpecializedWithoutOverride(): void + { + $encoder = new class () extends AbstractEncoder implements SpecializedInterface { + }; + + $image = Mockery::mock(ImageInterface::class); + + $this->expectException(LogicException::class); + $encoder->encode($image); + } + + public function testSetOptions(): void + { + $encoder = new class () extends AbstractEncoder { + public int $quality = 75; + public bool $interlaced = false; + + public function encode(ImageInterface $image): EncodedImageInterface + { + return parent::encode($image); + } + }; + + $result = $encoder->setOptions(quality: 90, interlaced: true); + $this->assertSame($encoder, $result); + $this->assertEquals(90, $encoder->quality); + $this->assertTrue($encoder->interlaced); + } + + public function testSetOptionsInvalidProperty(): void + { + $encoder = new class () extends AbstractEncoder { + public int $quality = 75; + + public function encode(ImageInterface $image): EncodedImageInterface + { + return parent::encode($image); + } + }; + + $this->expectException(InvalidArgumentException::class); + $encoder->setOptions(nonExistentProperty: 42); + } + + public function testCreateEncodedImage(): void + { + $encoder = new class () extends AbstractEncoder { + public function encode(ImageInterface $image): EncodedImageInterface + { + return parent::encode($image); + } + + public function testCreateEncodedImage(): EncodedImage + { + return $this->createEncodedImage(function ($stream): void { + fwrite($stream, 'test data'); + }); + } + }; + + $result = $encoder->testCreateEncodedImage(); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertEquals('test data', $result->toString()); + } + + public function testCreateEncodedImageWithMediaType(): void + { + $encoder = new class () extends AbstractEncoder { + public function encode(ImageInterface $image): EncodedImageInterface + { + return parent::encode($image); + } + + public function testCreateEncodedImage(): EncodedImage + { + return $this->createEncodedImage(function ($stream): void { + fwrite($stream, 'png data'); + }, 'image/png'); + } + }; + + $result = $encoder->testCreateEncodedImage(); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertEquals('png data', $result->toString()); + $this->assertEquals('image/png', $result->mediaType()); + } +} diff --git a/tests/Unit/Drivers/AbstractFontProcessorTest.php b/tests/Unit/Drivers/AbstractFontProcessorTest.php new file mode 100644 index 000000000..c6c417b2c --- /dev/null +++ b/tests/Unit/Drivers/AbstractFontProcessorTest.php @@ -0,0 +1,265 @@ +path())) + ->setWrapWidth(20) + ->setSize(50) + ->setLineHeight(1.25) + ->setAlignmentHorizontal(Alignment::CENTER); + + $processor = Mockery::mock(AbstractFontProcessor::class)->makePartial(); + + $processor + ->shouldReceive('boxSize') + ->with('T', $font) + ->andReturn(new Size(12, 6)); + $processor + ->shouldReceive('boxSize') + ->with('Hy', $font) + ->andReturn(new Size(24, 6)); + + $processor + ->shouldReceive('boxSize') + ->with('AAAA', $font) + ->andReturn(new Size(24, 6, new Point(1000, 0))); + + $processor + ->shouldReceive('boxSize') + ->with('AAAA BBBB', $font) + ->andReturn(new Size(24, 6)); + + $processor + ->shouldReceive('boxSize') + ->with('BBBB', $font) + ->andReturn(new Size(24, 6, new Point(2000, 0))); + + $processor + ->shouldReceive('boxSize') + ->with('BBBB CCCC', $font) + ->andReturn(new Size(24, 6)); + + $processor + ->shouldReceive('boxSize') + ->with('CCCC', $font) + ->andReturn(new Size(24, 6, new Point(3000, 0))); + + $processor + ->shouldReceive('boxSize') + ->with($text, $font) + ->andReturn(new Size(100, 25, new Point(10, 0))); + + $block = $processor->textBlock($text, $font, new Point(0, 0)); + + $this->assertInstanceOf(TextBlock::class, $block); + $this->assertEquals(3, $block->count()); + $this->assertEquals(-512, $block->at(0)->position()->x()); + $this->assertEquals(-16, $block->at(0)->position()->y()); + $this->assertEquals(-1012, $block->at(1)->position()->x()); + $this->assertEquals(-8, $block->at(1)->position()->y()); + $this->assertEquals(-1512, $block->at(2)->position()->x()); + $this->assertEquals(0, $block->at(2)->position()->y()); + } + + public function testTextBlockWithLeftAlignment(): void + { + $text = 'AAAA BBBB'; + $font = (new Font(Resource::create('test.ttf')->path())) + ->setWrapWidth(20) + ->setSize(50) + ->setLineHeight(1.25) + ->setAlignmentHorizontal(Alignment::LEFT); + + $processor = Mockery::mock(AbstractFontProcessor::class)->makePartial(); + + $processor + ->shouldReceive('boxSize') + ->with('T', $font) + ->andReturn(new Size(12, 6)); + $processor + ->shouldReceive('boxSize') + ->with('Hy', $font) + ->andReturn(new Size(24, 6)); + + $processor + ->shouldReceive('boxSize') + ->with('AAAA', $font) + ->andReturn(new Size(24, 6, new Point(0, 0))); + $processor + ->shouldReceive('boxSize') + ->with('AAAA BBBB', $font) + ->andReturn(new Size(50, 6)); + $processor + ->shouldReceive('boxSize') + ->with('BBBB', $font) + ->andReturn(new Size(24, 6, new Point(0, 0))); + + $block = $processor->textBlock($text, $font, new Point(10, 20)); + + $this->assertInstanceOf(TextBlock::class, $block); + $this->assertEquals(2, $block->count()); + // LEFT alignment: xAdjustment = 0 + $this->assertEquals(10, $block->at(0)->position()->x()); + $this->assertEquals(10, $block->at(1)->position()->x()); + } + + public function testTextBlockWithRightAlignment(): void + { + $text = 'AAAA BBBB'; + $font = (new Font(Resource::create('test.ttf')->path())) + ->setWrapWidth(20) + ->setSize(50) + ->setLineHeight(1.25) + ->setAlignmentHorizontal(Alignment::RIGHT); + + $processor = Mockery::mock(AbstractFontProcessor::class)->makePartial(); + + $processor + ->shouldReceive('boxSize') + ->with('T', $font) + ->andReturn(new Size(12, 6)); + $processor + ->shouldReceive('boxSize') + ->with('Hy', $font) + ->andReturn(new Size(24, 6)); + + // Longest line determines blockWidth + $processor + ->shouldReceive('boxSize') + ->with('AAAA', $font) + ->andReturn(new Size(20, 6, new Point(0, 0))); + $processor + ->shouldReceive('boxSize') + ->with('AAAA BBBB', $font) + ->andReturn(new Size(50, 6)); + $processor + ->shouldReceive('boxSize') + ->with('BBBB', $font) + ->andReturn(new Size(15, 6, new Point(0, 0))); + + $block = $processor->textBlock($text, $font, new Point(0, 0)); + + $this->assertInstanceOf(TextBlock::class, $block); + $this->assertEquals(2, $block->count()); + // RIGHT alignment: xAdjustment = blockWidth - lineWidth (rounded) + // Line 1: blockWidth (50) - lineWidth (20+0) = 30 + // Line 2: blockWidth (50) - lineWidth (15+0) = 35 + $this->assertNotEquals( + $block->at(0)->position()->x(), + $block->at(1)->position()->x() + ); + } + + public function testTextBlockWithoutFontFile(): void + { + $text = 'Hello'; + $font = (new Font()) + ->setSize(50) + ->setLineHeight(1.25) + ->setAlignmentHorizontal(Alignment::LEFT); + + $processor = Mockery::mock(AbstractFontProcessor::class)->makePartial(); + + $processor + ->shouldReceive('boxSize') + ->with('T', $font) + ->andReturn(new Size(12, 10)); + $processor + ->shouldReceive('boxSize') + ->with('Hy', $font) + ->andReturn(new Size(24, 10)); + $processor + ->shouldReceive('boxSize') + ->with('Hello', $font) + ->andReturn(new Size(50, 10, new Point(0, 0))); + + $block = $processor->textBlock($text, $font, new Point(0, 100)); + + $this->assertInstanceOf(TextBlock::class, $block); + $this->assertEquals(1, $block->count()); + // Without font file: y = pivot->y() (no capHeight added) + // With font file: y = pivot->y() + capHeight + // This tests the branch where hasFile() returns false + $this->assertInstanceOf(TextBlock::class, $block); + } + + public function testTextBlockWithoutWrapWidth(): void + { + $text = 'Hello World'; + $font = (new Font(Resource::create('test.ttf')->path())) + ->setSize(50) + ->setLineHeight(1.25) + ->setAlignmentHorizontal(Alignment::LEFT); + + $processor = Mockery::mock(AbstractFontProcessor::class)->makePartial(); + + $processor + ->shouldReceive('boxSize') + ->with('T', $font) + ->andReturn(new Size(12, 6)); + $processor + ->shouldReceive('boxSize') + ->with('Hy', $font) + ->andReturn(new Size(24, 6)); + $processor + ->shouldReceive('boxSize') + ->with('Hello World', $font) + ->andReturn(new Size(100, 10, new Point(0, 0))); + + $block = $processor->textBlock($text, $font, new Point(0, 0)); + + $this->assertInstanceOf(TextBlock::class, $block); + // Without wrap width, text should not be wrapped + $this->assertEquals(1, $block->count()); + } + + public function testNativeFontSize(): void + { + $font = (new Font(Resource::create('test.ttf')->path()))->setSize(32); + $processor = Mockery::mock(AbstractFontProcessor::class)->makePartial(); + $this->assertEquals(32, $processor->nativeFontSize($font)); + } + + public function testTypographicalSize(): void + { + $font = new Font(Resource::create('test.ttf')->path()); + $processor = Mockery::mock(AbstractFontProcessor::class)->makePartial(); + $processor->shouldReceive('boxSize')->with('Hy', $font)->andReturn(new Size(24, 6)); + $this->assertEquals(6, $processor->typographicalSize($font)); + } + + public function testCapHeight(): void + { + $font = new Font(Resource::create('test.ttf')->path()); + $processor = Mockery::mock(AbstractFontProcessor::class)->makePartial(); + $processor->shouldReceive('boxSize')->with('T', $font)->andReturn(new Size(24, 6)); + $this->assertEquals(6, $processor->capHeight($font)); + } + + public function testLeading(): void + { + $font = (new Font(Resource::create('test.ttf')->path()))->setLineHeight(3); + $processor = Mockery::mock(AbstractFontProcessor::class)->makePartial(); + $processor->shouldReceive('boxSize')->with('Hy', $font)->andReturn(new Size(24, 6)); + $this->assertEquals(18, $processor->leading($font)); + } +} diff --git a/tests/Unit/Drivers/Gd/Analyzers/ColorspaceAnalyzerTest.php b/tests/Unit/Drivers/Gd/Analyzers/ColorspaceAnalyzerTest.php new file mode 100644 index 000000000..b4df4accb --- /dev/null +++ b/tests/Unit/Drivers/Gd/Analyzers/ColorspaceAnalyzerTest.php @@ -0,0 +1,28 @@ +setDriver($driver); + $result = $analyzer->analyze($resource->imageObject($driver)); + $this->assertInstanceOf($colorspace, $result); + } +} diff --git a/tests/Unit/Drivers/Gd/Analyzers/HeightAnalyzerTest.php b/tests/Unit/Drivers/Gd/Analyzers/HeightAnalyzerTest.php new file mode 100644 index 000000000..a1a47533f --- /dev/null +++ b/tests/Unit/Drivers/Gd/Analyzers/HeightAnalyzerTest.php @@ -0,0 +1,29 @@ +setDriver($driver); + $result = $analyzer->analyze($resource->imageObject($driver)); + $this->assertEquals($size->height(), $result); + } +} diff --git a/tests/Unit/Drivers/Gd/Analyzers/PixelColorAnalyzerTest.php b/tests/Unit/Drivers/Gd/Analyzers/PixelColorAnalyzerTest.php new file mode 100644 index 000000000..58536ebd0 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Analyzers/PixelColorAnalyzerTest.php @@ -0,0 +1,37 @@ +readTestImage('tile.png'); + $analyzer = new PixelColorAnalyzer(0, 0); + $analyzer->setDriver(new Driver()); + $result = $analyzer->analyze($image); + $this->assertInstanceOf(ColorInterface::class, $result); + $this->assertEquals('b4e000', $result->toHex()); + } + + public function testAnalyzeOutsideImageArea(): void + { + $image = $this->readTestImage('tile.png'); + $analyzer = new PixelColorAnalyzer(200, 0); + $analyzer->setDriver(new Driver()); + $this->expectException(InvalidArgumentException::class); + $analyzer->analyze($image); + } +} diff --git a/tests/Unit/Drivers/Gd/Analyzers/PixelColorsAnalyzerTest.php b/tests/Unit/Drivers/Gd/Analyzers/PixelColorsAnalyzerTest.php new file mode 100644 index 000000000..927e80a0e --- /dev/null +++ b/tests/Unit/Drivers/Gd/Analyzers/PixelColorsAnalyzerTest.php @@ -0,0 +1,40 @@ +readTestImage('animation.gif'); + $analyzer = new PixelColorsAnalyzer(0, 0); + $analyzer->setDriver(new Driver()); + $result = $analyzer->analyze($image); + $this->assertInstanceOf(Collection::class, $result); + $colors = array_map(fn(ColorInterface $color) => $color->toHex(), $result->toArray()); + $this->assertEquals($colors, ["394b63", "394b63", "394b63", "ffa601", "ffa601", "ffa601", "ffa601", "394b63"]); + } + + public function testAnalyzeNonAnimated(): void + { + $image = $this->readTestImage('tile.png'); + $analyzer = new PixelColorsAnalyzer(0, 0); + $analyzer->setDriver(new Driver()); + $result = $analyzer->analyze($image); + $this->assertInstanceOf(Collection::class, $result); + $this->assertInstanceOf(ColorInterface::class, $result->first()); + $this->assertEquals('b4e000', $result->first()->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Analyzers/ResolutionAnalyzerTest.php b/tests/Unit/Drivers/Gd/Analyzers/ResolutionAnalyzerTest.php new file mode 100644 index 000000000..578c3f90e --- /dev/null +++ b/tests/Unit/Drivers/Gd/Analyzers/ResolutionAnalyzerTest.php @@ -0,0 +1,31 @@ +setDriver($driver); + $result = $analyzer->analyze($resource->imageObject($driver)); + $this->assertInstanceOf(Resolution::class, $result); + $this->assertEquals($resolution->perInch()->x(), $result->perInch()->x(), $resource->filename()); + $this->assertEquals($resolution->perInch()->y(), $result->perInch()->y(), $resource->filename()); + } +} diff --git a/tests/Unit/Drivers/Gd/Analyzers/WidthAnalyzerTest.php b/tests/Unit/Drivers/Gd/Analyzers/WidthAnalyzerTest.php new file mode 100644 index 000000000..6b6dfbcf7 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Analyzers/WidthAnalyzerTest.php @@ -0,0 +1,29 @@ +setDriver($driver); + $result = $analyzer->analyze($resource->imageObject($driver)); + $this->assertEquals($size->width(), $result); + } +} diff --git a/tests/Unit/Drivers/Gd/ClonerTest.php b/tests/Unit/Drivers/Gd/ClonerTest.php new file mode 100644 index 000000000..503cc7a81 --- /dev/null +++ b/tests/Unit/Drivers/Gd/ClonerTest.php @@ -0,0 +1,76 @@ +path()); + $clone = Cloner::clone($gd); + + $this->assertEquals(16, imagesx($gd)); + $this->assertEquals(16, imagesy($gd)); + $this->assertEquals(16, imagesx($clone)); + $this->assertEquals(16, imagesy($clone)); + + $this->assertEquals( + imagecolorsforindex($gd, imagecolorat($gd, 10, 10)), + imagecolorsforindex($clone, imagecolorat($clone, 10, 10)) + ); + } + + public function testCloneEmpty(): void + { + $gd = imagecreatefromgif(Resource::create('gradient.gif')->path()); + $clone = Cloner::cloneEmpty($gd, new Size(12, 12), new Color(255, 0, 0, 0)); + + $this->assertEquals(16, imagesx($gd)); + $this->assertEquals(16, imagesy($gd)); + $this->assertEquals(12, imagesx($clone)); + $this->assertEquals(12, imagesy($clone)); + + $this->assertEquals( + ['red' => 0, 'green' => 255, 'blue' => 2, 'alpha' => 0], + imagecolorsforindex($gd, imagecolorat($gd, 10, 10)), + ); + + $this->assertEquals( + ['red' => 255, 'green' => 0, 'blue' => 0, 'alpha' => 127], + imagecolorsforindex($clone, imagecolorat($clone, 10, 10)) + ); + } + + public function testCloneBlended(): void + { + $gd = imagecreatefromgif(Resource::create('gradient.gif')->path()); + $clone = Cloner::cloneBlended($gd, new Color(255, 0, 255, 1)); + + $this->assertEquals(16, imagesx($gd)); + $this->assertEquals(16, imagesy($gd)); + $this->assertEquals(16, imagesx($clone)); + $this->assertEquals(16, imagesy($clone)); + + $this->assertEquals( + ['red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 127], + imagecolorsforindex($gd, imagecolorat($gd, 1, 0)), + ); + + $this->assertEquals( + ['red' => 255, 'green' => 0, 'blue' => 255, 'alpha' => 0], + imagecolorsforindex($clone, imagecolorat($clone, 1, 0)) + ); + } +} diff --git a/tests/Unit/Drivers/Gd/ColorProcessorTest.php b/tests/Unit/Drivers/Gd/ColorProcessorTest.php new file mode 100644 index 000000000..1b978f4e0 --- /dev/null +++ b/tests/Unit/Drivers/Gd/ColorProcessorTest.php @@ -0,0 +1,64 @@ +export(new Color(255, 55, 0, 1)); + $this->assertEquals(16725760, $result); + } + + public function testImport(): void + { + $processor = new ColorProcessor(); + $result = $processor->import(16725760); + $this->assertInstanceOf(Color::class, $result); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(55, $result->channel(Green::class)->value()); + $this->assertEquals(0, $result->channel(Blue::class)->value()); + $this->assertEquals(255, $result->channel(Alpha::class)->value()); + } + + public function testImportArray(): void + { + $processor = new ColorProcessor(); + $result = $processor->import(['red' => 255, 'green' => 55, 'blue' => 0, 'alpha' => 0]); + $this->assertInstanceOf(Color::class, $result); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(55, $result->channel(Green::class)->value()); + $this->assertEquals(0, $result->channel(Blue::class)->value()); + $this->assertEquals(255, $result->channel(Alpha::class)->value()); + + $result = $processor->import(['red' => 255, 'green' => 55, 'blue' => 0, 'alpha' => 127]); + $this->assertInstanceOf(Color::class, $result); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(55, $result->channel(Green::class)->value()); + $this->assertEquals(0, $result->channel(Blue::class)->value()); + $this->assertEquals(0, $result->channel(Alpha::class)->value()); + } + + public function testImportInvalid(): void + { + $processor = new ColorProcessor(); + $this->expectException(InvalidArgumentException::class); + $processor->import('test'); + } +} diff --git a/tests/Unit/Drivers/Gd/CoreTest.php b/tests/Unit/Drivers/Gd/CoreTest.php new file mode 100644 index 000000000..69fbf3c2b --- /dev/null +++ b/tests/Unit/Drivers/Gd/CoreTest.php @@ -0,0 +1,154 @@ +core = new Core([ + new Frame(imagecreatetruecolor(3, 2)), + new Frame(imagecreatetruecolor(3, 2)), + new Frame(imagecreatetruecolor(3, 2)), + ]); + } + + public function getTestFrame(): Frame + { + return new Frame(imagecreatetruecolor(3, 2)); + } + + public function testAdd(): void + { + $this->assertEquals(3, $this->core->count()); + $result = $this->core->add($this->getTestFrame()); + $this->assertEquals(4, $this->core->count()); + $this->assertInstanceOf(Core::class, $result); + } + + public function testSetNative(): void + { + $gd1 = imagecreatetruecolor(3, 2); + $gd2 = imagecreatetruecolor(3, 2); + $core = new Core([new Frame($gd1)]); + $this->assertEquals($gd1, $core->native()); + $core->setNative($gd2); + $this->assertEquals($gd2, $core->native()); + } + + public function testCount(): void + { + $this->assertEquals(3, $this->core->count()); + } + + public function testIterator(): void + { + foreach ($this->core as $frame) { + $this->assertInstanceOf(Frame::class, $frame); + } + } + + public function testNative(): void + { + $this->assertInstanceOf(GdImage::class, $this->core->native()); + } + + public function testFrame(): void + { + $this->assertInstanceOf(Frame::class, $this->core->frame(0)); + $this->assertInstanceOf(Frame::class, $this->core->frame(1)); + $this->assertInstanceOf(Frame::class, $this->core->frame(2)); + $this->expectException(InvalidArgumentException::class); + $this->core->frame(3); + } + + public function testSetGetLoops(): void + { + $this->assertEquals(0, $this->core->loops()); + $result = $this->core->setLoops(12); + $this->assertInstanceOf(Core::class, $result); + $this->assertEquals(12, $this->core->loops()); + } + + public function testHas(): void + { + $this->assertTrue($this->core->has(0)); + $this->assertTrue($this->core->has(1)); + $this->assertTrue($this->core->has(2)); + $this->assertFalse($this->core->has(3)); + } + + public function testPush(): void + { + $this->assertEquals(3, $this->core->count()); + $result = $this->core->push($this->getTestFrame()); + $this->assertEquals(4, $this->core->count()); + $this->assertEquals(4, $result->count()); + } + + public function testGet(): void + { + $this->assertInstanceOf(Frame::class, $this->core->get(0)); + $this->assertInstanceOf(Frame::class, $this->core->get(1)); + $this->assertInstanceOf(Frame::class, $this->core->get(2)); + $this->assertNull($this->core->get(3)); + $this->assertEquals('foo', $this->core->get(3, 'foo')); + } + + public function testClear(): void + { + $result = $this->core->clear(); + $this->assertEquals(0, $this->core->count()); + $this->assertEquals(0, $result->count()); + } + + public function testSlice(): void + { + $this->assertEquals(3, $this->core->count()); + $result = $this->core->slice(1, 2); + $this->assertEquals(2, $this->core->count()); + $this->assertEquals(2, $result->count()); + } + + public function testFirst(): void + { + $this->assertInstanceOf(Frame::class, $this->core->first()); + } + + public function testLast(): void + { + $this->assertInstanceOf(Frame::class, $this->core->last()); + } + + public function testClone(): void + { + $cloned = clone $this->core; + + $this->assertInstanceOf(Core::class, $cloned); + $this->assertEquals($this->core->count(), $cloned->count()); + + // Verify frames are independent (deep clone) + $this->assertNotSame($this->core->frame(0)->native(), $cloned->frame(0)->native()); + } + + public function testMeta(): void + { + $meta = $this->core->meta(); + $this->assertInstanceOf(CollectionInterface::class, $meta); + } +} diff --git a/tests/Unit/Drivers/Gd/Decoders/AbstractDecoderTest.php b/tests/Unit/Drivers/Gd/Decoders/AbstractDecoderTest.php new file mode 100644 index 000000000..6701cb4d5 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Decoders/AbstractDecoderTest.php @@ -0,0 +1,36 @@ +makePartial(); + $this->assertEquals( + MediaType::IMAGE_JPEG, + $decoder->mediaTypeByFilePath(Resource::create('test.jpg')->path()) + ); + } + + public function testGetMediaTypeFromFileBinary(): void + { + $decoder = Mockery::mock(AbstractDecoder::class)->makePartial(); + $this->assertEquals( + MediaType::IMAGE_JPEG, + $decoder->mediaTypeByBinary(Resource::create('test.jpg')->data()), + ); + } +} diff --git a/tests/Unit/Drivers/Gd/Decoders/Base64ImageDecoderTest.php b/tests/Unit/Drivers/Gd/Decoders/Base64ImageDecoderTest.php new file mode 100644 index 000000000..4efc9c142 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Decoders/Base64ImageDecoderTest.php @@ -0,0 +1,42 @@ +decoder = new Base64ImageDecoder(); + $this->decoder->setDriver(new Driver()); + } + + public function testDecode(): void + { + $result = $this->decoder->decode( + Resource::create('blue.gif')->base64() + ); + + $this->assertInstanceOf(Image::class, $result); + } + + public function testDecoderInvalid(): void + { + $this->expectException(DecoderException::class); + $this->decoder->decode('test'); + } +} diff --git a/tests/Unit/Drivers/Gd/Decoders/BinaryImageDecoderTest.php b/tests/Unit/Drivers/Gd/Decoders/BinaryImageDecoderTest.php new file mode 100644 index 000000000..d107e0d0a --- /dev/null +++ b/tests/Unit/Drivers/Gd/Decoders/BinaryImageDecoderTest.php @@ -0,0 +1,80 @@ +decoder = new BinaryImageDecoder(); + $this->decoder->setDriver(new Driver()); + } + + public function testDecodePng(): void + { + $image = $this->decoder->decode(Resource::create('tile.png')->data()); + $this->assertInstanceOf(Image::class, $image); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $this->assertCount(1, $image); + } + + public function testDecodeGif(): void + { + $image = $this->decoder->decode(Resource::create('red.gif')->data()); + $this->assertInstanceOf(Image::class, $image); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $this->assertCount(1, $image); + } + + public function testDecodeAnimatedGif(): void + { + $image = $this->decoder->decode(Resource::create('cats.gif')->data()); + $this->assertInstanceOf(Image::class, $image); + $this->assertEquals(75, $image->width()); + $this->assertEquals(50, $image->height()); + $this->assertCount(4, $image); + } + + public function testDecodeJpegWithExif(): void + { + $image = $this->decoder->decode(Resource::create('exif.jpg')->data()); + $this->assertInstanceOf(Image::class, $image); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $this->assertCount(1, $image); + $this->assertEquals('Oliver Vogel', $image->exif('IFD0.Artist')); + } + + public function testDecodeStringable(): void + { + $image = $this->decoder->decode(Resource::create('tile.png')->stringableData()); + $this->assertInstanceOf(Image::class, $image); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $this->assertCount(1, $image); + } + + public function testDecodeNonString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->decoder->decode(new stdClass()); + } +} diff --git a/tests/Unit/Drivers/Gd/Decoders/DataUriImageDecoderTest.php b/tests/Unit/Drivers/Gd/Decoders/DataUriImageDecoderTest.php new file mode 100644 index 000000000..afc34a91a --- /dev/null +++ b/tests/Unit/Drivers/Gd/Decoders/DataUriImageDecoderTest.php @@ -0,0 +1,53 @@ +decoder = new DataUriImageDecoder(); + $this->decoder->setDriver(new Driver()); + } + + public function testDecode(): void + { + $result = $this->decoder->decode(Resource::create('blue.gif')->dataUri()); + $this->assertInstanceOf(Image::class, $result); + } + + public function testDecoderNonString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->decoder->decode(new stdClass()); + } + + public function testDecoderInvalid(): void + { + $this->expectException(InvalidArgumentException::class); + $this->decoder->decode('invalid'); + } + + public function testDecoderNonImage(): void + { + $this->expectException(DecoderException::class); + $this->decoder->decode('data:text/plain;charset=utf-8,test'); + } +} diff --git a/tests/Unit/Drivers/Gd/Decoders/EncodedImageObjectDecoderTest.php b/tests/Unit/Drivers/Gd/Decoders/EncodedImageObjectDecoderTest.php new file mode 100644 index 000000000..31cdc2d7d --- /dev/null +++ b/tests/Unit/Drivers/Gd/Decoders/EncodedImageObjectDecoderTest.php @@ -0,0 +1,33 @@ +decoder = new EncodedImageObjectDecoder(); + $this->decoder->setDriver(new Driver()); + } + + public function testDecode(): void + { + $result = $this->decoder->decode(new EncodedImage(Resource::create()->data())); + $this->assertInstanceOf(Image::class, $result); + } +} diff --git a/tests/Unit/Drivers/Gd/Decoders/FilePathImageDecoderTest.php b/tests/Unit/Drivers/Gd/Decoders/FilePathImageDecoderTest.php new file mode 100644 index 000000000..662ad844e --- /dev/null +++ b/tests/Unit/Drivers/Gd/Decoders/FilePathImageDecoderTest.php @@ -0,0 +1,59 @@ +decoder = new FilePathImageDecoder(); + $this->decoder->setDriver(new Driver()); + } + + #[DataProvider('validFormatPathsProvider')] + public function testDecode(string|Stringable $path, ?string $exception): void + { + if ($exception !== null) { + $this->expectException($exception); + } + + $result = $this->decoder->decode($path); + + if ($exception === null) { + $this->assertInstanceOf(Image::class, $result); + } + } + + public static function validFormatPathsProvider(): Generator + { + yield [Resource::create('cats.gif')->path(), null]; + yield [Resource::create('animation.gif')->path(), null]; + yield [Resource::create('red.gif')->path(), null]; + yield [Resource::create('green.gif')->path(), null]; + yield [Resource::create('blue.gif')->path(), null]; + yield [Resource::create('gradient.bmp')->path(), null]; + yield [Resource::create('circle.png')->path(), null]; + yield [Resource::create('circle.png')->stringablePath(), null]; + yield ['no-path', FileNotFoundException::class]; + yield [str_repeat('x', PHP_MAXPATHLEN + 1), InvalidArgumentException::class]; + } +} diff --git a/tests/Unit/Drivers/Gd/Decoders/ImageObjectDecoderTest.php b/tests/Unit/Drivers/Gd/Decoders/ImageObjectDecoderTest.php new file mode 100644 index 000000000..cc67155c0 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Decoders/ImageObjectDecoderTest.php @@ -0,0 +1,23 @@ +decode($this->readTestImage('blue.gif')); + $this->assertInstanceOf(Image::class, $result); + } +} diff --git a/tests/Unit/Drivers/Gd/Decoders/NativeObjectDecoderTest.php b/tests/Unit/Drivers/Gd/Decoders/NativeObjectDecoderTest.php new file mode 100644 index 000000000..0b77f2635 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Decoders/NativeObjectDecoderTest.php @@ -0,0 +1,34 @@ +decoder = new NativeObjectDecoder(); + $this->decoder->setDriver(new Driver()); + } + + public function testDecode(): void + { + $result = $this->decoder->decode( + imagecreatetruecolor(3, 2) + ); + + $this->assertInstanceOf(Image::class, $result); + } +} diff --git a/tests/Unit/Drivers/Gd/Decoders/SplFileInfoImageDecoderTest.php b/tests/Unit/Drivers/Gd/Decoders/SplFileInfoImageDecoderTest.php new file mode 100644 index 000000000..048016b04 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Decoders/SplFileInfoImageDecoderTest.php @@ -0,0 +1,27 @@ +setDriver(new Driver()); + + $result = $decoder->decode(Resource::create('blue.gif')->splFileInfo()); + $this->assertInstanceOf(Image::class, $result); + } +} diff --git a/tests/Unit/Drivers/Gd/Decoders/StreamImageDecoderTest.php b/tests/Unit/Drivers/Gd/Decoders/StreamImageDecoderTest.php new file mode 100644 index 000000000..c9bdf2d31 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Decoders/StreamImageDecoderTest.php @@ -0,0 +1,27 @@ +setDriver(new Driver()); + + $result = $decoder->decode(Resource::create('test.jpg')->stream()); + $this->assertInstanceOf(Image::class, $result); + } +} diff --git a/tests/Unit/Drivers/Gd/DriverTest.php b/tests/Unit/Drivers/Gd/DriverTest.php new file mode 100644 index 000000000..f4d806684 --- /dev/null +++ b/tests/Unit/Drivers/Gd/DriverTest.php @@ -0,0 +1,306 @@ +driver = new Driver(); + } + + public function testId(): void + { + $this->assertEquals('GD', $this->driver->id()); + } + + public function testCreateImage(): void + { + $image = $this->driver->createImage(3, 2); + $this->assertInstanceOf(ImageInterface::class, $image); + $this->assertEquals(3, $image->width()); + $this->assertEquals(2, $image->height()); + } + + /** + * @param array $decoders + */ + #[DataProviderExternal(InputDataProvider::class, 'decodeImageDataProvider')] + public function testHandleImageInput(mixed $input, ?array $decoders, string $resultClassname): void + { + $this->assertInstanceOf($resultClassname, $this->driver->decodeImage($input, $decoders)); + } + + /** + * @param array $decoders + */ + #[DataProviderExternal(InputDataProvider::class, 'decodeColorDataProvider')] + public function testHandleColorInput(mixed $input, ?array $decoders, string $resultClassname): void + { + $this->assertInstanceOf($resultClassname, $this->driver->decodeColor($input, $decoders)); + } + + /** + * @param array $decoders + */ + #[DataProviderExternal(InputDataProvider::class, 'decodeImageDataProvider')] + public function testHandleColorInputFail(mixed $input, ?array $decoders, string $resultClassname): void + { + $this->expectException(ImageException::class); + $this->driver->decodeColor($input); + } + + /** + * @param array $decoders + */ + #[DataProviderExternal(InputDataProvider::class, 'decodeColorDataProvider')] + public function testHandleImageInputFail(mixed $input, ?array $decoders, string $resultClassname): void + { + $this->expectException(ImageException::class); + $this->driver->decodeImage($input); + } + + public function testColorProcessor(): void + { + $result = $this->driver->colorProcessor(Resource::create()->imageObject(Driver::class)); + $this->assertInstanceOf(ColorProcessorInterface::class, $result); + } + + #[DataProvider('supportsDataProvider')] + public function testSupports(bool $result, mixed $identifier): void + { + $this->assertEquals($result, $this->driver->supports($identifier)); + } + + public static function supportsDataProvider(): Generator + { + yield [true, Format::JPEG]; + yield [true, MediaType::IMAGE_JPEG]; + yield [true, MediaType::IMAGE_JPG]; + yield [true, FileExtension::JPG]; + yield [true, FileExtension::JPEG]; + yield [true, 'jpg']; + yield [true, 'jpeg']; + yield [true, 'image/jpg']; + yield [true, 'image/jpeg']; + + yield [true, Format::WEBP]; + yield [true, MediaType::IMAGE_WEBP]; + yield [true, MediaType::IMAGE_X_WEBP]; + yield [true, FileExtension::WEBP]; + yield [true, 'webp']; + yield [true, 'image/webp']; + yield [true, 'image/x-webp']; + + yield [true, Format::GIF]; + yield [true, MediaType::IMAGE_GIF]; + yield [true, FileExtension::GIF]; + yield [true, 'gif']; + yield [true, 'image/gif']; + + yield [true, Format::PNG]; + yield [true, MediaType::IMAGE_PNG]; + yield [true, MediaType::IMAGE_X_PNG]; + yield [true, FileExtension::PNG]; + yield [true, 'png']; + yield [true, 'image/png']; + yield [true, 'image/x-png']; + + yield [true, Format::AVIF]; + yield [true, MediaType::IMAGE_AVIF]; + yield [true, MediaType::IMAGE_X_AVIF]; + yield [true, FileExtension::AVIF]; + yield [true, 'avif']; + yield [true, 'image/avif']; + yield [true, 'image/x-avif']; + + yield [true, Format::BMP]; + yield [true, FileExtension::BMP]; + yield [true, MediaType::IMAGE_BMP]; + yield [true, MediaType::IMAGE_MS_BMP]; + yield [true, MediaType::IMAGE_X_BITMAP]; + yield [true, MediaType::IMAGE_X_BMP]; + yield [true, MediaType::IMAGE_X_MS_BMP]; + yield [true, MediaType::IMAGE_X_WINDOWS_BMP]; + yield [true, MediaType::IMAGE_X_WIN_BITMAP]; + yield [true, MediaType::IMAGE_X_XBITMAP]; + yield [true, 'bmp']; + yield [true, 'image/bmp']; + yield [true, 'image/ms-bmp']; + yield [true, 'image/x-bitmap']; + yield [true, 'image/x-bmp']; + yield [true, 'image/x-ms-bmp']; + yield [true, 'image/x-windows-bmp']; + yield [true, 'image/x-win-bitmap']; + yield [true, 'image/x-xbitmap']; + + yield [false, Format::TIFF]; + yield [false, MediaType::IMAGE_TIFF]; + yield [false, FileExtension::TIFF]; + yield [false, FileExtension::TIF]; + yield [false, 'tif']; + yield [false, 'tiff']; + yield [false, 'image/tiff']; + + yield [false, Format::JP2]; + yield [false, MediaType::IMAGE_JP2]; + yield [false, MediaType::IMAGE_JPX]; + yield [false, MediaType::IMAGE_JPM]; + yield [false, FileExtension::TIFF]; + yield [false, FileExtension::TIF]; + yield [false, FileExtension::JP2]; + yield [false, FileExtension::J2K]; + yield [false, FileExtension::JPF]; + yield [false, FileExtension::JPM]; + yield [false, FileExtension::JPG2]; + yield [false, FileExtension::J2C]; + yield [false, FileExtension::JPC]; + yield [false, FileExtension::JPX]; + yield [false, 'jp2']; + yield [false, 'j2k']; + yield [false, 'jpf']; + yield [false, 'jpm']; + yield [false, 'jpg2']; + yield [false, 'j2c']; + yield [false, 'jpc']; + yield [false, 'jpx']; + + yield [false, Format::HEIC]; + yield [false, MediaType::IMAGE_HEIC]; + yield [false, MediaType::IMAGE_HEIF]; + yield [false, FileExtension::HEIC]; + yield [false, FileExtension::HEIF]; + yield [false, 'heic']; + yield [false, 'heif']; + yield [false, 'image/heic']; + yield [false, 'image/heif']; + + yield [false, 'tga']; + yield [false, 'image/tga']; + yield [false, 'image/x-targa']; + yield [false, 'foo']; + yield [false, '']; + } + + public function testVersion(): void + { + $this->assertTrue(is_string($this->driver->version())); + } + + public function testSpecializeModifier(): void + { + $this->assertInstanceOf( + ResizeModifier::class, + $this->driver->specializeModifier(new GenericResizeModifier(100)), + ); + + $this->assertInstanceOf( + ResizeModifier::class, + $this->driver->specializeModifier(new ResizeModifier(100)), + ); + } + + public function testSpecializeAnalyzer(): void + { + $this->assertInstanceOf( + WidthAnalyzer::class, + $this->driver->specializeAnalyzer(new GenericWidthAnalyzer()), + ); + + $this->assertInstanceOf( + WidthAnalyzer::class, + $this->driver->specializeAnalyzer(new WidthAnalyzer()), + ); + } + + public function testSpecializeEncoder(): void + { + $this->assertInstanceOf( + PngEncoder::class, + $this->driver->specializeEncoder(new GenericPngEncoder()), + ); + + $this->assertInstanceOf( + PngEncoder::class, + $this->driver->specializeEncoder(new PngEncoder()), + ); + } + + public function testSpecializeDecoder(): void + { + $this->assertInstanceOf( + FilePathImageDecoder::class, + $this->driver->specializeDecoder(new GenericFilePathImageDecoder()), + ); + + $this->assertInstanceOf( + FilePathImageDecoder::class, + $this->driver->specializeDecoder(new FilePathImageDecoder()), + ); + } + + public function testSpecializeFailure(): void + { + $this->expectException(NotSupportedException::class); + $this->driver->specializeAnalyzer(new class () implements AnalyzerInterface, SpecializableInterface + { + protected DriverInterface $driver; + + public function analyze(ImageInterface $image): mixed + { + return true; + } + + /** @return array **/ + public function specializationArguments(): array + { + return []; + } + + public function setDriver(DriverInterface $driver): SpecializableInterface + { + return $this; + } + + public function driver(): DriverInterface + { + return $this->driver; + } + }); + } +} diff --git a/tests/Unit/Drivers/Gd/Encoders/AvifEncoderTest.php b/tests/Unit/Drivers/Gd/Encoders/AvifEncoderTest.php new file mode 100644 index 000000000..6bc109fbb --- /dev/null +++ b/tests/Unit/Drivers/Gd/Encoders/AvifEncoderTest.php @@ -0,0 +1,24 @@ +createTestImage(3, 2); + $encoder = new AvifEncoder(10); + $result = $encoder->encode($image); + $this->assertMediaType('image/avif', $result); + $this->assertEquals('image/avif', $result->mimetype()); + } +} diff --git a/tests/Unit/Drivers/Gd/Encoders/BmpEncoderTest.php b/tests/Unit/Drivers/Gd/Encoders/BmpEncoderTest.php new file mode 100644 index 000000000..39f2659de --- /dev/null +++ b/tests/Unit/Drivers/Gd/Encoders/BmpEncoderTest.php @@ -0,0 +1,24 @@ +createTestImage(3, 2); + $encoder = new BmpEncoder(); + $result = $encoder->encode($image); + $this->assertMediaType(['image/bmp', 'image/x-ms-bmp'], $result); + $this->assertEquals('image/bmp', $result->mimetype()); + } +} diff --git a/tests/Unit/Drivers/Gd/Encoders/GifEncoderTest.php b/tests/Unit/Drivers/Gd/Encoders/GifEncoderTest.php new file mode 100644 index 000000000..18128efc1 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Encoders/GifEncoderTest.php @@ -0,0 +1,52 @@ +createTestAnimation(); + $encoder = new GifEncoder(); + $result = $encoder->encode($image); + $this->assertMediaType('image/gif', $result); + $this->assertEquals('image/gif', $result->mimetype()); + $this->assertFalse( + Decoder::decode((string) $result)->firstFrame()->imageDescriptor()->isInterlaced() + ); + } + + public function testEncodeInterlaced(): void + { + $image = $this->createTestImage(3, 2); + $encoder = new GifEncoder(interlaced: true); + $result = $encoder->encode($image); + $this->assertMediaType('image/gif', $result); + $this->assertEquals('image/gif', $result->mimetype()); + $this->assertTrue( + Decoder::decode((string) $result)->firstFrame()->imageDescriptor()->isInterlaced() + ); + } + + public function testEncodeInterlacedAnimation(): void + { + $image = $this->createTestAnimation(); + $encoder = new GifEncoder(interlaced: true); + $result = $encoder->encode($image); + $this->assertMediaType('image/gif', $result); + $this->assertEquals('image/gif', $result->mimetype()); + $this->assertTrue( + Decoder::decode((string) $result)->firstFrame()->imageDescriptor()->isInterlaced() + ); + } +} diff --git a/tests/Unit/Drivers/Gd/Encoders/JpegEncoderTest.php b/tests/Unit/Drivers/Gd/Encoders/JpegEncoderTest.php new file mode 100644 index 000000000..14d0c33e0 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Encoders/JpegEncoderTest.php @@ -0,0 +1,40 @@ +createTestImage(3, 2); + $encoder = new JpegEncoder(75); + $encoder->setDriver(new Driver()); + $result = $encoder->encode($image); + $this->assertMediaType('image/jpeg', $result); + $this->assertEquals('image/jpeg', $result->mimetype()); + } + + public function testEncodeProgressive(): void + { + $image = $this->createTestImage(3, 2); + $encoder = new JpegEncoder(progressive: true); + $encoder->setDriver(new Driver()); + $result = $encoder->encode($image); + $this->assertMediaType('image/jpeg', $result); + $this->assertEquals('image/jpeg', $result->mimetype()); + $this->assertTrue($this->isProgressiveJpeg($result)); + } +} diff --git a/tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php b/tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php new file mode 100644 index 000000000..003bcb384 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php @@ -0,0 +1,94 @@ +createTestImage(3, 2); + $encoder = new PngEncoder(); + $result = $encoder->encode($image); + $this->assertMediaType('image/png', $result); + $this->assertEquals('image/png', $result->mimetype()); + $this->assertFalse($this->isInterlacedPng($result)); + } + + public function testEncodeInterlaced(): void + { + $image = $this->createTestImage(3, 2); + $encoder = new PngEncoder(interlaced: true); + $result = $encoder->encode($image); + $this->assertMediaType('image/png', $result); + $this->assertEquals('image/png', $result->mimetype()); + $this->assertTrue($this->isInterlacedPng($result)); + } + + #[DataProvider('indexedDataProvider')] + public function testEncoderIndexed(ImageInterface $image, PngEncoder $encoder, string $result): void + { + $this->assertEquals( + $result, + $this->pngColorType($encoder->encode($image)), + ); + } + + public static function indexedDataProvider(): Generator + { + yield [ + static::createTestImage(3, 2), // new + new PngEncoder(indexed: false), + 'truecolor-alpha', + ]; + yield [ + static::createTestImage(3, 2), // new + new PngEncoder(indexed: true), + 'indexed', + ]; + yield [ + static::readTestImage('circle.png'), // truecolor-alpha + new PngEncoder(indexed: false), + 'truecolor-alpha', + ]; + yield [ + static::readTestImage('circle.png'), // indexedcolor-alpha + new PngEncoder(indexed: true), + 'indexed', + ]; + yield [ + static::readTestImage('tile.png'), // indexed + new PngEncoder(indexed: false), + 'truecolor-alpha', + ]; + yield [ + static::readTestImage('tile.png'), // indexed + new PngEncoder(indexed: true), + 'indexed', + ]; + yield [ + static::readTestImage('test.jpg'), // jpeg + new PngEncoder(indexed: false), + 'truecolor-alpha', + ]; + yield [ + static::readTestImage('test.jpg'), // jpeg + new PngEncoder(indexed: true), + 'indexed', + ]; + } +} diff --git a/tests/Unit/Drivers/Gd/Encoders/WebpEncoderTest.php b/tests/Unit/Drivers/Gd/Encoders/WebpEncoderTest.php new file mode 100644 index 000000000..4878a5de3 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Encoders/WebpEncoderTest.php @@ -0,0 +1,24 @@ +createTestImage(3, 2); + $encoder = new WebpEncoder(75); + $result = $encoder->encode($image); + $this->assertMediaType('image/webp', $result); + $this->assertEquals('image/webp', $result->mimetype()); + } +} diff --git a/tests/Unit/Drivers/Gd/FontProcessorTest.php b/tests/Unit/Drivers/Gd/FontProcessorTest.php new file mode 100644 index 000000000..1720bdf53 --- /dev/null +++ b/tests/Unit/Drivers/Gd/FontProcessorTest.php @@ -0,0 +1,149 @@ +boxSize('test', (new Font())->setSize(1)); + $this->assertInstanceOf(SizeInterface::class, $size); + $this->assertEquals(20, $size->width()); + $this->assertEquals(8, $size->height()); + } + + public function testBoxSizeGdTwo(): void + { + $processor = new FontProcessor(); + $size = $processor->boxSize('test', (new Font())->setSize(2)); + $this->assertInstanceOf(SizeInterface::class, $size); + $this->assertEquals(24, $size->width()); + $this->assertEquals(14, $size->height()); + } + + public function testBoxSizeGdThree(): void + { + $processor = new FontProcessor(); + $size = $processor->boxSize('test', (new Font())->setSize(3)); + $this->assertInstanceOf(SizeInterface::class, $size); + $this->assertEquals(28, $size->width()); + $this->assertEquals(14, $size->height()); + } + + public function testBoxSizeGdFour(): void + { + $processor = new FontProcessor(); + $size = $processor->boxSize('test', (new Font())->setSize(4)); + $this->assertInstanceOf(SizeInterface::class, $size); + $this->assertEquals(32, $size->width()); + $this->assertEquals(16, $size->height()); + } + + public function testBoxSizeGdFive(): void + { + $processor = new FontProcessor(); + $size = $processor->boxSize('test', (new Font())->setSize(5)); + $this->assertInstanceOf(SizeInterface::class, $size); + $this->assertEquals(36, $size->width()); + $this->assertEquals(16, $size->height()); + } + + public function testBoxSizeTtf(): void + { + $processor = new FontProcessor(); + $size = $processor->boxSize('ABC', $this->testFont()); + $this->assertInstanceOf(SizeInterface::class, $size); + $this->assertContains($size->width(), [74, 75, 76]); + $this->assertContains($size->height(), [19, 20, 21]); + } + + public function testNativeFontSize(): void + { + $processor = new FontProcessor(); + $size = $processor->nativeFontSize(new Font('5')); + $this->assertEquals(9.12, $size); + } + + public function testTextBlock(): void + { + $processor = new FontProcessor(); + $result = $processor->textBlock('test', new Font(), new Point(0, 0)); + $this->assertInstanceOf(TextBlock::class, $result); + } + + public function testTypographicalSize(): void + { + $processor = new FontProcessor(); + $result = $processor->typographicalSize(new Font()); + $this->assertEquals(8, $result); + } + + public function testCapHeight(): void + { + $processor = new FontProcessor(); + $result = $processor->capHeight(new Font()); + $this->assertEquals(8, $result); + } + + public function testLeading(): void + { + $processor = new FontProcessor(); + $result = $processor->leading(new Font()); + $this->assertEquals(8, $result); + } + + public function testNativeFontSizeTtf(): void + { + $processor = new FontProcessor(); + $size = $processor->nativeFontSize($this->testFont()); + $this->assertEquals(42.56, $size); + } + + public function testTextBlockTtf(): void + { + $processor = new FontProcessor(); + $result = $processor->textBlock('test', $this->testFont(), new Point(0, 0)); + $this->assertInstanceOf(TextBlock::class, $result); + } + + public function testTypographicalSizeTtf(): void + { + $processor = new FontProcessor(); + $result = $processor->typographicalSize($this->testFont()); + $this->assertContains($result, [44, 45]); + } + + public function testCapHeightTtf(): void + { + $processor = new FontProcessor(); + $result = $processor->capHeight($this->testFont()); + $this->assertContains($result, [44, 45]); + } + + public function testLeadingTtf(): void + { + $processor = new FontProcessor(); + $result = $processor->leading($this->testFont()); + $this->assertContains($result, [44, 45]); + } + + private function testFont(): Font + { + return (new Font(Resource::create('test.ttf')->path()))->setSize(56); + } +} diff --git a/tests/Unit/Drivers/Gd/FrameTest.php b/tests/Unit/Drivers/Gd/FrameTest.php new file mode 100644 index 000000000..749588273 --- /dev/null +++ b/tests/Unit/Drivers/Gd/FrameTest.php @@ -0,0 +1,131 @@ +getTestFrame(); + $this->assertInstanceOf(Frame::class, $frame); + } + + public function testGetNative(): void + { + $frame = $this->getTestFrame(); + $this->assertInstanceOf(GdImage::class, $frame->native()); + } + + public function testSetCore(): void + { + $core1 = imagecreatetruecolor(3, 2); + $core2 = imagecreatetruecolor(3, 3); + $frame = new Frame($core1); + $this->assertEquals(2, $frame->size()->height()); + $result = $frame->setNative($core2); + $this->assertInstanceOf(Frame::class, $result); + $this->assertEquals(3, $frame->size()->height()); + } + + public function testGetSize(): void + { + $frame = $this->getTestFrame(); + $this->assertInstanceOf(Size::class, $frame->size()); + } + + public function testSetGetDelay(): void + { + $frame = $this->getTestFrame(); + $this->assertEquals(0, $frame->delay()); + + $result = $frame->setDelay(1.5); + $this->assertInstanceOf(Frame::class, $result); + $this->assertEquals(1.5, $frame->delay()); + } + + public function testSetGetDisposalMethod(): void + { + $frame = $this->getTestFrame(); + $this->assertEquals(1, $frame->disposalMethod()); + + $result = $frame->setDisposalMethod(3); + $this->assertInstanceOf(Frame::class, $result); + $this->assertEquals(3, $frame->disposalMethod()); + } + + public function testSetGetOffsetLeft(): void + { + $frame = $this->getTestFrame(); + $this->assertEquals(0, $frame->offsetLeft()); + + $result = $frame->setOffsetLeft(100); + $this->assertInstanceOf(Frame::class, $result); + $this->assertEquals(100, $frame->offsetLeft()); + } + + public function testSetGetOffsetTop(): void + { + $frame = $this->getTestFrame(); + $this->assertEquals(0, $frame->offsetTop()); + + $result = $frame->setOffsetTop(100); + $this->assertInstanceOf(Frame::class, $result); + $this->assertEquals(100, $frame->offsetTop()); + } + + public function testSetGetOffset(): void + { + $frame = $this->getTestFrame(); + $this->assertEquals(0, $frame->offsetTop()); + $this->assertEquals(0, $frame->offsetLeft()); + + $result = $frame->setOffset(100, 200); + $this->assertInstanceOf(Frame::class, $result); + $this->assertEquals(100, $frame->offsetLeft()); + $this->assertEquals(200, $frame->offsetTop()); + } + + public function testToImage(): void + { + $frame = $this->getTestFrame(); + $this->assertInstanceOf(Image::class, $frame->toImage(new Driver())); + } + + public function testDebugInfo(): void + { + $info = $this->getTestFrame()->__debugInfo(); + $this->assertEquals(0, $info['delay']); + $this->assertEquals(0, $info['left']); + $this->assertEquals(0, $info['top']); + $this->assertEquals(1, $info['disposalMethod']); + } + + public function testClone(): void + { + $frame = $this->getTestFrame(); + $cloned = clone $frame; + + $this->assertInstanceOf(Frame::class, $cloned); + $this->assertNotSame($frame->native(), $cloned->native()); + $this->assertEquals($frame->size()->width(), $cloned->size()->width()); + $this->assertEquals($frame->size()->height(), $cloned->size()->height()); + } +} diff --git a/tests/Unit/Drivers/Gd/ImageTest.php b/tests/Unit/Drivers/Gd/ImageTest.php new file mode 100644 index 000000000..66cc24c43 --- /dev/null +++ b/tests/Unit/Drivers/Gd/ImageTest.php @@ -0,0 +1,762 @@ +image = (new Image( + new Driver(), + new Core([ + new Frame(imagecreatetruecolor(3, 2)), + new Frame(imagecreatetruecolor(3, 2)), + ]) + ))->setExif( + new Collection([ + 'test' => 'foo' + ]) + ); + } + + public function testClone(): void + { + $image = $this->readTestImage('gradient.gif'); + $clone = clone $image; + + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $clone->width()); + $result = $clone->crop(4, 4); + $this->assertEquals(16, $image->width()); + $this->assertEquals(4, $clone->width()); + $this->assertEquals(4, $result->width()); + + $this->assertEquals('ff0000', $image->colorAt(0, 0)->toHex()); + $this->assertTransparency($image->colorAt(1, 0)); + + $this->assertEquals('ff0000', $clone->colorAt(0, 0)->toHex()); + $this->assertTransparency($image->colorAt(1, 0)); + } + + public function testDriver(): void + { + $this->assertInstanceOf(Driver::class, $this->image->driver()); + } + + public function testCore(): void + { + $this->assertInstanceOf(Core::class, $this->image->core()); + } + + public function testCount(): void + { + $this->assertEquals(2, $this->image->count()); + } + + public function testIteration(): void + { + foreach ($this->image as $frame) { + $this->assertInstanceOf(Frame::class, $frame); + } + } + + public function testIsAnimated(): void + { + $this->assertTrue($this->image->isAnimated()); + } + + public function testSetGetLoops(): void + { + $this->assertEquals(0, $this->image->loops()); + $result = $this->image->setLoops(10); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(10, $this->image->loops()); + } + + public function testRemoveAnimation(): void + { + $this->assertTrue($this->image->isAnimated()); + $result = $this->image->removeAnimation(); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertFalse($this->image->isAnimated()); + } + + public function testSliceAnimation(): void + { + $this->assertEquals(2, $this->image->count()); + $result = $this->image->sliceAnimation(0, 1); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(1, $this->image->count()); + } + + public function testExif(): void + { + $this->assertInstanceOf(Collection::class, $this->image->exif()); + $this->assertEquals('foo', $this->image->exif('test')); + } + + public function testModify(): void + { + $result = $this->image->modify(new GrayscaleModifier()); + $this->assertInstanceOf(Image::class, $result); + } + + public function testAnalyze(): void + { + $result = $this->image->analyze(new WidthAnalyzer()); + $this->assertEquals(3, $result); + } + + public function testEncode(): void + { + $result = $this->image->encode(new PngEncoder()); + $this->assertInstanceOf(EncodedImage::class, $result); + } + + public function testAutoEncode(): void + { + $result = $this->readTestImage('blue.gif')->encode(); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/gif', $result); + } + + public function testEncodeByMediaType(): void + { + $result = $this->readTestImage('blue.gif')->encodeUsingMediaType('image/png'); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/png', $result); + + $result = $this->readTestImage('blue.gif')->encodeUsingMediaType(MediaType::IMAGE_PNG); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/png', $result); + } + + public function testEncodeByExtension(): void + { + $result = $this->readTestImage('blue.gif')->encodeUsingFileExtension('png'); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/png', $result); + + $result = $this->readTestImage('blue.gif')->encodeUsingFileExtension(FileExtension::PNG); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/png', $result); + } + + public function testEncodeByPath(): void + { + $result = $this->readTestImage('blue.gif')->encodeUsingPath('foo/bar.png'); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/png', $result); + } + + public function testSaveAsFormat(): void + { + $path = __DIR__ . '/tmp.png'; + $result = $this->readTestImage('blue.gif')->save($path); + $this->assertInstanceOf(Image::class, $result); + $this->assertFileExists($path); + $this->assertMediaType('image/png', file_get_contents($path)); + unlink($path); + } + + public function testSaveFallback(): void + { + $path = __DIR__ . '/tmp.unknown'; + $this->expectException(NotSupportedException::class); + $this->readTestImage('blue.gif')->save($path); + } + + public function testSaveUndeterminedPath(): void + { + $this->expectException(EncoderException::class); + $this->createTestImage(2, 3)->save(); + } + + public function testWidthHeightSize(): void + { + $this->assertEquals(3, $this->image->width()); + $this->assertEquals(2, $this->image->height()); + $this->assertInstanceOf(SizeInterface::class, $this->image->size()); + } + + public function testColorspace(): void + { + $this->assertInstanceOf(ColorspaceInterface::class, $this->image->colorspace()); + } + + public function testSetColorspace(): void + { + $this->expectException(NotSupportedException::class); + $this->image->setColorspace(Colorspace::class); + } + + public function testSetGetResolution(): void + { + $resolution = $this->image->resolution(); + $this->assertInstanceOf(ResolutionInterface::class, $resolution); + $this->assertEquals(96, $resolution->x()); + $this->assertEquals(96, $resolution->y()); + $result = $this->image->setResolution(300, 300); + $resolution = $this->image->resolution(); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(300, $resolution->x()); + $this->assertEquals(300, $resolution->y()); + } + + public function testPickColor(): void + { + $this->assertInstanceOf(ColorInterface::class, $this->image->colorAt(0, 0)); + $this->assertInstanceOf(ColorInterface::class, $this->image->colorAt(0, 0, 1)); + } + + public function testPickColors(): void + { + $result = $this->image->colorsAt(0, 0); + $this->assertInstanceOf(Collection::class, $result); + $this->assertEquals(2, $result->count()); + } + + public function testProfile(): void + { + $this->expectException(NotSupportedException::class); + $this->image->profile(); + } + + public function testReduceColors(): void + { + $image = $this->readTestImage(); + $result = $image->reduceColors(8); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testSharpen(): void + { + $this->assertInstanceOf(Image::class, $this->image->sharpen(12)); + } + + public function testText(): void + { + $this->assertInstanceOf(Image::class, $this->image->text('test', 0, 0, new Font())); + } + + public function testBackgroundDefault(): void + { + $image = $this->readTestImage('gradient.gif'); + $this->assertColor(0, 0, 0, 0, $image->colorAt(1, 0)); + $result = $image->fillTransparentAreas(); + $this->assertColor(255, 255, 255, 255, $image->colorAt(1, 0)); + $this->assertColor(255, 255, 255, 255, $result->colorAt(1, 0)); + } + + public function testBackgroundArgument(): void + { + $image = $this->readTestImage('gradient.gif'); + $this->assertColor(0, 0, 0, 0, $image->colorAt(1, 0)); + $result = $image->fillTransparentAreas('ff5500'); + $this->assertColor(255, 85, 0, 255, $image->colorAt(1, 0)); + $this->assertColor(255, 85, 0, 255, $result->colorAt(1, 0)); + } + + public function testBackgroundIgnoreTransparencyInBackgroundColor(): void + { + $image = $this->readTestImage('gradient.gif'); + $this->assertColor(0, 0, 0, 0, $image->colorAt(1, 0)); + $result = $image->fillTransparentAreas('ff550033'); + $this->assertColor(255, 85, 0, 51, $image->colorAt(1, 0), 1); + $this->assertColor(255, 85, 0, 51, $result->colorAt(1, 0), 1); + } + + public function testInvert(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(0, 0)->toHex()); + $this->assertEquals('ffa601', $image->colorAt(25, 25)->toHex()); + $result = $image->invert(); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals('ff510f', $image->colorAt(0, 0)->toHex()); + $this->assertEquals('0059fe', $image->colorAt(25, 25)->toHex()); + } + + public function testPixelate(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(0, 0)->toHex()); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $result = $image->pixelate(10); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals('00aef0', $image->colorAt(0, 0)->toHex()); + $this->assertEquals('6aaa8b', $image->colorAt(14, 14)->toHex()); + } + + public function testGrayscale(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertFalse($image->colorAt(0, 0)->isGrayscale()); + $result = $image->grayscale(); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertTrue($image->colorAt(0, 0)->isGrayscale()); + } + + public function testBrightness(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $result = $image->brightness(30); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals('4cfaff', $image->colorAt(14, 14)->toHex()); + } + + public function testDebugInfo(): void + { + $info = $this->readTestImage('trim.png')->__debugInfo(); + $this->assertArrayHasKey('width', $info); + $this->assertArrayHasKey('height', $info); + } + + public function testContrast(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->contrast(10); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testGamma(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->gamma(1.5); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testColorize(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->colorize(10, 20, 30); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testFlip(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->flip(); + $this->assertInstanceOf(ImageInterface::class, $result); + + $result = $image->flip(Direction::VERTICAL); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testBlur(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->blur(5); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testRotate(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->rotate(45); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testOrient(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->orient(); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testTrim(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->trim(10); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testInsert(): void + { + $image = $this->readTestImage('trim.png'); + $watermark = $this->createTestImage(5, 5); + $result = $image->insert($watermark); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testInsertWithAlignment(): void + { + $image = $this->readTestImage('trim.png'); + $watermark = $this->createTestImage(5, 5); + $result = $image->insert($watermark, 10, 10, Alignment::CENTER, .5); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testFill(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->fill('ff0000'); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testFillAtPosition(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->fill('ff0000', 0, 0); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testResize(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->resize(100, 100); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(100, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testResizeWithFraction(): void + { + $image = $this->readTestImage('trim.png'); + $originalWidth = $image->width(); + $originalHeight = $image->height(); + $result = $image->resize(Fraction::HALF, Fraction::HALF); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals((int) round($originalWidth * 0.5), $result->width()); + $this->assertEquals((int) round($originalHeight * 0.5), $result->height()); + } + + public function testResizeDown(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->resizeDown(100, 100); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testScale(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->scale(100); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testScaleWithFraction(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->scale(Fraction::DOUBLE); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testScaleDown(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->scaleDown(100); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testCover(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->cover(10, 10); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(10, $result->width()); + $this->assertEquals(10, $result->height()); + } + + public function testCoverWithFraction(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->cover(Fraction::HALF, Fraction::HALF); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testCoverDown(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->coverDown(10, 10); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(10, $result->width()); + $this->assertEquals(10, $result->height()); + } + + public function testContainDown(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->containDown(100, 100); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(100, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testContainDownWithBackground(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->containDown(100, 100, 'ff0000', Alignment::CENTER); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testContain(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->contain(100, 100); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(100, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testContainWithBackground(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->contain(100, 100, 'ff0000', Alignment::CENTER); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testCropWithAlignment(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->crop(10, 10, 0, 0, null, Alignment::CENTER); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(10, $result->width()); + $this->assertEquals(10, $result->height()); + } + + public function testCropWithFraction(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->crop(Fraction::HALF, Fraction::HALF); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testResizeCanvas(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->resizeCanvas(100, 100); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(100, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testResizeCanvasWithBackground(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->resizeCanvas(100, 100, 'ff0000', Alignment::CENTER); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testResizeCanvasRelative(): void + { + $image = $this->readTestImage('trim.png'); + $originalWidth = $image->width(); + $originalHeight = $image->height(); + $result = $image->resizeCanvasRelative(10, 10); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals($originalWidth + 10, $result->width()); + $this->assertEquals($originalHeight + 10, $result->height()); + } + + public function testResizeCanvasRelativeWithBackground(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->resizeCanvasRelative(10, 10, 'ff0000', Alignment::CENTER); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawPixel(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawPixel(5, 5, 'ff0000'); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawRectangle(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawRectangle(function ($rectangle): void { + $rectangle->size(5, 5); + $rectangle->background('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawRectangleObject(): void + { + $image = $this->createTestImage(10, 10); + $rect = new Rectangle(5, 5, new Point(0, 0)); + $result = $image->drawRectangle($rect); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawEllipse(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawEllipse(function ($ellipse): void { + $ellipse->size(6, 4); + $ellipse->background('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawCircle(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawCircle(function ($circle): void { + $circle->radius(3); + $circle->background('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawPolygon(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawPolygon(function ($polygon): void { + $polygon->point(0, 0); + $polygon->point(5, 0); + $polygon->point(5, 5); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawLine(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawLine(function ($line): void { + $line->from(0, 0); + $line->to(9, 9); + $line->color('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawBezier(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawBezier(function ($bezier): void { + $bezier->point(0, 0); + $bezier->point(3, 5); + $bezier->point(6, 2); + $bezier->point(9, 9); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithRectangle(): void + { + $image = $this->createTestImage(10, 10); + $rect = new Rectangle(5, 5, new Point(0, 0)); + $result = $image->draw($rect); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithEllipse(): void + { + $image = $this->createTestImage(10, 10); + $ellipse = new Ellipse(6, 4, new Point(5, 5)); + $result = $image->draw($ellipse); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithCircle(): void + { + $image = $this->createTestImage(10, 10); + $circle = new Circle(6, new Point(5, 5)); + $result = $image->draw($circle); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithLine(): void + { + $image = $this->createTestImage(10, 10); + $line = new Line(new Point(0, 0), new Point(9, 9)); + $result = $image->draw($line); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithBezier(): void + { + $image = $this->createTestImage(10, 10); + $bezier = new Bezier([new Point(0, 0), new Point(3, 5), new Point(6, 2), new Point(9, 9)]); + $result = $image->draw($bezier); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithPolygon(): void + { + $image = $this->createTestImage(10, 10); + $polygon = new Polygon([new Point(0, 0), new Point(5, 0), new Point(5, 5)]); + $result = $image->draw($polygon); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testEncodeUsingFormat(): void + { + $image = $this->readTestImage('blue.gif'); + $result = $image->encodeUsingFormat(Format::PNG); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/png', $result); + } + + public function testBackgroundColor(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->backgroundColor(); + $this->assertInstanceOf(ColorInterface::class, $result); + } + + public function testSetBackgroundColor(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->setBackgroundColor('ff0000'); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testSetProfile(): void + { + $this->expectException(NotSupportedException::class); + $this->readTestImage('trim.png')->profile(); + } + + public function testRemoveProfile(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->removeProfile(); + $this->assertInstanceOf(ImageInterface::class, $result); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/BlurModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/BlurModifierTest.php new file mode 100644 index 000000000..a3ec94611 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/BlurModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $image->modify(new BlurModifier(30)); + $this->assertEquals('4fa68d', $image->colorAt(14, 14)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/BrightnessModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/BrightnessModifierTest.php new file mode 100644 index 000000000..71a0f141d --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/BrightnessModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $image->modify(new BrightnessModifier(30)); + $this->assertEquals('4cfaff', $image->colorAt(14, 14)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/ColorizeModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/ColorizeModifierTest.php new file mode 100644 index 000000000..4d287a1f2 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/ColorizeModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('tile.png'); + $image = $image->modify(new ColorizeModifier(100, -100, -100)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(5, 5)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(15, 15)); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/ContainDownModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/ContainDownModifierTest.php new file mode 100644 index 000000000..ab6574474 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/ContainDownModifierTest.php @@ -0,0 +1,42 @@ +readTestImage('blue.gif'); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $image->modify(new ContainDownModifier(30, 20, 'f00')); + $this->assertEquals(30, $image->width()); + $this->assertEquals(20, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(0, 19)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(29, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(29, 19)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(6, 2)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(7, 1)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(6, 17)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(7, 18)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(23, 1)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(23, 2)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(23, 17)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(23, 18)); + $this->assertColor(100, 100, 255, 255, $image->colorAt(7, 2)); + $this->assertColor(100, 100, 255, 255, $image->colorAt(22, 2)); + $this->assertColor(100, 100, 255, 255, $image->colorAt(7, 17)); + $this->assertColor(100, 100, 255, 255, $image->colorAt(22, 17)); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/ContainModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/ContainModifierTest.php new file mode 100644 index 000000000..087270b1e --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/ContainModifierTest.php @@ -0,0 +1,29 @@ +readTestImage('blocks.png'); + $this->assertEquals(640, $image->width()); + $this->assertEquals(480, $image->height()); + $image->modify(new ContainModifier(200, 100, 'ff0')); + $this->assertEquals(200, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertTransparency($image->colorAt(140, 10)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(175, 10)); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/ContrastModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/ContrastModifierTest.php new file mode 100644 index 000000000..cbbcf8c75 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/ContrastModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $image->modify(new ContrastModifier(30)); + $this->assertEquals('00ceff', $image->colorAt(14, 14)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/CoverDownModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/CoverDownModifierTest.php new file mode 100644 index 000000000..3046a628b --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/CoverDownModifierTest.php @@ -0,0 +1,39 @@ +readTestImage('blocks.png'); + $this->assertEquals(640, $image->width()); + $this->assertEquals(480, $image->height()); + $image->modify(new CoverDownModifier(100, 100, Alignment::CENTER)); + $this->assertEquals(100, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(90, 90)); + $this->assertColor(0, 255, 0, 255, $image->colorAt(65, 70)); + $this->assertColor(0, 0, 255, 255, $image->colorAt(70, 52)); + $this->assertTransparency($image->colorAt(90, 30)); + } + + public function testModifyOddSize(): void + { + $image = $this->createTestImage(375, 250); + $image->modify(new CoverDownModifier(240, 90, Alignment::CENTER)); + $this->assertEquals(240, $image->width()); + $this->assertEquals(90, $image->height()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/CoverModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/CoverModifierTest.php new file mode 100644 index 000000000..e58b04f19 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/CoverModifierTest.php @@ -0,0 +1,39 @@ +readTestImage('blocks.png'); + $this->assertEquals(640, $image->width()); + $this->assertEquals(480, $image->height()); + $image->modify(new CoverModifier(100, 100, Alignment::CENTER)); + $this->assertEquals(100, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(90, 90)); + $this->assertColor(0, 255, 0, 255, $image->colorAt(65, 70)); + $this->assertColor(0, 0, 255, 255, $image->colorAt(70, 52)); + $this->assertTransparency($image->colorAt(90, 30)); + } + + public function testModifyOddSize(): void + { + $image = $this->createTestImage(375, 250); + $image->modify(new CoverModifier(240, 90, Alignment::CENTER)); + $this->assertEquals(240, $image->width()); + $this->assertEquals(90, $image->height()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/CropModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/CropModifierTest.php new file mode 100644 index 000000000..2ed03742c --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/CropModifierTest.php @@ -0,0 +1,91 @@ +readTestImage('blocks.png'); + $image = $image->modify(new CropModifier(200, 200, 0, 0, 'ffffff', Alignment::BOTTOM_RIGHT)); + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(5, 5)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(100, 100)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(190, 190)); + } + + public function testModifyExtend(): void + { + $image = $this->readTestImage('blocks.png'); + $image = $image->modify(new CropModifier(800, 100, -10, -10, 'ff0000', Alignment::TOP_LEFT)); + $this->assertEquals(800, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(9, 9)); + $this->assertColor(0, 0, 255, 255, $image->colorAt(16, 16)); + $this->assertColor(0, 0, 255, 255, $image->colorAt(445, 16)); + $this->assertTransparency($image->colorAt(460, 16)); + } + + public function testModifySinglePixel(): void + { + $image = $this->createTestImage(1, 1); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new CropModifier(3, 3, 0, 0, 'ff0', Alignment::CENTER)); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(2, 2)); + } + + public function testModifyKeepsResolution(): void + { + $image = $this->readTestImage('300dpi.png'); + $this->assertEquals(300, round($image->resolution()->perInch()->x())); + $image = $image->modify(new CropModifier(800, 100, -10, -10, 'ff0000')); + $this->assertEquals(300, round($image->resolution()->perInch()->x())); + } + + public function testHalfTransparent(): void + { + $image = $this->createTestImage(16, 16); + $image->modify(new CropModifier(32, 32, 0, 0, '00f3', Alignment::CENTER)); + $this->assertEquals(32, $image->width()); + $this->assertEquals(32, $image->height()); + $this->assertColor(0, 0, 255, 51, $image->colorAt(5, 5), 1); + $this->assertColor(0, 0, 255, 51, $image->colorAt(16, 5), 1); + $this->assertColor(0, 0, 255, 51, $image->colorAt(30, 5), 1); + $this->assertColor(0, 0, 255, 51, $image->colorAt(5, 16), 1); + $this->assertColor(255, 0, 0, 255, $image->colorAt(16, 16)); + $this->assertColor(0, 0, 255, 51, $image->colorAt(30, 16), 1); + $this->assertColor(0, 0, 255, 51, $image->colorAt(5, 30), 1); + $this->assertColor(0, 0, 255, 51, $image->colorAt(16, 30), 1); + $this->assertColor(0, 0, 255, 51, $image->colorAt(30, 30), 1); + } + + public function testMergeTransparentBackgrounds(): void + { + $image = $this->createTestImage(1, 1)->fill('f00'); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new CropModifier(3, 3, 0, 0, '00f3', Alignment::CENTER)); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(0, 0, 255, 51, $image->colorAt(0, 0), 1); + $this->assertColor(255, 0, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(0, 0, 255, 51, $image->colorAt(2, 2), 1); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/DrawBezierModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/DrawBezierModifierTest.php new file mode 100644 index 000000000..e1ee97599 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/DrawBezierModifierTest.php @@ -0,0 +1,49 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Bezier([ + new Point(0, 0), + new Point(15, 0), + new Point(15, 15), + new Point(0, 15) + ]); + $drawable->setBackgroundColor('b53717'); + $image->modify(new DrawBezierModifier($drawable)); + $this->assertEquals('b53717', $image->colorAt(5, 5)->toHex()); + } + + public function testApplyWithoutBackgroundColor(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Bezier([ + new Point(15, 15), + new Point(30, 15), + new Point(30, 30), + new Point(15, 30) + ]); + $drawable->setBorder('fff', 5); + $image->modify(new DrawBezierModifier($drawable)); + $this->assertEquals('ffffff', $image->colorAt(26, 24)->toHex()); // border + $this->assertEquals('ffa601', $image->colorAt(19, 23)->toHex()); // background + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/DrawEllipseModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/DrawEllipseModifierTest.php new file mode 100644 index 000000000..e23268e16 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/DrawEllipseModifierTest.php @@ -0,0 +1,39 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Ellipse(10, 10, new Point(14, 14)); + $drawable->setBackgroundColor('b53717'); + $image->modify(new DrawEllipseModifier($drawable)); + $this->assertEquals('b53717', $image->colorAt(14, 14)->toHex()); + } + + public function testApplyWithoutBackground(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Ellipse(30, 30, new Point(14, 14)); + $drawable->setBorder('fff', 5); + $image->modify(new DrawEllipseModifier($drawable)); + $this->assertEquals('ffffff', $image->colorAt(5, 5)->toHex()); // border of circle + $this->assertEquals('ffa601', $image->colorAt(20, 20)->toHex()); // background of circle + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/DrawLineModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/DrawLineModifierTest.php new file mode 100644 index 000000000..6c8a3fae8 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/DrawLineModifierTest.php @@ -0,0 +1,28 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $line = new Line(new Point(0, 0), new Point(10, 0), 4); + $line->setBackgroundColor('b53517'); + $image->modify(new DrawLineModifier($line)); + $this->assertEquals('b53517', $image->colorAt(0, 0)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/DrawPixelModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/DrawPixelModifierTest.php new file mode 100644 index 000000000..a209dcb1c --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/DrawPixelModifierTest.php @@ -0,0 +1,25 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $image->modify(new DrawPixelModifier(new Point(14, 14), 'ffffff')); + $this->assertEquals('ffffff', $image->colorAt(14, 14)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/DrawPolygonModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/DrawPolygonModifierTest.php new file mode 100644 index 000000000..cc4fe8c42 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/DrawPolygonModifierTest.php @@ -0,0 +1,39 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Polygon([new Point(0, 0), new Point(15, 15), new Point(20, 20)]); + $drawable->setBackgroundColor('b53717'); + $image->modify(new DrawPolygonModifier($drawable)); + $this->assertEquals('b53717', $image->colorAt(14, 14)->toHex()); + } + + public function testApplyWithoutBackgroundColor(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Polygon([new Point(10, 10), new Point(40, 10), new Point(40, 30)]); + $drawable->setBorder('fff', 4); + $image->modify(new DrawPolygonModifier($drawable)); + $this->assertEquals('ffffff', $image->colorAt(19, 10)->toHex()); // border + $this->assertEquals('ffa601', $image->colorAt(30, 17)->toHex()); // background + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/DrawRectangleModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/DrawRectangleModifierTest.php new file mode 100644 index 000000000..2acfd3eef --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/DrawRectangleModifierTest.php @@ -0,0 +1,51 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $rectangle = new Rectangle(300, 200, new Point(14, 14)); + $rectangle->setBackgroundColor('ffffff'); + $image->modify(new DrawRectangleModifier($rectangle)); + $this->assertEquals('ffffff', $image->colorAt(14, 14)->toHex()); + } + + public function testApplyWithBorder(): void + { + $image = $this->readTestImage('trim.png'); + $rectangle = new Rectangle(10, 10, new Point(0, 0)); + $rectangle->setBackgroundColor('ffffff'); + $rectangle->setBorder('ff0000', 1); + $image->modify(new DrawRectangleModifier($rectangle)); + $this->assertEquals('ff0000', $image->colorAt(0, 0)->toHex()); + } + + public function testApplyWithoutBackground(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $rectangle = new Rectangle(30, 30, new Point(0, 0)); + $rectangle->setBorder('fff', 5); + $image->modify(new DrawRectangleModifier($rectangle)); + $this->assertEquals('ffffff', $image->colorAt(2, 2)->toHex()); // border + $this->assertEquals('ffa601', $image->colorAt(20, 20)->toHex()); // background + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/FillModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/FillModifierTest.php new file mode 100644 index 000000000..1cfa8ef6c --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/FillModifierTest.php @@ -0,0 +1,38 @@ +readTestImage('blocks.png'); + $this->assertEquals('0000ff', $image->colorAt(420, 270)->toHex()); + $this->assertEquals('ff0000', $image->colorAt(540, 400)->toHex()); + $image->modify(new FillModifier(new Color(204, 204, 204), new Point(540, 400))); + $this->assertEquals('0000ff', $image->colorAt(420, 270)->toHex()); + $this->assertEquals('cccccc', $image->colorAt(540, 400)->toHex()); + } + + public function testFillAllColor(): void + { + $image = $this->readTestImage('blocks.png'); + $this->assertEquals('0000ff', $image->colorAt(420, 270)->toHex()); + $this->assertEquals('ff0000', $image->colorAt(540, 400)->toHex()); + $image->modify(new FillModifier(new Color(204, 204, 204))); + $this->assertEquals('cccccc', $image->colorAt(420, 270)->toHex()); + $this->assertEquals('cccccc', $image->colorAt(540, 400)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/FlipModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/FlipModifierTest.php new file mode 100644 index 000000000..747a11e5d --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/FlipModifierTest.php @@ -0,0 +1,59 @@ +readTestImage('tile.png'); + $this->assertEquals('b4e000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('445160', $image->colorAt(12, 12)->toHex()); + $image->modify(new FlipModifier()); + $this->assertEquals('00000000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('b4e000', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('445160', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 12)->toHex()); + } + + public function testFlipImageHorizontal(): void + { + $image = $this->readTestImage('tile.png'); + $this->assertEquals('b4e000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('445160', $image->colorAt(12, 12)->toHex()); + $image->modify(new FlipModifier(Direction::HORIZONTAL)); + $this->assertEquals('00000000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('b4e000', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('445160', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 12)->toHex()); + } + + public function testFlipImageVertical(): void + { + $image = $this->readTestImage('tile.png'); + $this->assertEquals('b4e000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('445160', $image->colorAt(12, 12)->toHex()); + $image->modify(new FlipModifier(Direction::VERTICAL)); + $this->assertEquals('00000000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('445160', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('b4e000', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 12)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/GammaModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/GammaModifierTest.php new file mode 100644 index 000000000..56c85a160 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/GammaModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(0, 0)->toHex()); + $image->modify(new GammaModifier(2.1)); + $this->assertEquals('00d5f8', $image->colorAt(0, 0)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/GrayscaleModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/GrayscaleModifierTest.php new file mode 100644 index 000000000..7beaf7502 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/GrayscaleModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $this->assertFalse($image->colorAt(0, 0)->isGrayscale()); + $image->modify(new GrayscaleModifier()); + $this->assertTrue($image->colorAt(0, 0)->isGrayscale()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/InsertModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/InsertModifierTest.php new file mode 100644 index 000000000..83071a8e6 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/InsertModifierTest.php @@ -0,0 +1,44 @@ +readTestImage('test.jpg'); + $this->assertEquals('febc44', $image->colorAt(300, 25)->toHex()); + $image->modify(new InsertModifier(Resource::create('circle.png')->path(), 0, 0, Alignment::TOP_RIGHT)); + $this->assertEquals('32250d', $image->colorAt(300, 25)->toHex()); + } + + public function testColorChangeTransparencyPng(): void + { + $image = $this->readTestImage('test.jpg'); + $this->assertEquals('febc44', $image->colorAt(300, 25)->toHex()); + $image->modify(new InsertModifier(Resource::create('circle.png')->path(), 0, 0, Alignment::TOP_RIGHT, .5)); + $this->assertColor(152, 112, 40, 255, $image->colorAt(300, 25), tolerance: 1); + $this->assertColor(255, 202, 107, 255, $image->colorAt(274, 5), tolerance: 1); + } + + public function testColorChangeTransparencyJpeg(): void + { + $image = $this->createTestImage(16, 16)->fill('0000ff'); + $this->assertEquals('0000ff', $image->colorAt(10, 10)->toHex()); + $image->modify(new InsertModifier(Resource::create('exif.jpg')->path(), transparency: .5)); + $this->assertColor(127, 83, 127, 255, $image->colorAt(10, 10), tolerance: 1); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/InvertModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/InvertModifierTest.php new file mode 100644 index 000000000..a2d07be34 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/InvertModifierTest.php @@ -0,0 +1,26 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(0, 0)->toHex()); + $this->assertEquals('ffa601', $image->colorAt(25, 25)->toHex()); + $image->modify(new InvertModifier()); + $this->assertEquals('ff510f', $image->colorAt(0, 0)->toHex()); + $this->assertEquals('0059fe', $image->colorAt(25, 25)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/PixelateModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/PixelateModifierTest.php new file mode 100644 index 000000000..3cdb9ecd1 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/PixelateModifierTest.php @@ -0,0 +1,26 @@ +readTestImage('sphere.webp'); + $this->assertEquals('ff7c00', $image->colorAt(2, 2)->toHex()); + $this->assertEquals('ff7a0d', $image->colorAt(29, 29)->toHex()); + $image->modify(new PixelateModifier(10)); + $this->assertEquals('e6a562', $image->colorAt(2, 2)->toHex()); + $this->assertEquals('6f95b2', $image->colorAt(29, 29)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/ReduceColorsModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/ReduceColorsModifierTest.php new file mode 100644 index 000000000..ad0498180 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/ReduceColorsModifierTest.php @@ -0,0 +1,65 @@ +readTestImage('gradient.bmp'); + $this->assertColorCount(15, $image); + $image->modify(new ReduceColorsModifier(4)); + $this->assertColorCount(4, $image); + } + + public function testNoColorReduction(): void + { + $image = $this->readTestImage('gradient.bmp'); + $this->assertColorCount(15, $image); + $image->modify(new ReduceColorsModifier(150)); + $this->assertColorCount(15, $image); + } + + public function testInvalidColorInput(): void + { + $image = $this->readTestImage('gradient.bmp'); + $this->expectException(InvalidArgumentException::class); + $image->modify(new ReduceColorsModifier(0)); + } + + private function assertColorCount(int $count, ImageInterface $image): void + { + $colors = []; + $width = $image->width(); + $height = $image->height(); + for ($x = 0; $x < $width; $x++) { + for ($y = 0; $y < $height; $y++) { + $rgb = imagecolorat($image->core()->native(), $x, $y); + $color = imagecolorsforindex($image->core()->native(), $rgb); + $color = implode('-', $color); + $colors[$color] = $color; + } + } + + $this->assertEquals(count($colors), $count); + } + + public function testVerifyColorValueAfterQuantization(): void + { + $image = $this->createTestImage(3, 2)->fill('f00'); + $image->modify(new ReduceColorsModifier(1)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(1, 1), 4); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/RemoveAnimationModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/RemoveAnimationModifierTest.php new file mode 100644 index 000000000..c9f16583b --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/RemoveAnimationModifierTest.php @@ -0,0 +1,51 @@ +readTestImage('animation.gif'); + $this->assertEquals(8, count($image)); + $result = $image->modify(new RemoveAnimationModifier(2)); + $this->assertEquals(1, count($image)); + $this->assertEquals(1, count($result)); + } + + public function testApplyPercent(): void + { + $image = $this->readTestImage('animation.gif'); + $this->assertEquals(8, count($image)); + $result = $image->modify(new RemoveAnimationModifier('20%')); + $this->assertEquals(1, count($image)); + $this->assertEquals(1, count($result)); + } + + public function testApplyNonAnimated(): void + { + $image = $this->readTestImage('test.jpg'); + $this->assertEquals(1, count($image)); + $result = $image->modify(new RemoveAnimationModifier()); + $this->assertEquals(1, count($image)); + $this->assertEquals(1, count($result)); + } + + public function testApplyInvalid(): void + { + $image = $this->readTestImage('animation.gif'); + $this->expectException(InvalidArgumentException::class); + $image->modify(new RemoveAnimationModifier('test')); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/ResizeCanvasModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/ResizeCanvasModifierTest.php new file mode 100644 index 000000000..a3376edcc --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/ResizeCanvasModifierTest.php @@ -0,0 +1,70 @@ +createTestImage(1, 1); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new ResizeCanvasModifier(3, 3, 'ff0', Alignment::CENTER)); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(2, 2)); + } + + public function testModifyWithTransparency(): void + { + $image = $this->readTestImage('tile.png'); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $image->modify(new ResizeCanvasModifier(18, 18, 'ff0', Alignment::CENTER)); + $this->assertEquals(18, $image->width()); + $this->assertEquals(18, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(180, 224, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(180, 224, 0, 255, $image->colorAt(2, 2)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(17, 17)); + $this->assertTransparency($image->colorAt(12, 1)); + + $image = $this->createTestImage(16, 16); + $image->modify(new ResizeCanvasModifier(32, 32, '00f5', Alignment::CENTER)); + $this->assertEquals(32, $image->width()); + $this->assertEquals(32, $image->height()); + $this->assertColor(0, 0, 255, 84, $image->colorAt(5, 5)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(16, 5)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(30, 5)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(5, 16)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(16, 16)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(30, 16)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(5, 30)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(16, 30)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(30, 30)); + } + + public function testModifyEdge(): void + { + $image = $this->createTestImage(1, 1); + $this->assertColor(255, 0, 0, 255, $image->colorAt(0, 0)); + $image->modify(new ResizeCanvasModifier(null, 2, 'ff0', Alignment::BOTTOM)); + $this->assertEquals(1, $image->width()); + $this->assertEquals(2, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(0, 1)); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/ResizeCanvasRelativeModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/ResizeCanvasRelativeModifierTest.php new file mode 100644 index 000000000..1199a945f --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/ResizeCanvasRelativeModifierTest.php @@ -0,0 +1,45 @@ +createTestImage(1, 1); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new ResizeCanvasRelativeModifier(2, 2, 'ff0', Alignment::CENTER)); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(2, 2)); + } + + public function testModifyWithTransparency(): void + { + $image = $this->readTestImage('tile.png'); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $image->modify(new ResizeCanvasRelativeModifier(2, 2, 'ff0', Alignment::CENTER)); + $this->assertEquals(18, $image->width()); + $this->assertEquals(18, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(180, 224, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(180, 224, 0, 255, $image->colorAt(2, 2)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(17, 17)); + $this->assertTransparency($image->colorAt(12, 1)); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/ResizeModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/ResizeModifierTest.php new file mode 100644 index 000000000..e2abbaf24 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/ResizeModifierTest.php @@ -0,0 +1,30 @@ +readTestImage('blocks.png'); + $this->assertEquals(640, $image->width()); + $this->assertEquals(480, $image->height()); + $image->modify(new ResizeModifier(200, 100)); + $this->assertEquals(200, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(150, 70)); + $this->assertColor(0, 255, 0, 255, $image->colorAt(125, 70)); + $this->assertColor(0, 0, 255, 255, $image->colorAt(130, 54)); + $this->assertTransparency($image->colorAt(170, 30)); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/ResolutionModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/ResolutionModifierTest.php new file mode 100644 index 000000000..df237fd46 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/ResolutionModifierTest.php @@ -0,0 +1,26 @@ +readTestImage('test.jpg'); + $this->assertEquals(72.0, $image->resolution()->x()); + $this->assertEquals(72.0, $image->resolution()->y()); + $image->modify(new ResolutionModifier(1, 2)); + $this->assertEquals(1.0, $image->resolution()->x()); + $this->assertEquals(2.0, $image->resolution()->y()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/RotateModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/RotateModifierTest.php new file mode 100644 index 000000000..f7214a7e2 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/RotateModifierTest.php @@ -0,0 +1,26 @@ +readTestImage('test.jpg'); + $this->assertEquals(320, $image->width()); + $this->assertEquals(240, $image->height()); + $image->modify(new RotateModifier(90, 'fff')); + $this->assertEquals(240, $image->width()); + $this->assertEquals(320, $image->height()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/SharpenModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/SharpenModifierTest.php new file mode 100644 index 000000000..b3029d2c3 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/SharpenModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $this->assertEquals('60ab96', $image->colorAt(15, 14)->toHex()); + $image->modify(new SharpenModifier(10)); + $this->assertEquals('4daba7', $image->colorAt(15, 14)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/TextModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/TextModifierTest.php new file mode 100644 index 000000000..7f657e682 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/TextModifierTest.php @@ -0,0 +1,37 @@ +setColor('ff0055'); + + $modifier = new class ('test', new Point(), $font) extends TextModifier + { + public function test(): ColorInterface + { + return $this->textColor(); + } + }; + + $modifier->setDriver(new Driver()); + + $this->assertInstanceOf(ColorInterface::class, $modifier->test()); + } +} diff --git a/tests/Unit/Drivers/Gd/Modifiers/TrimModifierTest.php b/tests/Unit/Drivers/Gd/Modifiers/TrimModifierTest.php new file mode 100644 index 000000000..b5a663d99 --- /dev/null +++ b/tests/Unit/Drivers/Gd/Modifiers/TrimModifierTest.php @@ -0,0 +1,55 @@ +readTestImage('trim.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier()); + $this->assertEquals(28, $image->width()); + $this->assertEquals(28, $image->height()); + } + + public function testTrimGradient(): void + { + $image = $this->readTestImage('radial.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier(50)); + $this->assertLessThan(50, $image->width()); + $this->assertLessThan(50, $image->height()); + } + + public function testTrimHighTolerance(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier(1000000)); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $this->assertColor(255, 255, 255, 0, $image->colorAt(0, 0)); + } + + public function testTrimAnimated(): void + { + $image = $this->readTestImage('animation.gif'); + $this->expectException(NotSupportedException::class); + $image->modify(new TrimModifier()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Analyzers/ColorspaceAnalyzerTest.php b/tests/Unit/Drivers/Imagick/Analyzers/ColorspaceAnalyzerTest.php new file mode 100644 index 000000000..143ecbf95 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Analyzers/ColorspaceAnalyzerTest.php @@ -0,0 +1,29 @@ +setDriver($driver); + $result = $analyzer->analyze($resource->imageObject($driver)); + $this->assertInstanceOf($colorspace, $result); + } +} diff --git a/tests/Unit/Drivers/Imagick/Analyzers/HeightAnalyzerTest.php b/tests/Unit/Drivers/Imagick/Analyzers/HeightAnalyzerTest.php new file mode 100644 index 000000000..efa9d4cca --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Analyzers/HeightAnalyzerTest.php @@ -0,0 +1,30 @@ +setDriver($driver); + $result = $analyzer->analyze($resource->imageObject($driver)); + $this->assertEquals($size->height(), $result); + } +} diff --git a/tests/Unit/Drivers/Imagick/Analyzers/PixelColorAnalyzerTest.php b/tests/Unit/Drivers/Imagick/Analyzers/PixelColorAnalyzerTest.php new file mode 100644 index 000000000..ae93d5152 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Analyzers/PixelColorAnalyzerTest.php @@ -0,0 +1,37 @@ +readTestImage('tile.png'); + $analyzer = new PixelColorAnalyzer(0, 0); + $analyzer->setDriver(new Driver()); + $result = $analyzer->analyze($image); + $this->assertInstanceOf(ColorInterface::class, $result); + $this->assertEquals('b4e000', $result->toHex()); + } + + public function testAnalyzeOutsideImageArea(): void + { + $image = $this->readTestImage('tile.png'); + $analyzer = new PixelColorAnalyzer(200, 0); + $analyzer->setDriver(new Driver()); + $this->expectException(InvalidArgumentException::class); + $analyzer->analyze($image); + } +} diff --git a/tests/Unit/Drivers/Imagick/Analyzers/PixelColorsAnalyzerTest.php b/tests/Unit/Drivers/Imagick/Analyzers/PixelColorsAnalyzerTest.php new file mode 100644 index 000000000..c48a8881d --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Analyzers/PixelColorsAnalyzerTest.php @@ -0,0 +1,40 @@ +readTestImage('animation.gif'); + $analyzer = new PixelColorsAnalyzer(0, 0); + $analyzer->setDriver(new Driver()); + $result = $analyzer->analyze($image); + $this->assertInstanceOf(Collection::class, $result); + $colors = array_map(fn(ColorInterface $color) => $color->toHex(), $result->toArray()); + $this->assertEquals($colors, ["394b63", "394b63", "394b63", "ffa601", "ffa601", "ffa601", "ffa601", "394b63"]); + } + + public function testAnalyzeNonAnimated(): void + { + $image = $this->readTestImage('tile.png'); + $analyzer = new PixelColorsAnalyzer(0, 0); + $analyzer->setDriver(new Driver()); + $result = $analyzer->analyze($image); + $this->assertInstanceOf(Collection::class, $result); + $this->assertInstanceOf(ColorInterface::class, $result->first()); + $this->assertEquals('b4e000', $result->first()->toHex()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Analyzers/ProfileAnalyzerTest.php b/tests/Unit/Drivers/Imagick/Analyzers/ProfileAnalyzerTest.php new file mode 100644 index 000000000..37372d48c --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Analyzers/ProfileAnalyzerTest.php @@ -0,0 +1,26 @@ +readTestImage('tile.png'); + $analyzer = new ProfileAnalyzer(); + $analyzer->setDriver(new Driver()); + $this->expectException(AnalyzerException::class); + $analyzer->analyze($image); + } +} diff --git a/tests/Unit/Drivers/Imagick/Analyzers/ResolutionAnalyzerTest.php b/tests/Unit/Drivers/Imagick/Analyzers/ResolutionAnalyzerTest.php new file mode 100644 index 000000000..9027e7802 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Analyzers/ResolutionAnalyzerTest.php @@ -0,0 +1,32 @@ +setDriver($driver); + $result = $analyzer->analyze($resource->imageObject($driver)); + $this->assertInstanceOf(Resolution::class, $result); + $this->assertEquals($resolution->perInch()->x(), round($result->perInch()->x()), $resource->filename()); + $this->assertEquals($resolution->perInch()->y(), round($result->perInch()->y()), $resource->filename()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Analyzers/WidthAnalyzerTest.php b/tests/Unit/Drivers/Imagick/Analyzers/WidthAnalyzerTest.php new file mode 100644 index 000000000..d081c33f1 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Analyzers/WidthAnalyzerTest.php @@ -0,0 +1,30 @@ +setDriver($driver); + $result = $analyzer->analyze($resource->imageObject($driver)); + $this->assertEquals($size->width(), $result); + } +} diff --git a/tests/Unit/Drivers/Imagick/ColorProcessorTest.php b/tests/Unit/Drivers/Imagick/ColorProcessorTest.php new file mode 100644 index 000000000..f1f9e3d55 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/ColorProcessorTest.php @@ -0,0 +1,117 @@ +export(new Color(255, 55, 0, .2)); + $this->assertInstanceOf(ImagickPixel::class, $result); + $this->assertEquals(['r' => 1, 'g' => 0.21568627450980393, 'b' => 0, 'a' => .2], $result->getColor(1)); + } + + public function testExportCmyk(): void + { + $processor = new ColorProcessor(new CmykColorspace()); + $result = $processor->export(new CmykColor(100, 0, 0, 0)); + $this->assertInstanceOf(ImagickPixel::class, $result); + $this->assertEquals(1.0, $result->getColorValue(Imagick::COLOR_CYAN)); + $this->assertEquals(0.0, $result->getColorValue(Imagick::COLOR_MAGENTA)); + $this->assertEquals(0.0, $result->getColorValue(Imagick::COLOR_YELLOW)); + $this->assertEquals(0.0, $result->getColorValue(Imagick::COLOR_BLACK)); + } + + public function testExportHsl(): void + { + $processor = new ColorProcessor(new HslColorspace()); + $result = $processor->export(new Color(255, 0, 0)); + $this->assertInstanceOf(ImagickPixel::class, $result); + } + + public function testImport(): void + { + $processor = new ColorProcessor(new Colorspace()); + $result = $processor->import(new ImagickPixel('rgb(255, 55, 0)')); + $this->assertInstanceOf(ColorInterface::class, $result); + $this->assertColor(255, 55, 0, 255, $result); + + $result = $processor->import(new ImagickPixel('rgba(255, 55, 0, .2)')); + $this->assertInstanceOf(ColorInterface::class, $result); + $this->assertColor(255, 55, 0, 51, $result); + + $pixel = new ImagickPixel(); + $pixel->setColorValue(Imagick::COLOR_RED, 1); + $pixel->setColorValue(Imagick::COLOR_GREEN, .3); + $pixel->setColorValue(Imagick::COLOR_BLUE, 0); + $pixel->setColorValue(Imagick::COLOR_ALPHA, .2); + $result = $processor->import($pixel); + $this->assertInstanceOf(ColorInterface::class, $result); + $this->assertColor(255, 77, 0, 51, $result); + } + + public function testImportColorCmyk(): void + { + $processor = new ColorProcessor(new CmykColorspace()); + $pixel = new ImagickPixel(); + $pixel->setColorValue(Imagick::COLOR_CYAN, 1.0); + $pixel->setColorValue(Imagick::COLOR_MAGENTA, 0.0); + $pixel->setColorValue(Imagick::COLOR_YELLOW, 0.0); + $pixel->setColorValue(Imagick::COLOR_BLACK, 0.0); + $result = $processor->import($pixel); + $this->assertInstanceOf(CmykColor::class, $result); + } + + public function testImportHsl(): void + { + $processor = new ColorProcessor(new HslColorspace()); + $result = $processor->import(new ImagickPixel('rgb(255, 0, 0)')); + $this->assertInstanceOf(HslColor::class, $result); + } + + public function testImportHsv(): void + { + $processor = new ColorProcessor(new HsvColorspace()); + $result = $processor->import(new ImagickPixel('rgb(255, 0, 0)')); + $this->assertInstanceOf(HsvColor::class, $result); + } + + public function testImportOklab(): void + { + $processor = new ColorProcessor(new OklabColorspace()); + $result = $processor->import(new ImagickPixel('rgb(255, 0, 0)')); + $this->assertInstanceOf(OklabColor::class, $result); + } + + public function testImportOklch(): void + { + $processor = new ColorProcessor(new OklchColorspace()); + $result = $processor->import(new ImagickPixel('rgb(255, 0, 0)')); + $this->assertInstanceOf(OklchColor::class, $result); + } +} diff --git a/tests/Unit/Drivers/Imagick/CoreTest.php b/tests/Unit/Drivers/Imagick/CoreTest.php new file mode 100644 index 000000000..d60de5898 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/CoreTest.php @@ -0,0 +1,188 @@ +newImage(10, 10, new ImagickPixel('red')); + $imagick->addImage($im); + + $im = new Imagick(); + $im->newImage(10, 10, new ImagickPixel('green')); + $imagick->addImage($im); + + $im = new Imagick(); + $im->newImage(10, 10, new ImagickPixel('blue')); + $imagick->addImage($im); + + $this->core = new Core($imagick); + } + + public function testAdd(): void + { + $imagick = new Imagick(); + $imagick->newImage(100, 100, new ImagickPixel('red')); + $this->assertEquals(3, $this->core->count()); + $result = $this->core->add(new Frame($imagick)); + $this->assertEquals(4, $this->core->count()); + $this->assertInstanceOf(Core::class, $result); + } + + public function testCount(): void + { + $this->assertEquals(3, $this->core->count()); + } + + public function testIterator(): void + { + foreach ($this->core as $frame) { + $this->assertInstanceOf(Frame::class, $frame); + } + } + + public function testNative(): void + { + $this->assertInstanceOf(Imagick::class, $this->core->native()); + } + + public function testSetNative(): void + { + $imagick1 = new Imagick(); + $imagick1->newImage(10, 10, new ImagickPixel('red')); + + $imagick2 = new Imagick(); + $imagick2->newImage(10, 10, new ImagickPixel('red')); + + $core = new Core($imagick1); + $this->assertEquals($imagick1, $core->native()); + $core->setNative($imagick2); + $this->assertEquals($imagick2, $core->native()); + } + + public function testFrame(): void + { + $this->assertInstanceOf(Frame::class, $this->core->frame(0)); + $this->assertInstanceOf(Frame::class, $this->core->frame(1)); + $this->assertInstanceOf(Frame::class, $this->core->frame(2)); + $this->expectException(InvalidArgumentException::class); + $this->core->frame(3); + } + + public function testSetGetLoops(): void + { + $this->assertEquals(0, $this->core->loops()); + $result = $this->core->setLoops(12); + $this->assertEquals(12, $this->core->loops()); + $this->assertInstanceOf(Core::class, $result); + } + + public function testHas(): void + { + $this->assertTrue($this->core->has(0)); + $this->assertTrue($this->core->has(1)); + $this->assertTrue($this->core->has(2)); + $this->assertFalse($this->core->has(3)); + } + + public function testPush(): void + { + $im = new Imagick(); + $im->newImage(100, 100, new ImagickPixel('green')); + $this->assertEquals(3, $this->core->count()); + $result = $this->core->push(new Frame($im)); + $this->assertEquals(4, $this->core->count()); + $this->assertEquals(4, $result->count()); + } + + public function testGet(): void + { + $this->assertInstanceOf(Frame::class, $this->core->get(0)); + $this->assertInstanceOf(Frame::class, $this->core->get(1)); + $this->assertInstanceOf(Frame::class, $this->core->get(2)); + $this->assertNull($this->core->get(3)); + $this->assertEquals('foo', $this->core->get(3, 'foo')); + } + + public function testClear(): void + { + $result = $this->core->clear(); + $this->assertEquals(0, $this->core->count()); + $this->assertEquals(0, $result->count()); + } + + public function testSlice(): void + { + $this->assertEquals(3, $this->core->count()); + $result = $this->core->slice(1, 2); + $this->assertEquals(2, $this->core->count()); + $this->assertEquals(2, $result->count()); + } + + public function testFirst(): void + { + $this->assertInstanceOf(Frame::class, $this->core->first()); + } + + public function testLast(): void + { + $this->assertInstanceOf(Frame::class, $this->core->last()); + } + + public function testClone(): void + { + $cloned = clone $this->core; + + $this->assertInstanceOf(Core::class, $cloned); + $this->assertEquals($this->core->count(), $cloned->count()); + + // Verify the underlying Imagick objects are independent + $this->assertNotSame($this->core->native(), $cloned->native()); + } + + public function testMeta(): void + { + $meta = $this->core->meta(); + $this->assertInstanceOf(CollectionInterface::class, $meta); + } + + public function testToArray(): void + { + $array = $this->core->toArray(); + $this->assertIsArray($array); + $this->assertCount(3, $array); + foreach ($array as $frame) { + $this->assertInstanceOf(Frame::class, $frame); + } + } + + public function testSet(): void + { + $imagick = new Imagick(); + $imagick->newImage(100, 100, new ImagickPixel('yellow')); + $this->assertEquals(3, $this->core->count()); + $result = $this->core->set(0, new Frame($imagick)); + $this->assertInstanceOf(CollectionInterface::class, $result); + $this->assertEquals(4, $this->core->count()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Decoders/Base64ImageDecoderTest.php b/tests/Unit/Drivers/Imagick/Decoders/Base64ImageDecoderTest.php new file mode 100644 index 000000000..d6d5ac90f --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Decoders/Base64ImageDecoderTest.php @@ -0,0 +1,39 @@ +decoder = new Base64ImageDecoder(); + $this->decoder->setDriver(new Driver()); + } + + public function testDecode(): void + { + $result = $this->decoder->decode(Resource::create('blue.gif')->base64()); + $this->assertInstanceOf(Image::class, $result); + } + + public function testDecoderInvalid(): void + { + $this->expectException(DecoderException::class); + $this->decoder->decode('test'); + } +} diff --git a/tests/Unit/Drivers/Imagick/Decoders/BinaryImageDecoderTest.php b/tests/Unit/Drivers/Imagick/Decoders/BinaryImageDecoderTest.php new file mode 100644 index 000000000..cad15fe65 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Decoders/BinaryImageDecoderTest.php @@ -0,0 +1,90 @@ +decoder = new BinaryImageDecoder(); + $this->decoder->setDriver(new Driver()); + } + + public function testDecodePng(): void + { + $image = $this->decoder->decode(file_get_contents(Resource::create('tile.png')->path())); + $this->assertInstanceOf(Image::class, $image); + $this->assertInstanceOf(RgbColorspace::class, $image->colorspace()); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $this->assertCount(1, $image); + } + + public function testDecodeGif(): void + { + $image = $this->decoder->decode(file_get_contents(Resource::create('red.gif')->path())); + $this->assertInstanceOf(Image::class, $image); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $this->assertCount(1, $image); + } + + public function testDecodeAnimatedGif(): void + { + $image = $this->decoder->decode(file_get_contents(Resource::create('cats.gif')->path())); + $this->assertInstanceOf(Image::class, $image); + $this->assertEquals(75, $image->width()); + $this->assertEquals(50, $image->height()); + $this->assertCount(4, $image); + } + + public function testDecodeJpegWithExif(): void + { + $image = $this->decoder->decode(file_get_contents(Resource::create('exif.jpg')->path())); + $this->assertInstanceOf(Image::class, $image); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $this->assertCount(1, $image); + $this->assertEquals('Oliver Vogel', $image->exif('IFD0.Artist')); + } + + public function testDecodeCmykImage(): void + { + $image = $this->decoder->decode(file_get_contents(Resource::create('cmyk.jpg')->path())); + $this->assertInstanceOf(Image::class, $image); + $this->assertInstanceOf(CmykColorspace::class, $image->colorspace()); + } + + public function testDecodeStringable(): void + { + $image = $this->decoder->decode(Resource::create('tile.png')->stringableData()); + $this->assertInstanceOf(Image::class, $image); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $this->assertCount(1, $image); + } + + public function testDecodeNonString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->decoder->decode(new stdClass()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Decoders/DataUriImageDecoderTest.php b/tests/Unit/Drivers/Imagick/Decoders/DataUriImageDecoderTest.php new file mode 100644 index 000000000..e2e5d6182 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Decoders/DataUriImageDecoderTest.php @@ -0,0 +1,53 @@ +decoder = new DataUriImageDecoder(); + $this->decoder->setDriver(new Driver()); + } + + public function testDecode(): void + { + $result = $this->decoder->decode(Resource::create('blue.gif')->dataUri()); + $this->assertInstanceOf(Image::class, $result); + } + + public function testDecoderNonString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->decoder->decode(new stdClass()); + } + + public function testDecoderInvalid(): void + { + $this->expectException(InvalidArgumentException::class); + $this->decoder->decode('invalid'); + } + + public function testDecoderNonImage(): void + { + $this->expectException(DecoderException::class); + $this->decoder->decode('data:text/plain;charset=utf-8,test'); + } +} diff --git a/tests/Unit/Drivers/Imagick/Decoders/EncodedImageObjectDecoderTest.php b/tests/Unit/Drivers/Imagick/Decoders/EncodedImageObjectDecoderTest.php new file mode 100644 index 000000000..e432b765e --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Decoders/EncodedImageObjectDecoderTest.php @@ -0,0 +1,33 @@ +decoder = new EncodedImageObjectDecoder(); + $this->decoder->setDriver(new Driver()); + } + + public function testDecode(): void + { + $result = $this->decoder->decode(new EncodedImage(Resource::create()->data())); + $this->assertInstanceOf(Image::class, $result); + } +} diff --git a/tests/Unit/Drivers/Imagick/Decoders/FilePathImageDecoderTest.php b/tests/Unit/Drivers/Imagick/Decoders/FilePathImageDecoderTest.php new file mode 100644 index 000000000..5c77ae696 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Decoders/FilePathImageDecoderTest.php @@ -0,0 +1,59 @@ +decoder = new FilePathImageDecoder(); + $this->decoder->setDriver(new Driver()); + } + + #[DataProvider('validFormatPathsProvider')] + public function testDecode(string|Stringable $path, ?string $exception): void + { + if ($exception !== null) { + $this->expectException($exception); + } + + $result = $this->decoder->decode($path); + + if ($exception === null) { + $this->assertInstanceOf(Image::class, $result); + } + } + + public static function validFormatPathsProvider(): Generator + { + yield [Resource::create('cats.gif')->path(), null]; + yield [Resource::create('animation.gif')->path(), null]; + yield [Resource::create('red.gif')->path(), null]; + yield [Resource::create('green.gif')->path(), null]; + yield [Resource::create('blue.gif')->path(), null]; + yield [Resource::create('gradient.bmp')->path(), null]; + yield [Resource::create('circle.png')->path(), null]; + yield [Resource::create('circle.png')->stringablePath(), null]; + yield ['no-path', FileNotFoundException::class]; + yield [str_repeat('x', PHP_MAXPATHLEN + 1), InvalidArgumentException::class]; + } +} diff --git a/tests/Unit/Drivers/Imagick/Decoders/ImageObjectDecoderTest.php b/tests/Unit/Drivers/Imagick/Decoders/ImageObjectDecoderTest.php new file mode 100644 index 000000000..c2945d741 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Decoders/ImageObjectDecoderTest.php @@ -0,0 +1,23 @@ +decode($this->readTestImage('blue.gif')); + $this->assertInstanceOf(Image::class, $result); + } +} diff --git a/tests/Unit/Drivers/Imagick/Decoders/NativeObjectDecoderTest.php b/tests/Unit/Drivers/Imagick/Decoders/NativeObjectDecoderTest.php new file mode 100644 index 000000000..e397c36eb --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Decoders/NativeObjectDecoderTest.php @@ -0,0 +1,36 @@ +decoder = new NativeObjectDecoder(); + $this->decoder->setDriver(new Driver()); + } + + public function testDecode(): void + { + $native = new Imagick(); + $native->newImage(3, 2, new ImagickPixel('red'), 'png'); + $result = $this->decoder->decode($native); + + $this->assertInstanceOf(Image::class, $result); + } +} diff --git a/tests/Unit/Drivers/Imagick/Decoders/SplFileInfoImageDecoderTest.php b/tests/Unit/Drivers/Imagick/Decoders/SplFileInfoImageDecoderTest.php new file mode 100644 index 000000000..87293afe2 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Decoders/SplFileInfoImageDecoderTest.php @@ -0,0 +1,26 @@ +setDriver(new Driver()); + $result = $decoder->decode(Resource::create('blue.gif')->splFileInfo()); + $this->assertInstanceOf(Image::class, $result); + } +} diff --git a/tests/Unit/Drivers/Imagick/Decoders/StreamImageDecoderTest.php b/tests/Unit/Drivers/Imagick/Decoders/StreamImageDecoderTest.php new file mode 100644 index 000000000..3af94e2bc --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Decoders/StreamImageDecoderTest.php @@ -0,0 +1,26 @@ +setDriver(new Driver()); + $result = $decoder->decode(Resource::create('test.jpg')->stream()); + $this->assertInstanceOf(Image::class, $result); + } +} diff --git a/tests/Unit/Drivers/Imagick/DriverTest.php b/tests/Unit/Drivers/Imagick/DriverTest.php new file mode 100644 index 000000000..60450ef41 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/DriverTest.php @@ -0,0 +1,305 @@ +driver = new Driver(); + } + + public function testId(): void + { + $this->assertEquals('Imagick', $this->driver->id()); + } + + public function testCreateImage(): void + { + $image = $this->driver->createImage(3, 2); + $this->assertInstanceOf(ImageInterface::class, $image); + $this->assertEquals(3, $image->width()); + $this->assertEquals(2, $image->height()); + } + + /** + * @param array $decoders + */ + #[DataProviderExternal(InputDataProvider::class, 'decodeImageDataProvider')] + public function testHandleImageInput(mixed $input, ?array $decoders, string $resultClassname): void + { + $this->assertInstanceOf($resultClassname, $this->driver->decodeImage($input, $decoders)); + } + + /** + * @param array $decoders + */ + #[DataProviderExternal(InputDataProvider::class, 'decodeColorDataProvider')] + public function testHandleColorInput(mixed $input, ?array $decoders, string $resultClassname): void + { + $this->assertInstanceOf($resultClassname, $this->driver->decodeColor($input, $decoders)); + } + + /** + * @param array $decoders + */ + #[DataProviderExternal(InputDataProvider::class, 'decodeImageDataProvider')] + public function testHandleColorInputFail(mixed $input, ?array $decoders, string $resultClassname): void + { + $this->expectException(ImageException::class); + $this->driver->decodeColor($input); + } + + /** + * @param array $decoders + */ + #[DataProviderExternal(InputDataProvider::class, 'decodeColorDataProvider')] + public function testHandleImageInputFail(mixed $input, ?array $decoders, string $resultClassname): void + { + $this->expectException(ImageException::class); + $this->driver->decodeImage($input); + } + + public function testColorProcessor(): void + { + $result = $this->driver->colorProcessor(Resource::create()->imageObject(Driver::class)); + $this->assertInstanceOf(ColorProcessorInterface::class, $result); + } + + #[DataProvider('supportsDataProvider')] + public function testSupports(bool $result, mixed $identifier): void + { + $this->assertEquals($result, $this->driver->supports($identifier)); + } + + public static function supportsDataProvider(): Generator + { + yield [true, Format::JPEG]; + yield [true, MediaType::IMAGE_JPEG]; + yield [true, MediaType::IMAGE_JPG]; + yield [true, FileExtension::JPG]; + yield [true, FileExtension::JPEG]; + yield [true, 'jpg']; + yield [true, 'jpeg']; + yield [true, 'image/jpg']; + yield [true, 'image/jpeg']; + + yield [true, Format::WEBP]; + yield [true, MediaType::IMAGE_WEBP]; + yield [true, MediaType::IMAGE_X_WEBP]; + yield [true, FileExtension::WEBP]; + yield [true, 'webp']; + yield [true, 'image/webp']; + yield [true, 'image/x-webp']; + + yield [true, Format::GIF]; + yield [true, MediaType::IMAGE_GIF]; + yield [true, FileExtension::GIF]; + yield [true, 'gif']; + yield [true, 'image/gif']; + + yield [true, Format::PNG]; + yield [true, MediaType::IMAGE_PNG]; + yield [true, MediaType::IMAGE_X_PNG]; + yield [true, FileExtension::PNG]; + yield [true, 'png']; + yield [true, 'image/png']; + yield [true, 'image/x-png']; + + yield [true, Format::AVIF]; + yield [true, MediaType::IMAGE_AVIF]; + yield [true, MediaType::IMAGE_X_AVIF]; + yield [true, FileExtension::AVIF]; + yield [true, 'avif']; + yield [true, 'image/avif']; + yield [true, 'image/x-avif']; + + yield [true, Format::BMP]; + yield [true, FileExtension::BMP]; + yield [true, MediaType::IMAGE_BMP]; + yield [true, MediaType::IMAGE_MS_BMP]; + yield [true, MediaType::IMAGE_X_BITMAP]; + yield [true, MediaType::IMAGE_X_BMP]; + yield [true, MediaType::IMAGE_X_MS_BMP]; + yield [true, MediaType::IMAGE_X_WINDOWS_BMP]; + yield [true, MediaType::IMAGE_X_WIN_BITMAP]; + yield [true, MediaType::IMAGE_X_XBITMAP]; + yield [true, 'bmp']; + yield [true, 'image/bmp']; + yield [true, 'image/ms-bmp']; + yield [true, 'image/x-bitmap']; + yield [true, 'image/x-bmp']; + yield [true, 'image/x-ms-bmp']; + yield [true, 'image/x-windows-bmp']; + yield [true, 'image/x-win-bitmap']; + yield [true, 'image/x-xbitmap']; + + yield [true, Format::TIFF]; + yield [true, MediaType::IMAGE_TIFF]; + yield [true, FileExtension::TIFF]; + yield [true, FileExtension::TIF]; + yield [true, 'tif']; + yield [true, 'tiff']; + yield [true, 'image/tiff']; + + yield [true, Format::JP2]; + yield [true, MediaType::IMAGE_JP2]; + yield [true, MediaType::IMAGE_JPX]; + yield [true, MediaType::IMAGE_JPM]; + yield [true, FileExtension::TIFF]; + yield [true, FileExtension::TIF]; + yield [true, FileExtension::JP2]; + yield [true, FileExtension::J2K]; + yield [true, FileExtension::JPF]; + yield [true, FileExtension::JPM]; + yield [true, FileExtension::JPG2]; + yield [true, FileExtension::J2C]; + yield [true, FileExtension::JPC]; + yield [true, FileExtension::JPX]; + yield [true, 'jp2']; + yield [true, 'j2k']; + yield [true, 'jpf']; + yield [true, 'jpm']; + yield [true, 'jpg2']; + yield [true, 'j2c']; + yield [true, 'jpc']; + yield [true, 'jpx']; + + yield [true, Format::HEIC]; + yield [true, MediaType::IMAGE_HEIC]; + yield [true, MediaType::IMAGE_HEIF]; + yield [true, FileExtension::HEIC]; + yield [true, FileExtension::HEIF]; + yield [true, 'heic']; + yield [true, 'heif']; + yield [true, 'image/heic']; + yield [true, 'image/heif']; + + yield [false, 'tga']; + yield [false, 'image/tga']; + yield [false, 'image/x-targa']; + yield [false, 'foo']; + yield [false, '']; + } + + public function testVersion(): void + { + $this->assertTrue(is_string($this->driver->version())); + } + + public function testSpecializeModifier(): void + { + $this->assertInstanceOf( + ResizeModifier::class, + $this->driver->specializeModifier(new GenericResizeModifier(100)), + ); + + $this->assertInstanceOf( + ResizeModifier::class, + $this->driver->specializeModifier(new ResizeModifier(100)), + ); + } + + public function testSpecializeAnalyzer(): void + { + $this->assertInstanceOf( + WidthAnalyzer::class, + $this->driver->specializeAnalyzer(new GenericWidthAnalyzer()), + ); + + $this->assertInstanceOf( + WidthAnalyzer::class, + $this->driver->specializeAnalyzer(new WidthAnalyzer()), + ); + } + + public function testSpecializeEncoder(): void + { + $this->assertInstanceOf( + PngEncoder::class, + $this->driver->specializeEncoder(new GenericPngEncoder()), + ); + + $this->assertInstanceOf( + PngEncoder::class, + $this->driver->specializeEncoder(new PngEncoder()), + ); + } + + public function testSpecializeDecoder(): void + { + $this->assertInstanceOf( + FilePathImageDecoder::class, + $this->driver->specializeDecoder(new GenericFilePathImageDecoder()), + ); + + $this->assertInstanceOf( + FilePathImageDecoder::class, + $this->driver->specializeDecoder(new FilePathImageDecoder()), + ); + } + + public function testSpecializeFailure(): void + { + $this->expectException(NotSupportedException::class); + $this->driver->specializeAnalyzer(new class () implements AnalyzerInterface, SpecializableInterface + { + protected DriverInterface $driver; + + public function analyze(ImageInterface $image): mixed + { + return true; + } + + /** @return array **/ + public function specializationArguments(): array + { + return []; + } + + public function setDriver(DriverInterface $driver): SpecializableInterface + { + return $this; + } + + public function driver(): DriverInterface + { + return $this->driver; + } + }); + } +} diff --git a/tests/Unit/Drivers/Imagick/Encoders/AvifEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/AvifEncoderTest.php new file mode 100644 index 000000000..d24e5e3fd --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Encoders/AvifEncoderTest.php @@ -0,0 +1,26 @@ +createTestImage(3, 2); + $encoder = new AvifEncoder(10); + $encoder->setDriver(new Driver()); + $result = $encoder->encode($image); + $this->assertMediaType('image/avif', $result); + $this->assertEquals('image/avif', $result->mimetype()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Encoders/BmpEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/BmpEncoderTest.php new file mode 100644 index 000000000..888b7d5c4 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Encoders/BmpEncoderTest.php @@ -0,0 +1,24 @@ +createTestImage(3, 2); + $encoder = new BmpEncoder(); + $result = $encoder->encode($image); + $this->assertMediaType(['image/bmp', 'image/x-ms-bmp'], $result); + $this->assertEquals('image/bmp', $result->mimetype()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Encoders/GifEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/GifEncoderTest.php new file mode 100644 index 000000000..cb316b969 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Encoders/GifEncoderTest.php @@ -0,0 +1,52 @@ +createTestImage(3, 2); + $encoder = new GifEncoder(); + $result = $encoder->encode($image); + $this->assertMediaType('image/gif', $result); + $this->assertEquals('image/gif', $result->mimetype()); + $this->assertFalse( + Decoder::decode((string) $result)->firstFrame()->imageDescriptor()->isInterlaced() + ); + } + + public function testEncodeInterlaced(): void + { + $image = $this->createTestImage(3, 2); + $encoder = new GifEncoder(interlaced: true); + $result = $encoder->encode($image); + $this->assertMediaType('image/gif', $result); + $this->assertEquals('image/gif', $result->mimetype()); + $this->assertTrue( + Decoder::decode((string) $result)->firstFrame()->imageDescriptor()->isInterlaced() + ); + } + + public function testEncodeInterlacedAnimation(): void + { + $image = $this->createTestAnimation(); + $encoder = new GifEncoder(interlaced: true); + $result = $encoder->encode($image); + $this->assertMediaType('image/gif', $result); + $this->assertEquals('image/gif', $result->mimetype()); + $this->assertTrue( + Decoder::decode((string) $result)->firstFrame()->imageDescriptor()->isInterlaced() + ); + } +} diff --git a/tests/Unit/Drivers/Imagick/Encoders/HeicEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/HeicEncoderTest.php new file mode 100644 index 000000000..fb7adf46f --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Encoders/HeicEncoderTest.php @@ -0,0 +1,26 @@ +createTestImage(3, 2); + $encoder = new HeicEncoder(75); + $encoder->setDriver(new Driver()); + $result = $encoder->encode($image); + $this->assertMediaType('image/heic', $result); + $this->assertEquals('image/heic', $result->mimetype()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Encoders/Jpeg2000EncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/Jpeg2000EncoderTest.php new file mode 100644 index 000000000..353d2ef87 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Encoders/Jpeg2000EncoderTest.php @@ -0,0 +1,26 @@ +createTestImage(3, 2); + $encoder = new Jpeg2000Encoder(75); + $encoder->setDriver(new Driver()); + $result = $encoder->encode($image); + $this->assertMediaType('image/jp2', $result); + $this->assertEquals('image/jp2', $result->mimetype()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Encoders/JpegEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/JpegEncoderTest.php new file mode 100644 index 000000000..9b095349e --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Encoders/JpegEncoderTest.php @@ -0,0 +1,71 @@ +createTestImage(3, 2); + $encoder = new JpegEncoder(75); + $encoder->setDriver(new Driver()); + $result = $encoder->encode($image); + $this->assertMediaType('image/jpeg', $result); + $this->assertEquals('image/jpeg', $result->mimetype()); + } + + public function testEncodeProgressive(): void + { + $image = $this->createTestImage(3, 2); + $encoder = new JpegEncoder(progressive: true); + $encoder->setDriver(new Driver()); + $result = $encoder->encode($image); + $this->assertMediaType('image/jpeg', $result); + $this->assertEquals('image/jpeg', $result->mimetype()); + $this->assertTrue($this->isProgressiveJpeg($result)); + } + + public function testEncodeStripExif(): void + { + $image = $this->readTestImage('exif.jpg'); + $this->assertEquals('Oliver Vogel', $image->exif('IFD0.Artist')); + + $encoder = new JpegEncoder(strip: true); + $encoder->setDriver(new Driver()); + $result = $encoder->encode($image); + $this->assertMediaType('image/jpeg', $result); + $this->assertEquals('image/jpeg', $result->mimetype()); + + $this->assertEmpty(exif_read_data($result->toStream())['IFD0.Artist'] ?? null); + } + + public function testEncodeStripExifKeepICCProfiles(): void + { + $image = $this->readTestImage('cmyk.jpg'); + $this->assertNotEmpty($image->core()->native()->getImageProfiles('icc')); + + $encoder = new JpegEncoder(strip: true); + $encoder->setDriver(new Driver()); + $result = $encoder->encode($image); + + $decoder = new StreamImageDecoder(); + $decoder->setDriver(new Driver()); + + $image = $decoder->decode($result->toStream()); + $this->assertNotEmpty($image->core()->native()->getImageProfiles('icc')); + } +} diff --git a/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php new file mode 100644 index 000000000..742cd3b55 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php @@ -0,0 +1,100 @@ +createTestImage(3, 2); + $encoder = new PngEncoder(); + $result = $encoder->encode($image); + $this->assertMediaType('image/png', $result); + $this->assertEquals('image/png', $result->mimetype()); + $this->assertFalse($this->isInterlacedPng($result)); + } + + public function testEncodeInterlaced(): void + { + $image = $this->createTestImage(3, 2); + $encoder = new PngEncoder(interlaced: true); + $result = $encoder->encode($image); + $this->assertMediaType('image/png', $result); + $this->assertEquals('image/png', $result->mimetype()); + $this->assertTrue($this->isInterlacedPng($result)); + } + + #[DataProvider('indexedDataProvider')] + public function testEncoderIndexed(ImageInterface $image, PngEncoder $encoder, string $result): void + { + $this->assertEquals( + $result, + $this->pngColorType($encoder->encode($image)), + ); + } + + public static function indexedDataProvider(): Generator + { + yield [ + static::createTestImage(3, 2), // new + new PngEncoder(indexed: false), + 'truecolor-alpha', + ]; + yield [ + static::createTestImage(3, 2), // new + new PngEncoder(indexed: true), + 'indexed', + ]; + + yield [ + static::createTestImage(3, 2)->fill('ccc'), // new grayscale + new PngEncoder(indexed: true), + 'indexed', + ]; + yield [ + static::readTestImage('circle.png'), // truecolor-alpha + new PngEncoder(indexed: false), + 'truecolor-alpha', + ]; + yield [ + static::readTestImage('circle.png'), // indexedcolor-alpha + new PngEncoder(indexed: true), + 'grayscale-alpha', // result should be 'indexed' but there seems to be no way to force this with imagick + ]; + yield [ + static::readTestImage('tile.png'), // indexed + new PngEncoder(indexed: false), + 'truecolor-alpha', + ]; + yield [ + static::readTestImage('tile.png'), // indexed + new PngEncoder(indexed: true), + 'indexed', + ]; + yield [ + static::readTestImage('test.jpg'), // jpeg + new PngEncoder(indexed: false), + 'truecolor-alpha', + ]; + yield [ + static::readTestImage('test.jpg'), // jpeg + new PngEncoder(indexed: true), + 'indexed', + ]; + } +} diff --git a/tests/Unit/Drivers/Imagick/Encoders/TiffEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/TiffEncoderTest.php new file mode 100644 index 000000000..15086631d --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Encoders/TiffEncoderTest.php @@ -0,0 +1,26 @@ +createTestImage(3, 2); + $encoder = new TiffEncoder(); + $encoder->setDriver(new Driver()); + $result = $encoder->encode($image); + $this->assertMediaType('image/tiff', $result); + $this->assertEquals('image/tiff', $result->mimetype()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Encoders/WebpEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/WebpEncoderTest.php new file mode 100644 index 000000000..456fdb082 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Encoders/WebpEncoderTest.php @@ -0,0 +1,26 @@ +createTestImage(3, 2); + $encoder = new WebpEncoder(75); + $encoder->setDriver(new Driver()); + $result = $encoder->encode($image); + $this->assertMediaType('image/webp', $result); + $this->assertEquals('image/webp', $result->mimetype()); + } +} diff --git a/tests/Unit/Drivers/Imagick/FontProcessorTest.php b/tests/Unit/Drivers/Imagick/FontProcessorTest.php new file mode 100644 index 000000000..615a55064 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/FontProcessorTest.php @@ -0,0 +1,79 @@ +boxSize( + 'ABC', + $this->testFont()->setSize(120), + ); + + $this->assertInstanceOf(SizeInterface::class, $size); + $this->assertEquals(163, $size->width()); + $this->assertEquals(72, $size->height()); + } + + public function testNativeFontSize(): void + { + $processor = new FontProcessor(); + $font = new Font(); + $font->setSize(14.2); + $size = $processor->nativeFontSize($font); + $this->assertEquals(14.2, $size); + } + + public function testTextBlock(): void + { + $processor = new FontProcessor(); + $result = $processor->textBlock( + 'test', + $this->testFont(), + new Point(0, 0), + ); + $this->assertInstanceOf(TextBlock::class, $result); + } + + public function testTypographicalSize(): void + { + $processor = new FontProcessor(); + $result = $processor->typographicalSize($this->testFont()); + $this->assertEquals(7, $result); + } + + public function testCapHeight(): void + { + $processor = new FontProcessor(); + $result = $processor->capHeight($this->testFont()); + $this->assertEquals(7, $result); + } + + public function testLeading(): void + { + $processor = new FontProcessor(); + $result = $processor->leading($this->testFont()); + $this->assertEquals(9, $result); + } + + private function testFont(): Font + { + return new Font(Resource::create('test.ttf')->path()); + } +} diff --git a/tests/Unit/Drivers/Imagick/FrameTest.php b/tests/Unit/Drivers/Imagick/FrameTest.php new file mode 100644 index 000000000..eac46d6d4 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/FrameTest.php @@ -0,0 +1,125 @@ +newImage(3, 2, new ImagickPixel('red'), 'png'); + $imagick->setImageDelay(125); // 1.25 seconds + $imagick->setImageDispose(0); + $imagick->setImagePage(3, 2, 8, 9); + + return new Frame($imagick); + } + + public function testConstructor(): void + { + $frame = $this->getTestFrame(); + $this->assertInstanceOf(Frame::class, $frame); + } + + public function testGetSize(): void + { + $frame = $this->getTestFrame(); + $this->assertInstanceOf(Size::class, $frame->size()); + } + + public function testSetGetDelay(): void + { + $frame = $this->getTestFrame(); + $this->assertEquals(1.25, $frame->delay()); + + $result = $frame->setDelay(2.5); + $this->assertInstanceOf(Frame::class, $result); + $this->assertEquals(2.5, $frame->delay()); + $this->assertEquals(250, $frame->native()->getImageDelay()); + } + + public function testSetGetDisposalMethod(): void + { + $frame = $this->getTestFrame(); + $this->assertEquals(0, $frame->disposalMethod()); + + $result = $frame->setDisposalMethod(3); + $this->assertInstanceOf(Frame::class, $result); + $this->assertEquals(3, $frame->disposalMethod()); + } + + public function testSetGetOffsetLeft(): void + { + $frame = $this->getTestFrame(); + $this->assertEquals(8, $frame->offsetLeft()); + + $result = $frame->setOffsetLeft(100); + $this->assertInstanceOf(Frame::class, $result); + $this->assertEquals(100, $frame->offsetLeft()); + } + + public function testSetGetOffsetTop(): void + { + $frame = $this->getTestFrame(); + $this->assertEquals(9, $frame->offsetTop()); + + $result = $frame->setOffsetTop(100); + $this->assertInstanceOf(Frame::class, $result); + $this->assertEquals(100, $frame->offsetTop()); + } + + public function testSetGetOffset(): void + { + $frame = $this->getTestFrame(); + $this->assertEquals(8, $frame->offsetLeft()); + $this->assertEquals(9, $frame->offsetTop()); + + $result = $frame->setOffset(100, 200); + $this->assertInstanceOf(Frame::class, $result); + $this->assertEquals(100, $frame->offsetLeft()); + $this->assertEquals(200, $frame->offsetTop()); + } + + public function testToImage(): void + { + $frame = $this->getTestFrame(); + $this->assertInstanceOf(Image::class, $frame->toImage(new Driver())); + } + + public function testSetGetNative(): void + { + $frame = $this->getTestFrame(); + $this->assertInstanceOf(Imagick::class, $frame->native()); + + $imagick = new Imagick(); + $imagick->newImage(5, 5, new ImagickPixel('blue'), 'png'); + $result = $frame->setNative($imagick); + $this->assertInstanceOf(Frame::class, $result); + $this->assertSame($imagick, $frame->native()); + } + + public function testDebugInfo(): void + { + $frame = $this->getTestFrame(); + $info = $frame->__debugInfo(); + $this->assertIsArray($info); + $this->assertArrayHasKey('delay', $info); + $this->assertArrayHasKey('left', $info); + $this->assertArrayHasKey('top', $info); + $this->assertArrayHasKey('disposalMethod', $info); + } +} diff --git a/tests/Unit/Drivers/Imagick/ImageTest.php b/tests/Unit/Drivers/Imagick/ImageTest.php new file mode 100644 index 000000000..2f3392411 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/ImageTest.php @@ -0,0 +1,748 @@ +readImage(Resource::create('animation.gif')->path()); + $this->image = (new Image( + new Driver(), + new Core($imagick), + ))->setExif( + new Collection([ + 'test' => 'foo' + ]) + ); + } + + public function testClone(): void + { + $image = $this->readTestImage('gradient.gif'); + $clone = clone $image; + + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $clone->width()); + $result = $clone->crop(4, 4); + $this->assertEquals(16, $image->width()); + $this->assertEquals(4, $clone->width()); + $this->assertEquals(4, $result->width()); + + $this->assertEquals('ff0000', $image->colorAt(0, 0)->toHex()); + $this->assertTransparency($image->colorAt(1, 0)); + + $this->assertEquals('ff0000', $clone->colorAt(0, 0)->toHex()); + $this->assertTransparency($clone->colorAt(1, 0)); + } + + public function testDriver(): void + { + $this->assertInstanceOf(Driver::class, $this->image->driver()); + } + + public function testCore(): void + { + $this->assertInstanceOf(Core::class, $this->image->core()); + } + + public function testCount(): void + { + $this->assertEquals(8, $this->image->count()); + } + + public function testIteration(): void + { + foreach ($this->image as $frame) { + $this->assertInstanceOf(Frame::class, $frame); + } + } + + public function testIsAnimated(): void + { + $this->assertTrue($this->image->isAnimated()); + } + + public function testSetGetLoops(): void + { + $this->assertEquals(3, $this->image->loops()); + $result = $this->image->setLoops(10); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(10, $this->image->loops()); + } + + public function testSetGetOrigin(): void + { + $origin = $this->image->origin(); + $this->assertInstanceOf(Origin::class, $origin); + $this->image->setOrigin(new Origin('test1', 'test2')); + $this->assertInstanceOf(Origin::class, $this->image->origin()); + $this->assertEquals('test1', $this->image->origin()->mediaType()); + $this->assertEquals('test2', $this->image->origin()->filePath()); + } + + public function testRemoveAnimation(): void + { + $this->assertTrue($this->image->isAnimated()); + $result = $this->image->removeAnimation(); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertFalse($this->image->isAnimated()); + } + + public function testSliceAnimation(): void + { + $this->assertEquals(8, $this->image->count()); + $result = $this->image->sliceAnimation(0, 2); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(2, $this->image->count()); + } + + public function testExif(): void + { + $this->assertInstanceOf(Collection::class, $this->image->exif()); + $this->assertEquals('foo', $this->image->exif('test')); + } + + public function testModify(): void + { + $result = $this->image->modify(new GrayscaleModifier()); + $this->assertInstanceOf(Image::class, $result); + } + + public function testAnalyze(): void + { + $result = $this->image->analyze(new WidthAnalyzer()); + $this->assertEquals(20, $result); + } + + public function testEncode(): void + { + $result = $this->image->encode(new PngEncoder()); + $this->assertInstanceOf(EncodedImage::class, $result); + } + + public function testAutoEncode(): void + { + $result = $this->readTestImage('blue.gif')->encode(); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/gif', $result); + } + + public function testEncodeByMediaType(): void + { + $result = $this->readTestImage('blue.gif')->encodeUsingMediaType('image/png'); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/png', $result); + } + + public function testEncodeByExtension(): void + { + $result = $this->readTestImage('blue.gif')->encodeUsingFileExtension('png'); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/png', $result); + } + + public function testEncodeByPath(): void + { + $result = $this->readTestImage('blue.gif')->encodeUsingPath('foo/bar.png'); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/png', $result); + } + + public function testSaveAsFormat(): void + { + $path = __DIR__ . '/tmp.png'; + $result = $this->readTestImage('blue.gif')->save($path); + $this->assertInstanceOf(Image::class, $result); + $this->assertFileExists($path); + $this->assertMediaType('image/png', file_get_contents($path)); + unlink($path); + } + + public function testSaveFallback(): void + { + $path = __DIR__ . '/tmp.unknown'; + $this->expectException(NotSupportedException::class); + $this->readTestImage('blue.gif')->save($path); + } + + public function testSaveUndeterminedPath(): void + { + $this->expectException(EncoderException::class); + $this->createTestImage(2, 3)->save(); + } + + public function testWidthHeightSize(): void + { + $this->assertEquals(20, $this->image->width()); + $this->assertEquals(15, $this->image->height()); + $this->assertInstanceOf(SizeInterface::class, $this->image->size()); + } + + public function testSetGetColorspace(): void + { + $this->assertInstanceOf(ColorspaceInterface::class, $this->image->colorspace()); + $this->assertInstanceOf(RgbColorspace::class, $this->image->colorspace()); + $result = $this->image->setColorspace(CmykColorspace::class); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertInstanceOf(CmykColorspace::class, $this->image->colorspace()); + } + + public function testSetGetResolution(): void + { + $resolution = $this->image->resolution(); + $this->assertInstanceOf(ResolutionInterface::class, $resolution); + $this->assertEquals(0, $resolution->x()); + $this->assertEquals(0, $resolution->y()); + $result = $this->image->setResolution(300, 300); + $resolution = $this->image->resolution(); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(300, $resolution->x()); + $this->assertEquals(300, $resolution->y()); + } + + public function testPickColor(): void + { + $this->assertInstanceOf(ColorInterface::class, $this->image->colorAt(0, 0)); + $this->assertInstanceOf(ColorInterface::class, $this->image->colorAt(0, 0, 1)); + } + + public function testPickColors(): void + { + $result = $this->image->colorsAt(0, 0); + $this->assertInstanceOf(Collection::class, $result); + $this->assertEquals(8, $result->count()); + } + + public function testProfile(): void + { + $this->expectException(AnalyzerException::class); + $this->image->profile(); + } + + public function testReduceColors(): void + { + $image = $this->readTestImage(); + $result = $image->reduceColors(8); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testSharpen(): void + { + $this->assertInstanceOf(Image::class, $this->image->sharpen(12)); + } + + public function testBackgroundDefault(): void + { + $image = $this->readTestImage('gradient.gif'); + $this->assertColor(0, 0, 0, 0, $image->colorAt(1, 0)); + $result = $image->fillTransparentAreas(); + $this->assertColor(255, 255, 255, 255, $image->colorAt(1, 0)); + $this->assertColor(255, 255, 255, 255, $result->colorAt(1, 0)); + } + + public function testBackgroundArgument(): void + { + $image = $this->readTestImage('gradient.gif'); + $this->assertColor(0, 0, 0, 0, $image->colorAt(1, 0)); + $result = $image->fillTransparentAreas('ff5500'); + $this->assertColor(255, 85, 0, 255, $image->colorAt(1, 0)); + $this->assertColor(255, 85, 0, 255, $result->colorAt(1, 0)); + } + + public function testBackgroundIgnoreTransparencyInBackgroundColor(): void + { + $image = $this->readTestImage('gradient.gif'); + $this->assertColor(0, 0, 0, 0, $image->colorAt(1, 0)); + $result = $image->fillTransparentAreas('ff550033'); + $this->assertColor(255, 85, 0, 51, $image->colorAt(1, 0), 1); + $this->assertColor(255, 85, 0, 51, $result->colorAt(1, 0), 1); + } + + public function testInvert(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(0, 0)->toHex()); + $this->assertEquals('ffa601', $image->colorAt(25, 25)->toHex()); + $result = $image->invert(); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals('ff510f', $image->colorAt(0, 0)->toHex()); + $this->assertEquals('0059fe', $image->colorAt(25, 25)->toHex()); + } + + public function testPixelate(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->pixelate(10); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testGrayscale(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertFalse($image->colorAt(0, 0)->isGrayscale()); + $result = $image->grayscale(); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertTrue($image->colorAt(0, 0)->isGrayscale()); + } + + public function testBrightness(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $result = $image->brightness(30); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals('39c9ff', $image->colorAt(14, 14)->toHex()); + } + + public function testDebugInfo(): void + { + $info = $this->readTestImage('trim.png')->__debugInfo(); + $this->assertArrayHasKey('width', $info); + $this->assertArrayHasKey('height', $info); + } + + public function testContrast(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->contrast(10); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testGamma(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->gamma(1.5); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testColorize(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->colorize(10, 20, 30); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testFlip(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->flip(); + $this->assertInstanceOf(ImageInterface::class, $result); + + $result = $image->flip(Direction::VERTICAL); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testBlur(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->blur(5); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testRotate(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->rotate(45); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testOrient(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->orient(); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testTrim(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->trim(10); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testInsert(): void + { + $image = $this->readTestImage('trim.png'); + $watermark = $this->createTestImage(5, 5); + $result = $image->insert($watermark); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testInsertWithAlignment(): void + { + $image = $this->readTestImage('trim.png'); + $watermark = $this->createTestImage(5, 5); + $result = $image->insert($watermark, 10, 10, Alignment::CENTER, .5); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testFill(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->fill('ff0000'); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testFillAtPosition(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->fill('ff0000', 0, 0); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testResize(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->resize(100, 100); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(100, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testResizeWithFraction(): void + { + $image = $this->readTestImage('trim.png'); + $originalWidth = $image->width(); + $originalHeight = $image->height(); + $result = $image->resize(Fraction::HALF, Fraction::HALF); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals((int) round($originalWidth * 0.5), $result->width()); + $this->assertEquals((int) round($originalHeight * 0.5), $result->height()); + } + + public function testResizeDown(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->resizeDown(100, 100); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testScale(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->scale(100); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testScaleWithFraction(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->scale(Fraction::DOUBLE); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testScaleDown(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->scaleDown(100); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testCover(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->cover(10, 10); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(10, $result->width()); + $this->assertEquals(10, $result->height()); + } + + public function testCoverWithFraction(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->cover(Fraction::HALF, Fraction::HALF); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testCoverDown(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->coverDown(10, 10); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(10, $result->width()); + $this->assertEquals(10, $result->height()); + } + + public function testContainDown(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->containDown(100, 100); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(100, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testContainDownWithBackground(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->containDown(100, 100, 'ff0000', Alignment::CENTER); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testContain(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->contain(100, 100); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(100, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testContainWithBackground(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->contain(100, 100, 'ff0000', Alignment::CENTER); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testCropWithAlignment(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->crop(10, 10, 0, 0, null, Alignment::CENTER); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(10, $result->width()); + $this->assertEquals(10, $result->height()); + } + + public function testCropWithFraction(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->crop(Fraction::HALF, Fraction::HALF); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testResizeCanvas(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->resizeCanvas(100, 100); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(100, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testResizeCanvasWithBackground(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->resizeCanvas(100, 100, 'ff0000', Alignment::CENTER); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testResizeCanvasRelative(): void + { + $image = $this->readTestImage('trim.png'); + $originalWidth = $image->width(); + $originalHeight = $image->height(); + $result = $image->resizeCanvasRelative(10, 10); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals($originalWidth + 10, $result->width()); + $this->assertEquals($originalHeight + 10, $result->height()); + } + + public function testResizeCanvasRelativeWithBackground(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->resizeCanvasRelative(10, 10, 'ff0000', Alignment::CENTER); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawPixel(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawPixel(5, 5, 'ff0000'); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawRectangle(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawRectangle(function ($rectangle): void { + $rectangle->size(5, 5); + $rectangle->background('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawRectangleObject(): void + { + $image = $this->createTestImage(10, 10); + $rect = new Rectangle(5, 5, new Point(0, 0)); + $result = $image->drawRectangle($rect); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawEllipse(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawEllipse(function ($ellipse): void { + $ellipse->size(6, 4); + $ellipse->background('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawCircle(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawCircle(function ($circle): void { + $circle->radius(3); + $circle->background('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawPolygon(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawPolygon(function ($polygon): void { + $polygon->point(0, 0); + $polygon->point(5, 0); + $polygon->point(5, 5); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawLine(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawLine(function ($line): void { + $line->from(0, 0); + $line->to(9, 9); + $line->color('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawBezier(): void + { + $image = $this->createTestImage(10, 10); + $result = $image->drawBezier(function ($bezier): void { + $bezier->point(0, 0); + $bezier->point(3, 5); + $bezier->point(6, 2); + $bezier->point(9, 9); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithRectangle(): void + { + $image = $this->createTestImage(10, 10); + $rect = new Rectangle(5, 5, new Point(0, 0)); + $result = $image->draw($rect); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithEllipse(): void + { + $image = $this->createTestImage(10, 10); + $ellipse = new Ellipse(6, 4, new Point(5, 5)); + $result = $image->draw($ellipse); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithCircle(): void + { + $image = $this->createTestImage(10, 10); + $circle = new Circle(6, new Point(5, 5)); + $result = $image->draw($circle); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithLine(): void + { + $image = $this->createTestImage(10, 10); + $line = new Line(new Point(0, 0), new Point(9, 9)); + $result = $image->draw($line); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithBezier(): void + { + $image = $this->createTestImage(10, 10); + $bezier = new Bezier([new Point(0, 0), new Point(3, 5), new Point(6, 2), new Point(9, 9)]); + $result = $image->draw($bezier); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithPolygon(): void + { + $image = $this->createTestImage(10, 10); + $polygon = new Polygon([new Point(0, 0), new Point(5, 0), new Point(5, 5)]); + $result = $image->draw($polygon); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testEncodeUsingFormat(): void + { + $image = $this->readTestImage('blue.gif'); + $result = $image->encodeUsingFormat(Format::PNG); + $this->assertInstanceOf(EncodedImage::class, $result); + $this->assertMediaType('image/png', $result); + } + + public function testBackgroundColor(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->backgroundColor(); + $this->assertInstanceOf(ColorInterface::class, $result); + } + + public function testSetBackgroundColor(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->setBackgroundColor('ff0000'); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testRemoveProfile(): void + { + $image = $this->readTestImage('trim.png'); + $result = $image->removeProfile(); + $this->assertInstanceOf(ImageInterface::class, $result); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/BlurModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/BlurModifierTest.php new file mode 100644 index 000000000..741570476 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/BlurModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $image->modify(new BlurModifier(30)); + $this->assertEquals('42acb2', $image->colorAt(14, 14)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/BrightnessModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/BrightnessModifierTest.php new file mode 100644 index 000000000..2f65584b0 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/BrightnessModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $image->modify(new BrightnessModifier(30)); + $this->assertEquals('39c9ff', $image->colorAt(14, 14)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/ColorizeModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/ColorizeModifierTest.php new file mode 100644 index 000000000..d2bc1c699 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/ColorizeModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('tile.png'); + $image = $image->modify(new ColorizeModifier(100, -100, -100)); + $this->assertColor(251, 0, 0, 255, $image->colorAt(5, 5)); + $this->assertColor(239, 0, 0, 255, $image->colorAt(15, 15)); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/ContainDownModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/ContainDownModifierTest.php new file mode 100644 index 000000000..ee86a4621 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/ContainDownModifierTest.php @@ -0,0 +1,42 @@ +readTestImage('blue.gif'); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $image->modify(new ContainDownModifier(30, 20, 'f00')); + $this->assertEquals(30, $image->width()); + $this->assertEquals(20, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(0, 19)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(29, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(29, 19)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(6, 2)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(7, 1)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(6, 17)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(7, 18)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(23, 1)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(23, 2)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(23, 17)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(23, 18)); + $this->assertColor(100, 100, 255, 255, $image->colorAt(7, 2)); + $this->assertColor(100, 100, 255, 255, $image->colorAt(22, 2)); + $this->assertColor(100, 100, 255, 255, $image->colorAt(7, 17)); + $this->assertColor(100, 100, 255, 255, $image->colorAt(22, 17)); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/ContainModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/ContainModifierTest.php new file mode 100644 index 000000000..3b53481ff --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/ContainModifierTest.php @@ -0,0 +1,34 @@ +readTestImage('blocks.png'); + $this->assertEquals(640, $image->width()); + $this->assertEquals(480, $image->height()); + $result = $image->modify(new ContainModifier(200, 100, 'ff0')); + $this->assertEquals(200, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(0, 0, 0, 0, $image->colorAt(140, 10)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(175, 10)); + $this->assertEquals(200, $result->width()); + $this->assertEquals(100, $result->height()); + $this->assertColor(255, 255, 0, 255, $result->colorAt(0, 0)); + $this->assertColor(0, 0, 0, 0, $result->colorAt(140, 10)); + $this->assertColor(255, 255, 0, 255, $result->colorAt(175, 10)); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/ContrastModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/ContrastModifierTest.php new file mode 100644 index 000000000..91e35dd90 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/ContrastModifierTest.php @@ -0,0 +1,52 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $image->modify(new ContrastModifier(30)); + $this->assertEquals('00cffc', $image->colorAt(14, 14)->toHex()); + } + + public function testApplyPreservesMidtone(): void + { + // sigmoidalContrastImage takes its midpoint argument in QuantumRange + // units. Passing 0 pivots the curve around pure black, lifting every + // pixel — including the midtone, which a contrast adjustment must keep + // fixed. After the fix the midpoint is QUANTUM_RANGE / 2 and a mid-grey + // pixel survives a contrast adjustment unchanged. + $image = $this->createMidGreyImage(); + $this->assertColor(128, 128, 128, 255, $image->colorAt(0, 0)); + $image->modify(new ContrastModifier(30)); + $this->assertColor(128, 128, 128, 255, $image->colorAt(0, 0), tolerance: 1); + } + + private function createMidGreyImage(): Image + { + $imagick = new Imagick(); + $imagick->newImage(1, 1, new ImagickPixel('rgb(128, 128, 128)'), 'png'); + $imagick->setImageType(Imagick::IMGTYPE_TRUECOLOR); + $imagick->setColorspace(Imagick::COLORSPACE_SRGB); + + return new Image(new Driver(), new Core($imagick)); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/CoverDownModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/CoverDownModifierTest.php new file mode 100644 index 000000000..8930fc40b --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/CoverDownModifierTest.php @@ -0,0 +1,39 @@ +readTestImage('blocks.png'); + $this->assertEquals(640, $image->width()); + $this->assertEquals(480, $image->height()); + $image->modify(new CoverDownModifier(100, 100, Alignment::CENTER)); + $this->assertEquals(100, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(90, 90)); + $this->assertColor(0, 255, 0, 255, $image->colorAt(65, 70)); + $this->assertColor(0, 0, 255, 255, $image->colorAt(70, 52)); + $this->assertTransparency($image->colorAt(90, 30)); + } + + public function testModifyOddSize(): void + { + $image = $this->createTestImage(375, 250); + $image->modify(new CoverDownModifier(240, 90, Alignment::CENTER)); + $this->assertEquals(240, $image->width()); + $this->assertEquals(90, $image->height()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/CoverModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/CoverModifierTest.php new file mode 100644 index 000000000..dcfa48e74 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/CoverModifierTest.php @@ -0,0 +1,39 @@ +readTestImage('blocks.png'); + $this->assertEquals(640, $image->width()); + $this->assertEquals(480, $image->height()); + $image->modify(new CoverModifier(100, 100, Alignment::CENTER)); + $this->assertEquals(100, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(90, 90)); + $this->assertColor(0, 255, 0, 255, $image->colorAt(65, 70)); + $this->assertColor(0, 0, 255, 255, $image->colorAt(70, 52)); + $this->assertTransparency($image->colorAt(90, 30)); + } + + public function testModifyOddSize(): void + { + $image = $this->createTestImage(375, 250); + $image->modify(new CoverModifier(240, 90, Alignment::CENTER)); + $this->assertEquals(240, $image->width()); + $this->assertEquals(90, $image->height()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/CropModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/CropModifierTest.php new file mode 100644 index 000000000..bd5ac51c6 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/CropModifierTest.php @@ -0,0 +1,100 @@ +readTestImage('blocks.png'); + $image = $image->modify(new CropModifier(200, 200, 0, 0, 'ffffff', Alignment::BOTTOM_RIGHT)); + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(5, 5)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(100, 100)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(190, 190)); + } + + public function testModifyExtend(): void + { + $image = $this->readTestImage('blocks.png'); + $image = $image->modify(new CropModifier(800, 100, -10, -10, 'ff0000', Alignment::TOP_LEFT)); + $this->assertEquals(800, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(9, 9)); + $this->assertColor(0, 0, 255, 255, $image->colorAt(16, 16)); + $this->assertColor(0, 0, 255, 255, $image->colorAt(445, 16)); + $this->assertTransparency($image->colorAt(460, 16)); + } + + public function testModifySinglePixel(): void + { + $image = $this->createTestImage(1, 1); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new CropModifier(3, 3, 0, 0, 'ff0', Alignment::CENTER)); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(2, 2)); + } + + public function testModifyKeepsColorspace(): void + { + $image = $this->readTestImage('cmyk.jpg'); + $this->assertInstanceOf(Cmyk::class, $image->colorspace()); + $image = $image->modify(new CropModifier(800, 100, -10, -10, 'ff0000')); + $this->assertInstanceOf(Cmyk::class, $image->colorspace()); + } + + public function testModifyKeepsResolution(): void + { + $image = $this->readTestImage('300dpi.png'); + $this->assertEquals(300, round($image->resolution()->perInch()->x())); + $image = $image->modify(new CropModifier(800, 100, -10, -10, 'ff0000')); + $this->assertEquals(300, round($image->resolution()->perInch()->x())); + } + + public function testHalfTransparent(): void + { + $image = $this->createTestImage(16, 16); + $image->modify(new CropModifier(32, 32, 0, 0, '00f3', Alignment::CENTER)); + $this->assertEquals(32, $image->width()); + $this->assertEquals(32, $image->height()); + $this->assertColor(0, 0, 255, 51, $image->colorAt(5, 5)); + $this->assertColor(0, 0, 255, 51, $image->colorAt(16, 5)); + $this->assertColor(0, 0, 255, 51, $image->colorAt(30, 5)); + $this->assertColor(0, 0, 255, 51, $image->colorAt(5, 16)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(16, 16)); + $this->assertColor(0, 0, 255, 51, $image->colorAt(30, 16)); + $this->assertColor(0, 0, 255, 51, $image->colorAt(5, 30)); + $this->assertColor(0, 0, 255, 51, $image->colorAt(16, 30)); + $this->assertColor(0, 0, 255, 51, $image->colorAt(30, 30)); + } + + public function testMergeTransparentBackgrounds(): void + { + $image = $this->createTestImage(1, 1)->fill('f00'); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new CropModifier(3, 3, 0, 0, '00f3', Alignment::CENTER)); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(0, 0, 255, 51, $image->colorAt(0, 0), 1); + $this->assertColor(255, 0, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(0, 0, 255, 51, $image->colorAt(2, 2), 1); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/DrawBezierModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/DrawBezierModifierTest.php new file mode 100644 index 000000000..bcfe60102 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/DrawBezierModifierTest.php @@ -0,0 +1,49 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Bezier([ + new Point(0, 0), + new Point(15, 0), + new Point(15, 15), + new Point(0, 15) + ]); + $drawable->setBackgroundColor('b53717'); + $image->modify(new DrawBezierModifier($drawable)); + $this->assertEquals('b53717', $image->colorAt(5, 5)->toHex()); + } + + public function testApplyWithoutBackgroundColor(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Bezier([ + new Point(15, 15), + new Point(30, 15), + new Point(30, 30), + new Point(15, 30) + ]); + $drawable->setBorder('fff', 5); + $image->modify(new DrawBezierModifier($drawable)); + $this->assertEquals('ffffff', $image->colorAt(26, 24)->toHex()); // border + $this->assertEquals('ffa601', $image->colorAt(19, 23)->toHex()); // background + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/DrawEllipseModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/DrawEllipseModifierTest.php new file mode 100644 index 000000000..8adefd418 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/DrawEllipseModifierTest.php @@ -0,0 +1,39 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Ellipse(10, 10, new Point(14, 14)); + $drawable->setBackgroundColor('b53717'); + $image->modify(new DrawEllipseModifier($drawable)); + $this->assertEquals('b53717', $image->colorAt(14, 14)->toHex()); + } + + public function testApplyWithoutBackground(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Ellipse(30, 30, new Point(14, 14)); + $drawable->setBorder('fff', 5); + $image->modify(new DrawEllipseModifier($drawable)); + $this->assertEquals('ffffff', $image->colorAt(5, 5)->toHex()); // border of circle + $this->assertEquals('ffa601', $image->colorAt(20, 20)->toHex()); // background of circle + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/DrawLineModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/DrawLineModifierTest.php new file mode 100644 index 000000000..6f9acf53f --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/DrawLineModifierTest.php @@ -0,0 +1,38 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $line = new Line(new Point(0, 0), new Point(10, 0), 4); + $line->setBackgroundColor('b53517'); + $image->modify(new DrawLineModifier($line)); + $this->assertEquals('b53517', $image->colorAt(0, 0)->toHex()); + } + + public function testApplyTransparent(): void + { + $image = $this->createTestImage(10, 10)->fill('ff5500'); + $this->assertColor(255, 85, 0, 255, $image->colorAt(5, 5)); + $line = new Line(new Point(0, 5), new Point(10, 5), 4); + $line->setBackgroundColor('fff4'); + $image->modify(new DrawLineModifier($line)); + $this->assertColor(255, 131, 69, 255, $image->colorAt(5, 5)); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/DrawPixelModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/DrawPixelModifierTest.php new file mode 100644 index 000000000..de0ae0137 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/DrawPixelModifierTest.php @@ -0,0 +1,25 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $image->modify(new DrawPixelModifier(new Point(14, 14), 'ffffff')); + $this->assertEquals('ffffff', $image->colorAt(14, 14)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/DrawPolygonModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/DrawPolygonModifierTest.php new file mode 100644 index 000000000..9c8317918 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/DrawPolygonModifierTest.php @@ -0,0 +1,39 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Polygon([new Point(0, 0), new Point(15, 15), new Point(20, 20)]); + $drawable->setBackgroundColor('b53717'); + $image->modify(new DrawPolygonModifier($drawable)); + $this->assertEquals('b53717', $image->colorAt(14, 14)->toHex()); + } + + public function testApplyWithoutBackgroundColor(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $drawable = new Polygon([new Point(10, 10), new Point(40, 10), new Point(40, 30)]); + $drawable->setBorder('fff', 4); + $image->modify(new DrawPolygonModifier($drawable)); + $this->assertEquals('ffffff', $image->colorAt(19, 10)->toHex()); // border + $this->assertEquals('ffa601', $image->colorAt(30, 17)->toHex()); // background + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/DrawRectangleModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/DrawRectangleModifierTest.php new file mode 100644 index 000000000..8c37f88eb --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/DrawRectangleModifierTest.php @@ -0,0 +1,51 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $rectangle = new Rectangle(300, 200, new Point(14, 14)); + $rectangle->setBackgroundColor('ffffff'); + $image->modify(new DrawRectangleModifier($rectangle)); + $this->assertEquals('ffffff', $image->colorAt(14, 14)->toHex()); + } + + public function testApplyWithBorder(): void + { + $image = $this->readTestImage('trim.png'); + $rectangle = new Rectangle(10, 10, new Point(0, 0)); + $rectangle->setBackgroundColor('ffffff'); + $rectangle->setBorder('ff0000', 1); + $image->modify(new DrawRectangleModifier($rectangle)); + $this->assertEquals('ff0000', $image->colorAt(0, 0)->toHex()); + } + + public function testApplyWithoutBackground(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(14, 14)->toHex()); + $rectangle = new Rectangle(30, 30, new Point(0, 0)); + $rectangle->setBorder('fff', 5); + $image->modify(new DrawRectangleModifier($rectangle)); + $this->assertEquals('ffffff', $image->colorAt(2, 2)->toHex()); // border + $this->assertEquals('ffa601', $image->colorAt(20, 20)->toHex()); // background + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/FillModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/FillModifierTest.php new file mode 100644 index 000000000..6dd29ca93 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/FillModifierTest.php @@ -0,0 +1,38 @@ +readTestImage('blocks.png'); + $this->assertEquals('0000ff', $image->colorAt(420, 270)->toHex()); + $this->assertEquals('ff0000', $image->colorAt(540, 400)->toHex()); + $image->modify(new FillModifier(new Color(204, 204, 204), new Point(540, 400))); + $this->assertEquals('0000ff', $image->colorAt(420, 270)->toHex()); + $this->assertEquals('cccccc', $image->colorAt(540, 400)->toHex()); + } + + public function testFillAllColor(): void + { + $image = $this->readTestImage('blocks.png'); + $this->assertEquals('0000ff', $image->colorAt(420, 270)->toHex()); + $this->assertEquals('ff0000', $image->colorAt(540, 400)->toHex()); + $image->modify(new FillModifier(new Color(204, 204, 204))); + $this->assertEquals('cccccc', $image->colorAt(420, 270)->toHex()); + $this->assertEquals('cccccc', $image->colorAt(540, 400)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/FlipModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/FlipModifierTest.php new file mode 100644 index 000000000..3eea1045d --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/FlipModifierTest.php @@ -0,0 +1,59 @@ +readTestImage('tile.png'); + $this->assertEquals('b4e000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('445160', $image->colorAt(12, 12)->toHex()); + $image->modify(new FlipModifier()); + $this->assertEquals('00000000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('b4e000', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('445160', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 12)->toHex()); + } + + public function testFlipImageHorizontal(): void + { + $image = $this->readTestImage('tile.png'); + $this->assertEquals('b4e000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('445160', $image->colorAt(12, 12)->toHex()); + $image->modify(new FlipModifier(Direction::HORIZONTAL)); + $this->assertEquals('00000000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('b4e000', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('445160', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 12)->toHex()); + } + + public function testFlipImageVertical(): void + { + $image = $this->readTestImage('tile.png'); + $this->assertEquals('b4e000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('00000000', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('445160', $image->colorAt(12, 12)->toHex()); + $image->modify(new FlipModifier(Direction::VERTICAL)); + $this->assertEquals('00000000', $image->colorAt(5, 5)->toHex()); + $this->assertEquals('445160', $image->colorAt(12, 5)->toHex()); + $this->assertEquals('b4e000', $image->colorAt(5, 12)->toHex()); + $this->assertEquals('00000000', $image->colorAt(12, 12)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/GammaModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/GammaModifierTest.php new file mode 100644 index 000000000..0926ff9ab --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/GammaModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(0, 0)->toHex()); + $image->modify(new GammaModifier(2.1)); + $this->assertEquals('00d5f8', $image->colorAt(0, 0)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/GrayscaleModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/GrayscaleModifierTest.php new file mode 100644 index 000000000..6ccf3e68e --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/GrayscaleModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $this->assertFalse($image->colorAt(0, 0)->isGrayscale()); + $image->modify(new GrayscaleModifier()); + $this->assertTrue($image->colorAt(0, 0)->isGrayscale()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/InsertModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/InsertModifierTest.php new file mode 100644 index 000000000..e1b1c2657 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/InsertModifierTest.php @@ -0,0 +1,44 @@ +readTestImage('test.jpg'); + $this->assertEquals('febc44', $image->colorAt(300, 25)->toHex()); + $image->modify(new InsertModifier(Resource::create('circle.png')->path(), 0, 0, Alignment::TOP_RIGHT)); + $this->assertEquals('33260e', $image->colorAt(300, 25)->toHex()); + } + + public function testColorChangeTransparencyPng(): void + { + $image = $this->readTestImage('test.jpg'); + $this->assertEquals('febc44', $image->colorAt(300, 25)->toHex()); + $image->modify(new InsertModifier(Resource::create('circle.png')->path(), 0, 0, Alignment::TOP_RIGHT, .5)); + $this->assertColor(152, 112, 40, 255, $image->colorAt(300, 25), tolerance: 1); + $this->assertColor(255, 202, 107, 255, $image->colorAt(274, 5), tolerance: 1); + } + + public function testColorChangeTransparencyJpeg(): void + { + $image = $this->createTestImage(16, 16)->fill('0000ff'); + $this->assertEquals('0000ff', $image->colorAt(10, 10)->toHex()); + $image->modify(new InsertModifier(Resource::create('exif.jpg')->path(), transparency: .5)); + $this->assertColor(127, 83, 127, 255, $image->colorAt(10, 10), tolerance: 1); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/InvertModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/InvertModifierTest.php new file mode 100644 index 000000000..18749e918 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/InvertModifierTest.php @@ -0,0 +1,36 @@ +readTestImage('trim.png'); + $this->assertEquals('00aef0', $image->colorAt(0, 0)->toHex()); + $this->assertEquals('ffa601', $image->colorAt(25, 25)->toHex()); + $image->modify(new InvertModifier()); + $this->assertEquals('ff510f', $image->colorAt(0, 0)->toHex()); + $this->assertEquals('0059fe', $image->colorAt(25, 25)->toHex()); + } + + public function testApplyPreservesAlphaChannel(): void + { + $image = $this->readTestImage('circle.png'); + $this->assertColor(0, 0, 0, 0, $image->colorAt(0, 0)); + $this->assertColor(0, 0, 0, 204, $image->colorAt(25, 25)); + $image->modify(new InvertModifier()); + $this->assertColor(255, 255, 255, 0, $image->colorAt(0, 0)); + $this->assertColor(255, 255, 255, 204, $image->colorAt(25, 25)); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/PixelateModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/PixelateModifierTest.php new file mode 100644 index 000000000..208de1a5b --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/PixelateModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $result = $image->modify(new PixelateModifier(10)); + $this->assertInstanceOf(ImageInterface::class, $result); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/ReduceColorsModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/ReduceColorsModifierTest.php new file mode 100644 index 000000000..d3c74835d --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/ReduceColorsModifierTest.php @@ -0,0 +1,47 @@ +readTestImage('gradient.bmp'); + $this->assertEquals(15, $image->core()->native()->getImageColors()); + $image->modify(new ReduceColorsModifier(4)); + $this->assertEquals(4, $image->core()->native()->getImageColors()); + } + + public function testNoColorReduction(): void + { + $image = $this->readTestImage('gradient.bmp'); + $this->assertEquals(15, $image->core()->native()->getImageColors()); + $image->modify(new ReduceColorsModifier(150)); + $this->assertEquals(15, $image->core()->native()->getImageColors()); + } + + public function testInvalidColorInput(): void + { + $image = $this->readTestImage('gradient.bmp'); + $this->expectException(InvalidArgumentException::class); + $image->modify(new ReduceColorsModifier(0)); + } + + public function testVerifyColorValueAfterQuantization(): void + { + $image = $this->createTestImage(3, 2)->fill('f00'); + $image->modify(new ReduceColorsModifier(1)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(1, 1)); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/RemoveAnimationModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/RemoveAnimationModifierTest.php new file mode 100644 index 000000000..0b2c44b24 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/RemoveAnimationModifierTest.php @@ -0,0 +1,51 @@ +readTestImage('animation.gif'); + $this->assertEquals(8, count($image)); + $result = $image->modify(new RemoveAnimationModifier(2)); + $this->assertEquals(1, count($image)); + $this->assertEquals(1, count($result)); + } + + public function testApplyPercent(): void + { + $image = $this->readTestImage('animation.gif'); + $this->assertEquals(8, count($image)); + $result = $image->modify(new RemoveAnimationModifier('20%')); + $this->assertEquals(1, count($image)); + $this->assertEquals(1, count($result)); + } + + public function testApplyNonAnimated(): void + { + $image = $this->readTestImage('test.jpg'); + $this->assertEquals(1, count($image)); + $result = $image->modify(new RemoveAnimationModifier()); + $this->assertEquals(1, count($image)); + $this->assertEquals(1, count($result)); + } + + public function testApplyInvalid(): void + { + $image = $this->readTestImage('animation.gif'); + $this->expectException(InvalidArgumentException::class); + $image->modify(new RemoveAnimationModifier('test')); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/ResizeCanvasModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/ResizeCanvasModifierTest.php new file mode 100644 index 000000000..5947c2117 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/ResizeCanvasModifierTest.php @@ -0,0 +1,71 @@ +createTestImage(1, 1); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new ResizeCanvasModifier(3, 3, 'ff0', Alignment::CENTER)); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(2, 2)); + } + + public function testModifyWithTransparency(): void + { + $image = $this->readTestImage('tile.png'); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $image->modify(new ResizeCanvasModifier(18, 18, 'ff0', Alignment::CENTER)); + $this->assertEquals(18, $image->width()); + $this->assertEquals(18, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(180, 224, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(180, 224, 0, 255, $image->colorAt(2, 2)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(17, 17)); + $this->assertTransparency($image->colorAt(12, 1)); + + $image = $this->createTestImage(16, 16); + $image->modify(new ResizeCanvasModifier(32, 32, '00f5', Alignment::CENTER)); + $this->assertEquals(32, $image->width()); + $this->assertEquals(32, $image->height()); + + $this->assertColor(0, 0, 255, 84, $image->colorAt(5, 5)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(16, 5)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(30, 5)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(5, 16)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(16, 16)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(30, 16)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(5, 30)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(16, 30)); + $this->assertColor(0, 0, 255, 84, $image->colorAt(30, 30)); + } + + public function testModifyEdge(): void + { + $image = $this->createTestImage(1, 1); + $this->assertColor(255, 0, 0, 255, $image->colorAt(0, 0)); + $image->modify(new ResizeCanvasModifier(null, 2, 'ff0', Alignment::BOTTOM)); + $this->assertEquals(1, $image->width()); + $this->assertEquals(2, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(0, 1)); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/ResizeCanvasRelativeModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/ResizeCanvasRelativeModifierTest.php new file mode 100644 index 000000000..7aed36b22 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/ResizeCanvasRelativeModifierTest.php @@ -0,0 +1,45 @@ +createTestImage(1, 1); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $image->modify(new ResizeCanvasRelativeModifier(2, 2, 'ff0', Alignment::CENTER)); + $this->assertEquals(3, $image->width()); + $this->assertEquals(3, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(255, 0, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(2, 2)); + } + + public function testModifyWithTransparency(): void + { + $image = $this->readTestImage('tile.png'); + $this->assertEquals(16, $image->width()); + $this->assertEquals(16, $image->height()); + $image->modify(new ResizeCanvasRelativeModifier(2, 2, 'ff0', Alignment::CENTER)); + $this->assertEquals(18, $image->width()); + $this->assertEquals(18, $image->height()); + $this->assertColor(255, 255, 0, 255, $image->colorAt(0, 0)); + $this->assertColor(180, 224, 0, 255, $image->colorAt(1, 1)); + $this->assertColor(180, 224, 0, 255, $image->colorAt(2, 2)); + $this->assertColor(255, 255, 0, 255, $image->colorAt(17, 17)); + $this->assertTransparency($image->colorAt(12, 1)); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/ResizeModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/ResizeModifierTest.php new file mode 100644 index 000000000..6970b16ff --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/ResizeModifierTest.php @@ -0,0 +1,27 @@ +readTestImage('blocks.png'); + $this->assertEquals(640, $image->width()); + $this->assertEquals(480, $image->height()); + $image->modify(new ResizeModifier(200, 100)); + $this->assertEquals(200, $image->width()); + $this->assertEquals(100, $image->height()); + $this->assertColor(255, 0, 0, 255, $image->colorAt(150, 70)); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/ResolutionModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/ResolutionModifierTest.php new file mode 100644 index 000000000..1ced8dea5 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/ResolutionModifierTest.php @@ -0,0 +1,26 @@ +readTestImage('test.jpg'); + $this->assertEquals(72.0, $image->resolution()->x()); + $this->assertEquals(72.0, $image->resolution()->y()); + $image->modify(new ResolutionModifier(1, 2)); + $this->assertEquals(1.0, $image->resolution()->x()); + $this->assertEquals(2.0, $image->resolution()->y()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/RotateModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/RotateModifierTest.php new file mode 100644 index 000000000..a280009cb --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/RotateModifierTest.php @@ -0,0 +1,26 @@ +readTestImage('test.jpg'); + $this->assertEquals(320, $image->width()); + $this->assertEquals(240, $image->height()); + $image->modify(new RotateModifier(90, 'fff')); + $this->assertEquals(240, $image->width()); + $this->assertEquals(320, $image->height()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/SharpenModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/SharpenModifierTest.php new file mode 100644 index 000000000..4522e037d --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/SharpenModifierTest.php @@ -0,0 +1,24 @@ +readTestImage('trim.png'); + $this->assertEquals('60ab96', $image->colorAt(15, 14)->toHex()); + $image->modify(new SharpenModifier(10)); + $this->assertEquals('4faca6', $image->colorAt(15, 14)->toHex()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/StripMetaModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/StripMetaModifierTest.php new file mode 100644 index 000000000..a6994fc07 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/StripMetaModifierTest.php @@ -0,0 +1,26 @@ +readTestImage('exif.jpg'); + $this->assertEquals('Oliver Vogel', $image->exif('IFD0.Artist')); + $image->modify(new StripMetaModifier()); + $this->assertNull($image->exif('IFD0.Artist')); + $result = $image->encodeUsingFormat(Format::JPEG); + $this->assertEmpty(exif_read_data($result->toStream())['IFD0.Artist'] ?? null); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/TextModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/TextModifierTest.php new file mode 100644 index 000000000..8de39300c --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/TextModifierTest.php @@ -0,0 +1,37 @@ +setColor('ff0055'); + + $modifier = new class ('test', new Point(), $font) extends TextModifier + { + public function test(): ColorInterface + { + return $this->textColor(); + } + }; + + $modifier->setDriver(new Driver()); + + $this->assertInstanceOf(ColorInterface::class, $modifier->test()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/TrimModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/TrimModifierTest.php new file mode 100644 index 000000000..0fd4748e3 --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/TrimModifierTest.php @@ -0,0 +1,55 @@ +readTestImage('trim.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier()); + $this->assertEquals(28, $image->width()); + $this->assertEquals(28, $image->height()); + } + + public function testTrimGradient(): void + { + $image = $this->readTestImage('radial.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier(50)); + $this->assertLessThan(50, $image->width()); + $this->assertLessThan(50, $image->height()); + } + + public function testTrimHighTolerance(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier(1000000)); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $this->assertColor(255, 255, 255, 0, $image->colorAt(0, 0)); + } + + public function testTrimAnimated(): void + { + $image = $this->readTestImage('animation.gif'); + $this->expectException(NotSupportedException::class); + $image->modify(new TrimModifier()); + } +} diff --git a/tests/Unit/Drivers/SpecializableAnalyzerTest.php b/tests/Unit/Drivers/SpecializableAnalyzerTest.php new file mode 100644 index 000000000..c201661d8 --- /dev/null +++ b/tests/Unit/Drivers/SpecializableAnalyzerTest.php @@ -0,0 +1,41 @@ +makePartial(); + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('analyze')->andReturn('test'); + $result = $analyzer->analyze($image); + $this->assertEquals('test', $result); + } + + public function testAnalyzeThrowsWhenSpecializedWithoutOverride(): void + { + $analyzer = new class () extends SpecializableAnalyzer implements SpecializedInterface { + protected function belongsToDriver(object $driver): bool + { + return true; + } + }; + + $image = Mockery::mock(ImageInterface::class); + + $this->expectException(LogicException::class); + $analyzer->analyze($image); + } +} diff --git a/tests/Unit/Drivers/SpecializableDecoderTest.php b/tests/Unit/Drivers/SpecializableDecoderTest.php new file mode 100644 index 000000000..b9247ccef --- /dev/null +++ b/tests/Unit/Drivers/SpecializableDecoderTest.php @@ -0,0 +1,32 @@ +expectException(DriverException::class); + $decoder->decode(null); + } + + public function testSupports(): void + { + $decoder = new class () extends SpecializableDecoder { + // + }; + $this->expectException(DriverException::class); + $decoder->supports(null); + } +} diff --git a/tests/Unit/Drivers/SpecializableModifierTest.php b/tests/Unit/Drivers/SpecializableModifierTest.php new file mode 100644 index 000000000..28dd0b2cc --- /dev/null +++ b/tests/Unit/Drivers/SpecializableModifierTest.php @@ -0,0 +1,41 @@ +makePartial(); + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('modify')->andReturn($image); + $result = $modifier->apply($image); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testApplyThrowsWhenSpecializedWithoutOverride(): void + { + $modifier = new class () extends SpecializableModifier implements SpecializedInterface { + protected function belongsToDriver(object $driver): bool + { + return true; + } + }; + + $image = Mockery::mock(ImageInterface::class); + + $this->expectException(LogicException::class); + $modifier->apply($image); + } +} diff --git a/tests/Unit/EncodedImageTest.php b/tests/Unit/EncodedImageTest.php new file mode 100644 index 000000000..90f4a3edc --- /dev/null +++ b/tests/Unit/EncodedImageTest.php @@ -0,0 +1,102 @@ +assertInstanceOf(EncodedImage::class, $image); + } + + public function testConstructorFromResource(): void + { + $fp = fopen('php://temp', 'r+'); + fwrite($fp, 'test data'); + rewind($fp); + $image = new EncodedImage($fp, 'image/png'); + $this->assertInstanceOf(EncodedImage::class, $image); + $this->assertEquals('image/png', $image->mediaType()); + $this->assertEquals('test data', (string) $image); + } + + public function testSave(): void + { + $image = new EncodedImage('foo'); + $path = __DIR__ . '/foo.tmp'; + $this->assertFalse(file_exists($path)); + $image->save($path); + $this->assertTrue(file_exists($path)); + $this->assertEquals('foo', file_get_contents($path)); + unlink($path); + } + + public function testToDataUri(): void + { + $image = new EncodedImage('foo'); + $this->assertEquals('data:application/octet-stream;base64,Zm9v', $image->toDataUri()); + } + + public function testToString(): void + { + $image = new EncodedImage('foo'); + $this->assertEquals('foo', (string) $image); + } + + public function testMediaType(): void + { + $image = new EncodedImage('foo'); + $this->assertEquals('application/octet-stream', $image->mediaType()); + + $image = new EncodedImage(Resource::create()->data(), 'image/jpeg'); + $this->assertEquals('image/jpeg', $image->mediaType()); + } + + public function testMimetype(): void + { + $image = new EncodedImage('foo'); + $this->assertEquals('application/octet-stream', $image->mimetype()); + + $image = new EncodedImage(Resource::create()->data(), 'image/jpeg'); + $this->assertEquals('image/jpeg', $image->mimetype()); + } + + public function testToBase64(): void + { + $image = new EncodedImage('foo'); + $this->assertEquals('Zm9v', $image->toBase64()); + } + + public function testDebugInfo(): void + { + $info = (new EncodedImage('foo', 'image/png'))->__debugInfo(); + $this->assertEquals('image/png', $info['mediaType']); + $this->assertEquals(3, $info['size']); + } + + public function testDebugInfoWhenSizeThrows(): void + { + // Create an EncodedImage, then close the internal stream resource to trigger + // the Throwable catch branch in __debugInfo + $image = new EncodedImage('foo', 'image/png'); + + // Use reflection to close the internal stream so size() throws + $ref = new \ReflectionClass($image); + $prop = $ref->getProperty('stream'); + $stream = $prop->getValue($image); + fclose($stream); + + $info = $image->__debugInfo(); + $this->assertEquals('image/png', $info['mediaType']); + $this->assertEquals(0, $info['size']); + } +} diff --git a/tests/Unit/Encoders/FileExtensionEncoderTest.php b/tests/Unit/Encoders/FileExtensionEncoderTest.php new file mode 100644 index 000000000..3bf40ba8f --- /dev/null +++ b/tests/Unit/Encoders/FileExtensionEncoderTest.php @@ -0,0 +1,162 @@ + + */ + private function testEncoder(string|FileExtension $extension, array $options = []): EncoderInterface + { + $encoder = new class ($extension, ...$options) extends FileExtensionEncoder + { + public function __construct(string|FileExtension $extension, mixed ...$options) + { + parent::__construct($extension, ...$options); + } + + public function test(string|FileExtension $extension): EncoderInterface + { + return $this->encoderByFileExtension($extension); + } + }; + + return $encoder->test($extension); + } + + #[DataProvider('targetEncoderProvider')] + public function testEncoderByFileExtensionString( + string|FileExtension $fileExtension, + string $targetEncoderClassname, + ): void { + $this->assertInstanceOf( + $targetEncoderClassname, + $this->testEncoder($fileExtension), + ); + } + + public static function targetEncoderProvider(): Generator + { + yield ['webp', WebpEncoder::class]; + yield ['avif', AvifEncoder::class]; + yield ['jpeg', JpegEncoder::class]; + yield ['jpg', JpegEncoder::class]; + yield ['bmp', BmpEncoder::class]; + yield ['gif', GifEncoder::class]; + yield ['png', PngEncoder::class]; + yield ['tiff', TiffEncoder::class]; + yield ['tif', TiffEncoder::class]; + yield ['jp2', Jpeg2000Encoder::class]; + yield ['heic', HeicEncoder::class]; + yield ['WEBP', WebpEncoder::class]; + yield ['AVIF', AvifEncoder::class]; + yield ['JPEG', JpegEncoder::class]; + yield ['JPG', JpegEncoder::class]; + yield ['BMP', BmpEncoder::class]; + yield ['GIF', GifEncoder::class]; + yield ['PNG', PngEncoder::class]; + yield ['TIFF', TiffEncoder::class]; + yield ['TIF', TiffEncoder::class]; + yield ['JP2', Jpeg2000Encoder::class]; + yield ['HEIC', HeicEncoder::class]; + yield [FileExtension::WEBP, WebpEncoder::class]; + yield [FileExtension::AVIF, AvifEncoder::class]; + yield [FileExtension::JPG, JpegEncoder::class]; + yield [FileExtension::BMP, BmpEncoder::class]; + yield [FileExtension::GIF, GifEncoder::class]; + yield [FileExtension::PNG, PngEncoder::class]; + yield [FileExtension::TIF, TiffEncoder::class]; + yield [FileExtension::TIFF, TiffEncoder::class]; + yield [FileExtension::JP2, Jpeg2000Encoder::class]; + yield [FileExtension::HEIC, HeicEncoder::class]; + } + + public function testArgumentsNotSupportedByTargetEncoder(): void + { + $encoder = $this->testEncoder( + 'png', + [ + 'interlaced' => true, // is not ignored + 'quality' => 10, // is ignored because png encoder has no quality argument + ], + ); + + $this->assertInstanceOf(PngEncoder::class, $encoder); + $this->assertTrue($encoder->interlaced); + } + + public function testEncoderByFileExtensionUnknown(): void + { + $this->expectException(NotSupportedException::class); + $this->testEncoder('test'); + } + + public function testEncoderByFileExtensionEmpty(): void + { + $this->expectException(InvalidArgumentException::class); + $this->testEncoder(''); + } + + public function testEncodeWithExplicitExtension(): void + { + $encoder = new FileExtensionEncoder('png'); + $encodedImage = Mockery::mock(EncodedImageInterface::class); + + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('encode')->once()->andReturn($encodedImage); + + $result = $encoder->encode($image); + $this->assertSame($encodedImage, $result); + } + + public function testEncodeWithNullExtensionFromOrigin(): void + { + $encoder = new FileExtensionEncoder(); + $encodedImage = Mockery::mock(EncodedImageInterface::class); + + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('origin')->andReturn(new Origin('image/png', '/path/to/image.png')); + $image->shouldReceive('encode')->once()->andReturn($encodedImage); + + $result = $encoder->encode($image); + $this->assertSame($encodedImage, $result); + } + + public function testEncodeWithNullExtensionAndNoOriginExtension(): void + { + $encoder = new FileExtensionEncoder(); + + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('origin')->andReturn(new Origin('application/octet-stream')); + + $this->expectException(NotSupportedException::class); + $encoder->encode($image); + } +} diff --git a/tests/Unit/Encoders/FormatEncoderTest.php b/tests/Unit/Encoders/FormatEncoderTest.php new file mode 100644 index 000000000..d282fe669 --- /dev/null +++ b/tests/Unit/Encoders/FormatEncoderTest.php @@ -0,0 +1,67 @@ +assertInstanceOf(FormatEncoder::class, $encoder); + } + + public function testConstructorWithNull(): void + { + $encoder = new FormatEncoder(); + $this->assertInstanceOf(FormatEncoder::class, $encoder); + } + + public function testEncodeWithFormat(): void + { + $encoder = new FormatEncoder(Format::JPEG, quality: 50); + $encodedImage = Mockery::mock(EncodedImageInterface::class); + + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('encode')->once()->andReturn($encodedImage); + + $result = $encoder->encode($image); + $this->assertSame($encodedImage, $result); + } + + public function testEncodeWithNullFormatFromOrigin(): void + { + $encoder = new FormatEncoder(); + $encodedImage = Mockery::mock(EncodedImageInterface::class); + + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('origin')->andReturn(new Origin('image/png')); + $image->shouldReceive('encode')->once()->andReturn($encodedImage); + + $result = $encoder->encode($image); + $this->assertSame($encodedImage, $result); + } + + public function testEncodeWithNullFormatAndUnsupportedOrigin(): void + { + $encoder = new FormatEncoder(); + + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('origin')->andReturn(new Origin('application/octet-stream')); + + $this->expectException(NotSupportedException::class); + $encoder->encode($image); + } +} diff --git a/tests/Unit/Encoders/MediaTypeEncoderTest.php b/tests/Unit/Encoders/MediaTypeEncoderTest.php new file mode 100644 index 000000000..302f5c91e --- /dev/null +++ b/tests/Unit/Encoders/MediaTypeEncoderTest.php @@ -0,0 +1,132 @@ + + */ + private function testEncoder(string|MediaType $mediaType, array $options = []): EncoderInterface + { + $encoder = new class ($mediaType, ...$options) extends MediaTypeEncoder + { + public function __construct(string|MediaType $mediaType, mixed ...$options) + { + parent::__construct($mediaType, ...$options); + } + + public function test(string|MediaType $mediaType): EncoderInterface + { + return $this->encoderByMediaType($mediaType); + } + }; + + return $encoder->test($mediaType); + } + + #[DataProvider('targetEncoderProvider')] + public function testEncoderByMediaType( + string|MediaType $mediaType, + string $targetEncoderClassname, + ): void { + $this->assertInstanceOf( + $targetEncoderClassname, + $this->testEncoder($mediaType) + ); + } + + public static function targetEncoderProvider(): Generator + { + yield ['image/webp', WebpEncoder::class]; + yield ['image/avif', AvifEncoder::class]; + yield ['image/jpeg', JpegEncoder::class]; + yield ['image/bmp', BmpEncoder::class]; + yield ['image/gif', GifEncoder::class]; + yield ['image/png', PngEncoder::class]; + yield ['image/png', PngEncoder::class]; + yield ['image/tiff', TiffEncoder::class]; + yield ['image/jp2', Jpeg2000Encoder::class]; + yield ['image/heic', HeicEncoder::class]; + yield [MediaType::IMAGE_WEBP, WebpEncoder::class]; + yield [MediaType::IMAGE_AVIF, AvifEncoder::class]; + yield [MediaType::IMAGE_JPEG, JpegEncoder::class]; + yield [MediaType::IMAGE_BMP, BmpEncoder::class]; + yield [MediaType::IMAGE_GIF, GifEncoder::class]; + yield [MediaType::IMAGE_PNG, PngEncoder::class]; + yield [MediaType::IMAGE_TIFF, TiffEncoder::class]; + yield [MediaType::IMAGE_JP2, Jpeg2000Encoder::class]; + yield [MediaType::IMAGE_HEIC, HeicEncoder::class]; + yield [MediaType::IMAGE_HEIF, HeicEncoder::class]; + } + + public function testArgumentsNotSupportedByTargetEncoder(): void + { + $encoder = $this->testEncoder( + 'image/png', + [ + 'interlaced' => true, // is not ignored + 'quality' => 10, // is ignored because png encoder has no quality argument + ], + ); + + $this->assertInstanceOf(PngEncoder::class, $encoder); + $this->assertTrue($encoder->interlaced); + } + + public function testEncoderByFileExtensionUnknown(): void + { + $this->expectException(NotSupportedException::class); + $this->testEncoder('test'); + } + + public function testEncodeWithExplicitMediaType(): void + { + $encoder = new MediaTypeEncoder('image/png'); + $encodedImage = Mockery::mock(EncodedImageInterface::class); + + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('encode')->once()->andReturn($encodedImage); + + $result = $encoder->encode($image); + $this->assertSame($encodedImage, $result); + } + + public function testEncodeWithNullMediaTypeFromOrigin(): void + { + $encoder = new MediaTypeEncoder(); + $encodedImage = Mockery::mock(EncodedImageInterface::class); + + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('origin')->andReturn(new Origin('image/jpeg')); + $image->shouldReceive('encode')->once()->andReturn($encodedImage); + + $result = $encoder->encode($image); + $this->assertSame($encodedImage, $result); + } +} diff --git a/tests/Unit/FileExtensionTest.php b/tests/Unit/FileExtensionTest.php new file mode 100644 index 000000000..742c28fff --- /dev/null +++ b/tests/Unit/FileExtensionTest.php @@ -0,0 +1,182 @@ +assertEquals(FileExtension::JPG, FileExtension::create(MediaType::IMAGE_JPEG)); + $this->assertEquals(FileExtension::JPG, FileExtension::create(Format::JPEG)); + $this->assertEquals(FileExtension::JPG, FileExtension::create(FileExtension::JPG)); + $this->assertEquals(FileExtension::JPG, FileExtension::create('jpg')); + $this->assertEquals(FileExtension::JPEG, FileExtension::create('jpeg')); + $this->assertEquals(FileExtension::JPG, FileExtension::create('image/jpeg')); + $this->assertEquals(FileExtension::JPG, FileExtension::create('JPG')); + $this->assertEquals(FileExtension::JPEG, FileExtension::create('JPEG')); + $this->assertEquals(FileExtension::JPG, FileExtension::create('IMAGE/JPEG')); + } + + public function testCreateUnknown(): void + { + $this->expectException(InvalidArgumentException::class); + FileExtension::create('foo'); + } + + public function testTryCreate(): void + { + $this->assertEquals(FileExtension::JPG, FileExtension::tryCreate(MediaType::IMAGE_JPEG)); + $this->assertEquals(FileExtension::JPG, FileExtension::tryCreate(Format::JPEG)); + $this->assertEquals(FileExtension::JPG, FileExtension::tryCreate(FileExtension::JPG)); + $this->assertEquals(FileExtension::JPG, FileExtension::tryCreate('jpg')); + $this->assertEquals(FileExtension::JPEG, FileExtension::tryCreate('jpeg')); + $this->assertEquals(FileExtension::JPG, FileExtension::tryCreate('image/jpeg')); + $this->assertNull(FileExtension::tryCreate('no-format')); + } + + public function testFormatJpeg(): void + { + $ext = FileExtension::JPEG; + $this->assertEquals(Format::JPEG, $ext->format()); + + $ext = FileExtension::JPG; + $this->assertEquals(Format::JPEG, $ext->format()); + } + + public function testFormatWebp(): void + { + $ext = FileExtension::WEBP; + $this->assertEquals(Format::WEBP, $ext->format()); + } + + public function testFormatGif(): void + { + $ext = FileExtension::GIF; + $this->assertEquals(Format::GIF, $ext->format()); + } + + public function testFormatPng(): void + { + $ext = FileExtension::PNG; + $this->assertEquals(Format::PNG, $ext->format()); + } + + public function testFormatAvif(): void + { + $ext = FileExtension::AVIF; + $this->assertEquals(Format::AVIF, $ext->format()); + } + + public function testFormatBmp(): void + { + $ext = FileExtension::BMP; + $this->assertEquals(Format::BMP, $ext->format()); + } + + public function testFormatTiff(): void + { + $ext = FileExtension::TIFF; + $this->assertEquals(Format::TIFF, $ext->format()); + + $ext = FileExtension::TIF; + $this->assertEquals(Format::TIFF, $ext->format()); + } + + public function testFormatJpeg2000(): void + { + $ext = FileExtension::JP2; + $this->assertEquals(Format::JP2, $ext->format()); + + $ext = FileExtension::J2K; + $this->assertEquals(Format::JP2, $ext->format()); + + $ext = FileExtension::J2C; + $this->assertEquals(Format::JP2, $ext->format()); + + $ext = FileExtension::JPG2; + $this->assertEquals(Format::JP2, $ext->format()); + + $ext = FileExtension::JP2K; + $this->assertEquals(Format::JP2, $ext->format()); + + $ext = FileExtension::JPF; + $this->assertEquals(Format::JP2, $ext->format()); + + $ext = FileExtension::JPX; + $this->assertEquals(Format::JP2, $ext->format()); + + $ext = FileExtension::JPC; + $this->assertEquals(Format::JP2, $ext->format()); + + $ext = FileExtension::JPM; + $this->assertEquals(Format::JP2, $ext->format()); + } + + public function testFormatHeic(): void + { + $ext = FileExtension::HEIC; + $this->assertEquals(Format::HEIC, $ext->format()); + + $ext = FileExtension::HEIF; + $this->assertEquals(Format::HEIC, $ext->format()); + } + + public function testFormatIco(): void + { + $ext = FileExtension::ICO; + $this->assertEquals(Format::ICO, $ext->format()); + } + + public function testCreateFromMediaTypeString(): void + { + $this->assertEquals(FileExtension::JPG, FileExtension::create('image/jpeg')); + $this->assertEquals(FileExtension::PNG, FileExtension::create('image/png')); + $this->assertEquals(FileExtension::GIF, FileExtension::create('image/gif')); + $this->assertEquals(FileExtension::BMP, FileExtension::create('image/bmp')); + $this->assertEquals(FileExtension::WEBP, FileExtension::create('image/webp')); + } + + #[DataProvider('mediaTypesDataProvider')] + public function testMediatypes(FileExtension $extension, int $mediaTypeCount, MediaType $mediaType): void + { + $this->assertCount($mediaTypeCount, $extension->mediaTypes()); + $this->assertEquals($mediaType, $extension->mediaType()); + } + + public static function mediaTypesDataProvider(): Generator + { + yield [FileExtension::JPEG, 4, MediaType::IMAGE_JPEG]; + yield [FileExtension::WEBP, 2, MediaType::IMAGE_WEBP]; + yield [FileExtension::GIF, 1, MediaType::IMAGE_GIF]; + yield [FileExtension::PNG, 2, MediaType::IMAGE_PNG]; + yield [FileExtension::AVIF, 2, MediaType::IMAGE_AVIF]; + yield [FileExtension::BMP, 9, MediaType::IMAGE_BMP]; + yield [FileExtension::TIFF, 1, MediaType::IMAGE_TIFF]; + yield [FileExtension::TIF, 1, MediaType::IMAGE_TIFF]; + yield [FileExtension::JP2, 4, MediaType::IMAGE_JP2]; + yield [FileExtension::HEIC, 3, MediaType::IMAGE_HEIC]; + yield [FileExtension::HEIF, 3, MediaType::IMAGE_HEIC]; + yield [FileExtension::JPF, 4, MediaType::IMAGE_JP2]; + yield [FileExtension::JPX, 4, MediaType::IMAGE_JP2]; + yield [FileExtension::JPC, 4, MediaType::IMAGE_JP2]; + yield [FileExtension::JPM, 4, MediaType::IMAGE_JP2]; + yield [FileExtension::J2K, 4, MediaType::IMAGE_JP2]; + yield [FileExtension::J2C, 4, MediaType::IMAGE_JP2]; + yield [FileExtension::JP2K, 4, MediaType::IMAGE_JP2]; + yield [FileExtension::JPG2, 4, MediaType::IMAGE_JP2]; + yield [FileExtension::ICO, 2, MediaType::IMAGE_X_ICON]; + yield [FileExtension::JPG, 4, MediaType::IMAGE_JPEG]; + } +} diff --git a/tests/Unit/FileTest.php b/tests/Unit/FileTest.php new file mode 100644 index 000000000..1aa910d97 --- /dev/null +++ b/tests/Unit/FileTest.php @@ -0,0 +1,128 @@ +assertInstanceOf(File::class, $file); + + $file = new File('foo'); + $this->assertInstanceOf(File::class, $file); + } + + public function testConstructorFromString(): void + { + $file = new File('foo'); + $this->assertInstanceOf(File::class, $file); + } + + public function testConstructorFromResource(): void + { + $file = new File(fopen('php://temp', 'r')); + $this->assertInstanceOf(File::class, $file); + } + + public function testFromPath(): void + { + $file = File::fromPath(Resource::create()->path()); + $this->assertInstanceOf(File::class, $file); + $this->assertTrue($file->size() > 0); + } + + public function testFromPathNotFound(): void + { + $this->expectException(FileNotFoundException::class); + File::fromPath('/tmp/nonexistent_file_' . hrtime(true) . '.jpg'); + } + + public function testSave(): void + { + $file = new File('foo'); + $filenames = [ + __DIR__ . '/01_file_' . strval(hrtime(true)) . '.test', + __DIR__ . '/02_file_' . strval(hrtime(true)) . '.test', + ]; + + foreach ($filenames as $name) { + $file->save($name); + } + + foreach ($filenames as $name) { + $this->assertFileExists($name); + $this->assertEquals('foo', file_get_contents($name)); + unlink($name); + } + } + + public function testSaveEmptyPath(): void + { + $file = new File('foo'); + $this->expectException(InvalidArgumentException::class); + $file->save(''); + } + + public function testSaveDirectoryNotFound(): void + { + $file = new File('foo'); + $this->expectException(DirectoryNotFoundException::class); + $file->save('/tmp/nonexistent_dir_' . hrtime(true) . '/test.txt'); + } + + public function testToString(): void + { + $file = new File('foo'); + $string = $file->toString(); + $this->assertEquals('foo', $string); + $this->assertEquals('foo', $string); + } + + public function testCastToString(): void + { + $file = new File('foo'); + $this->assertEquals('foo', (string) $file); + } + + public function testToStream(): void + { + $file = new File('foo'); + $fp = $file->toStream(); + $this->assertIsResource($fp); + } + + public function testSize(): void + { + $file = new File(); + $this->assertEquals(0, $file->size()); + + $file = new File('foo'); + $this->assertEquals(3, $file->size()); + } + + public function testSavePathTooLong(): void + { + $file = new File('foo'); + $longPath = '/tmp/' . str_repeat('a', PHP_MAXPATHLEN + 1) . '.test'; + $this->expectException(InvalidArgumentException::class); + $file->save($longPath); + } + + public function testConstructorInvalidType(): void + { + $this->expectException(InvalidArgumentException::class); + new File(123); + } +} diff --git a/tests/Unit/FormatTest.php b/tests/Unit/FormatTest.php new file mode 100644 index 000000000..c0fe32330 --- /dev/null +++ b/tests/Unit/FormatTest.php @@ -0,0 +1,335 @@ +assertEquals(Format::JPEG, Format::create(Format::JPEG)); + $this->assertEquals(Format::JPEG, Format::create('jpg')); + $this->assertEquals(Format::JPEG, Format::create('jpeg')); + $this->assertEquals(Format::JPEG, Format::create('image/jpeg')); + $this->assertEquals(Format::GIF, Format::create('image/gif')); + $this->assertEquals(Format::JPEG, Format::create('JPG')); + $this->assertEquals(Format::JPEG, Format::create('JPEG')); + $this->assertEquals(Format::JPEG, Format::create('IMAGE/JPEG')); + $this->assertEquals(Format::GIF, Format::create('IMAGE/GIF')); + $this->assertEquals(Format::PNG, Format::create(FileExtension::PNG)); + $this->assertEquals(Format::WEBP, Format::create(MediaType::IMAGE_WEBP)); + } + + public function testCreateUnknown(): void + { + $this->expectException(InvalidArgumentException::class); + Format::create('foo'); + } + + public function testTryCreate(): void + { + $this->assertEquals(Format::JPEG, Format::tryCreate(Format::JPEG)); + $this->assertEquals(Format::JPEG, Format::tryCreate('jpg')); + $this->assertEquals(Format::JPEG, Format::tryCreate('jpeg')); + $this->assertEquals(Format::JPEG, Format::tryCreate('image/jpeg')); + $this->assertEquals(Format::GIF, Format::tryCreate('image/gif')); + $this->assertEquals(Format::PNG, Format::tryCreate(FileExtension::PNG)); + $this->assertEquals(Format::WEBP, Format::tryCreate(MediaType::IMAGE_WEBP)); + $this->assertNull(Format::tryCreate('no-format')); + } + + public function testMediaTypesJpeg(): void + { + $format = Format::JPEG; + $mediaTypes = $format->mediaTypes(); + $this->assertIsArray($mediaTypes); + $this->assertCount(4, $mediaTypes); + + $this->assertEquals(MediaType::IMAGE_JPEG, $format->mediaType()); + } + + public function testMediaTypesWebp(): void + { + $format = Format::WEBP; + $mediaTypes = $format->mediaTypes(); + $this->assertIsArray($mediaTypes); + $this->assertCount(2, $mediaTypes); + + $this->assertEquals(MediaType::IMAGE_WEBP, $format->mediaType()); + } + + public function testMediaTypesFGif(): void + { + $format = Format::GIF; + $mediaTypes = $format->mediaTypes(); + $this->assertIsArray($mediaTypes); + $this->assertCount(1, $mediaTypes); + + $this->assertEquals(MediaType::IMAGE_GIF, $format->mediaType()); + } + + public function testMediaTypesPng(): void + { + $format = Format::PNG; + $mediaTypes = $format->mediaTypes(); + $this->assertIsArray($mediaTypes); + $this->assertCount(2, $mediaTypes); + + $this->assertEquals(MediaType::IMAGE_PNG, $format->mediaType()); + } + + public function testMediaTypesAvif(): void + { + $format = Format::AVIF; + $mediaTypes = $format->mediaTypes(); + $this->assertIsArray($mediaTypes); + $this->assertCount(2, $mediaTypes); + + $this->assertEquals(MediaType::IMAGE_AVIF, $format->mediaType()); + } + + public function testMediaTypesBmp(): void + { + $format = Format::BMP; + $mediaTypes = $format->mediaTypes(); + $this->assertIsArray($mediaTypes); + $this->assertCount(9, $mediaTypes); + + $this->assertEquals(MediaType::IMAGE_BMP, $format->mediaType()); + } + + public function testMediaTypesTiff(): void + { + $format = Format::TIFF; + $mediaTypes = $format->mediaTypes(); + $this->assertIsArray($mediaTypes); + $this->assertCount(1, $mediaTypes); + + $this->assertEquals(MediaType::IMAGE_TIFF, $format->mediaType()); + } + + public function testMediaTypesJpeg2000(): void + { + $format = Format::JP2; + $mediaTypes = $format->mediaTypes(); + $this->assertIsArray($mediaTypes); + $this->assertCount(4, $mediaTypes); + + $this->assertEquals(MediaType::IMAGE_JP2, $format->mediaType()); + } + + public function testMediaTypesHeic(): void + { + $format = Format::HEIC; + $mediaTypes = $format->mediaTypes(); + $this->assertIsArray($mediaTypes); + $this->assertCount(3, $mediaTypes); + + $this->assertEquals(MediaType::IMAGE_HEIC, $format->mediaType()); + } + + public function testEncoderJpeg(): void + { + $format = Format::JPEG; + $this->assertInstanceOf(JpegEncoder::class, $format->encoder()); + } + + public function testEncoderAvif(): void + { + $format = Format::AVIF; + $this->assertInstanceOf(AvifEncoder::class, $format->encoder()); + } + + public function testEncoderWebp(): void + { + $format = Format::WEBP; + $this->assertInstanceOf(WebpEncoder::class, $format->encoder()); + } + + public function testEncoderGif(): void + { + $format = Format::GIF; + $this->assertInstanceOf(GifEncoder::class, $format->encoder()); + } + + public function testEncoderPng(): void + { + $format = Format::PNG; + $this->assertInstanceOf(PngEncoder::class, $format->encoder()); + } + + public function testEncoderBitmap(): void + { + $format = Format::BMP; + $this->assertInstanceOf(BmpEncoder::class, $format->encoder()); + } + + public function testEncoderTiff(): void + { + $format = Format::TIFF; + $this->assertInstanceOf(TiffEncoder::class, $format->encoder()); + } + + public function testEncoderJpep2000(): void + { + $format = Format::JP2; + $this->assertInstanceOf(Jpeg2000Encoder::class, $format->encoder()); + } + + public function testEncoderHeic(): void + { + $format = Format::HEIC; + $this->assertInstanceOf(HeicEncoder::class, $format->encoder()); + } + + public function testFileExtensionsJpeg(): void + { + $format = Format::JPEG; + $extensions = $format->fileExtensions(); + $this->assertIsArray($extensions); + $this->assertCount(4, $extensions); + + $this->assertEquals(FileExtension::JPG, $format->fileExtension()); + } + + public function testFileExtensionsWebp(): void + { + $format = Format::WEBP; + $extensions = $format->fileExtensions(); + $this->assertIsArray($extensions); + $this->assertCount(1, $extensions); + + $this->assertEquals(FileExtension::WEBP, $format->fileExtension()); + } + + public function testFileExtensionsGif(): void + { + $format = Format::GIF; + $extensions = $format->fileExtensions(); + $this->assertIsArray($extensions); + $this->assertCount(1, $extensions); + + $this->assertEquals(FileExtension::GIF, $format->fileExtension()); + } + + public function testFileExtensionsPng(): void + { + $format = Format::PNG; + $extensions = $format->fileExtensions(); + $this->assertIsArray($extensions); + $this->assertCount(1, $extensions); + + $this->assertEquals(FileExtension::PNG, $format->fileExtension()); + } + + public function testFileExtensionsAvif(): void + { + $format = Format::AVIF; + $extensions = $format->fileExtensions(); + $this->assertIsArray($extensions); + $this->assertCount(1, $extensions); + + $this->assertEquals(FileExtension::AVIF, $format->fileExtension()); + } + + public function testFileExtensionsBmp(): void + { + $format = Format::BMP; + $extensions = $format->fileExtensions(); + $this->assertIsArray($extensions); + $this->assertCount(1, $extensions); + + $this->assertEquals(FileExtension::BMP, $format->fileExtension()); + } + + public function testFileExtensionsTiff(): void + { + $format = Format::TIFF; + $extensions = $format->fileExtensions(); + $this->assertIsArray($extensions); + $this->assertCount(2, $extensions); + + $this->assertEquals(FileExtension::TIF, $format->fileExtension()); + } + + public function testFileExtensionsJp2(): void + { + $format = Format::JP2; + $extensions = $format->fileExtensions(); + $this->assertIsArray($extensions); + $this->assertCount(9, $extensions); + + $this->assertEquals(FileExtension::JP2, $format->fileExtension()); + } + + public function testFileExtensionsHeic(): void + { + $format = Format::HEIC; + $extensions = $format->fileExtensions(); + $this->assertIsArray($extensions); + $this->assertCount(2, $extensions); + + $this->assertEquals(FileExtension::HEIC, $format->fileExtension()); + } + + public function testMediaTypesIco(): void + { + $format = Format::ICO; + $mediaTypes = $format->mediaTypes(); + $this->assertIsArray($mediaTypes); + $this->assertCount(2, $mediaTypes); + + $this->assertEquals(MediaType::IMAGE_X_ICON, $format->mediaType()); + } + + public function testFileExtensionsIco(): void + { + $format = Format::ICO; + $extensions = $format->fileExtensions(); + $this->assertIsArray($extensions); + $this->assertCount(1, $extensions); + + $this->assertEquals(FileExtension::ICO, $format->fileExtension()); + } + + public function testEncoderIco(): void + { + $format = Format::ICO; + $this->assertInstanceOf(IcoEncoder::class, $format->encoder()); + } + + public function testEncoderWithOptions(): void + { + $encoder = Format::JPEG->encoder(quality: 50, progressive: true); + $this->assertInstanceOf(JpegEncoder::class, $encoder); + $this->assertEquals(50, $encoder->quality); + $this->assertTrue($encoder->progressive); + } + + public function testEncoderWithFilteredOptions(): void + { + // Options that don't exist on the encoder should be filtered out + $encoder = Format::JPEG->encoder(quality: 75, nonexistent: 'value'); + $this->assertInstanceOf(JpegEncoder::class, $encoder); + $this->assertEquals(75, $encoder->quality); + } +} diff --git a/tests/Unit/FractionTest.php b/tests/Unit/FractionTest.php new file mode 100644 index 000000000..1138689c8 --- /dev/null +++ b/tests/Unit/FractionTest.php @@ -0,0 +1,24 @@ +assertEquals(12, Fraction::FULL->of(12)); + $this->assertEquals(24, Fraction::DOUBLE->of(12)); + $this->assertEquals(18, Fraction::ONE_AND_A_HALF->of(12)); + $this->assertEquals(36, Fraction::TRIPLE->of(12)); + $this->assertEquals(9, Fraction::THREE_QUARTER->of(12)); + $this->assertEquals(3, Fraction::QUARTER->of(12)); + $this->assertEquals(8, Fraction::TWO_THIRDS->of(12)); + $this->assertEquals(4, Fraction::THIRD->of(12)); + $this->assertEquals(6, Fraction::HALF->of(12)); + } +} diff --git a/tests/Unit/Geometry/BezierTest.php b/tests/Unit/Geometry/BezierTest.php new file mode 100644 index 000000000..14b27facf --- /dev/null +++ b/tests/Unit/Geometry/BezierTest.php @@ -0,0 +1,273 @@ +assertInstanceOf(Bezier::class, $bezier); + $this->assertEquals(0, $bezier->count()); + } + + public function testCount(): void + { + $bezier = new Bezier([ + new Point(), + new Point(), + new Point(), + new Point() + ]); + $this->assertEquals(4, $bezier->count()); + } + + public function testArrayAccess(): void + { + $bezier = new Bezier([ + new Point(), + new Point(), + new Point(), + new Point() + ]); + $this->assertInstanceOf(Point::class, $bezier[0]); + $this->assertInstanceOf(Point::class, $bezier[1]); + $this->assertInstanceOf(Point::class, $bezier[2]); + $this->assertInstanceOf(Point::class, $bezier[3]); + } + + public function testAddPoint(): void + { + $bezier = new Bezier([ + new Point(), + new Point() + ]); + $this->assertEquals(2, $bezier->count()); + $result = $bezier->addPoint(new Point()); + $this->assertEquals(3, $bezier->count()); + $this->assertInstanceOf(Bezier::class, $result); + } + + public function testFirst(): void + { + $bezier = new Bezier([ + new Point(50, 45), + new Point(100, -49), + new Point(-100, 100), + new Point(200, 300), + ]); + $this->assertEquals(50, $bezier->first()->x()); + $this->assertEquals(45, $bezier->first()->y()); + } + + public function testFirstEmpty(): void + { + $bezier = new Bezier(); + $this->assertNull($bezier->first()); + } + + public function testSecond(): void + { + $bezier = new Bezier([ + new Point(50, 45), + new Point(100, -49), + new Point(-100, 100), + new Point(200, 300), + ]); + $this->assertEquals(100, $bezier->second()->x()); + $this->assertEquals(-49, $bezier->second()->y()); + } + + public function testSecondEmpty(): void + { + $bezier = new Bezier(); + $this->assertNull($bezier->second()); + } + + public function testThird(): void + { + $bezier = new Bezier([ + new Point(50, 45), + new Point(100, -49), + new Point(-100, 100), + new Point(200, 300), + ]); + $this->assertEquals(-100, $bezier->third()->x()); + $this->assertEquals(100, $bezier->third()->y()); + } + + public function testThirdEmpty(): void + { + $bezier = new Bezier(); + $this->assertNull($bezier->third()); + } + + public function testLast(): void + { + $bezier = new Bezier([ + new Point(50, 45), + new Point(100, -49), + new Point(-100, 100), + new Point(200, 300), + ]); + $this->assertEquals(200, $bezier->last()->x()); + $this->assertEquals(300, $bezier->last()->y()); + } + + public function testLastEmpty(): void + { + $bezier = new Bezier(); + $this->assertNull($bezier->last()); + } + + public function testOffsetExists(): void + { + $bezier = new Bezier(); + $this->assertFalse($bezier->offsetExists(0)); + $this->assertFalse($bezier->offsetExists(1)); + $bezier->addPoint(new Point(0, 0)); + $this->assertTrue($bezier->offsetExists(0)); + $this->assertFalse($bezier->offsetExists(1)); + } + + public function testOffsetSetUnset(): void + { + $bezier = new Bezier(); + $bezier->offsetSet(0, new Point()); + $bezier->offsetSet(2, new Point()); + $this->assertTrue($bezier->offsetExists(0)); + $this->assertFalse($bezier->offsetExists(1)); + $this->assertTrue($bezier->offsetExists(2)); + $bezier->offsetUnset(2); + $this->assertTrue($bezier->offsetExists(0)); + $this->assertFalse($bezier->offsetExists(1)); + $this->assertFalse($bezier->offsetExists(2)); + } + + public function testGetSetPivotPoint(): void + { + $bezier = new Bezier(); + $this->assertInstanceOf(Point::class, $bezier->pivot()); + $this->assertEquals(0, $bezier->pivot()->x()); + $this->assertEquals(0, $bezier->pivot()->y()); + $result = $bezier->setPivot(new Point(12, 34)); + $this->assertInstanceOf(Bezier::class, $result); + $this->assertEquals(12, $bezier->pivot()->x()); + $this->assertEquals(34, $bezier->pivot()->y()); + } + + public function testToArray(): void + { + $bezier = new Bezier([ + new Point(50, 50), + new Point(100, 50), + new Point(-50, -100), + new Point(50, 100), + ]); + $this->assertEquals([50, 50, 100, 50, -50, -100, 50, 100], $bezier->toArray()); + } + + public function testPosition(): void + { + $bezier = new Bezier([], new Point(10, 20)); + $position = $bezier->position(); + $this->assertInstanceOf(Point::class, $position); + $this->assertEquals(10, $position->x()); + $this->assertEquals(20, $position->y()); + } + + public function testSetPosition(): void + { + $bezier = new Bezier(); + $this->assertEquals(0, $bezier->position()->x()); + $this->assertEquals(0, $bezier->position()->y()); + + $result = $bezier->setPosition(new Point(50, 60)); + $this->assertInstanceOf(Bezier::class, $result); + $this->assertEquals(50, $bezier->position()->x()); + $this->assertEquals(60, $bezier->position()->y()); + } + + public function testGetIterator(): void + { + $bezier = new Bezier([ + new Point(10, 20), + new Point(30, 40), + ]); + + $points = []; + foreach ($bezier as $point) { + $points[] = $point; + } + + $this->assertCount(2, $points); + $this->assertEquals(10, $points[0]->x()); + $this->assertEquals(20, $points[0]->y()); + $this->assertEquals(30, $points[1]->x()); + $this->assertEquals(40, $points[1]->y()); + } + + public function testFactory(): void + { + $bezier = new Bezier([ + new Point(10, 20), + new Point(30, 40), + ]); + $factory = $bezier->factory(); + $this->assertInstanceOf(BezierFactory::class, $factory); + } + + public function testAdjust(): void + { + $bezier = new Bezier(); + $this->assertEquals(null, $bezier->backgroundColor()); + $adjusted = $bezier->adjust(fn(BezierFactory $factory) => $factory->background('f50')); + $this->assertEquals(null, $bezier->backgroundColor()); + $this->assertEquals('f50', $adjusted->backgroundColor()); + } + + public function testClone(): void + { + $bezier = new Bezier([ + new Point(10, 20), + new Point(30, 40), + ], new Point(5, 5)); + + $clone = clone $bezier; + + // Values should match + $this->assertEquals(10, $clone->first()->x()); + $this->assertEquals(20, $clone->first()->y()); + $this->assertEquals(5, $clone->pivot()->x()); + $this->assertEquals(5, $clone->pivot()->y()); + + // Points and pivot should be deep-cloned + $this->assertNotSame($bezier->pivot(), $clone->pivot()); + $this->assertNotSame($bezier->first(), $clone->first()); + } + + public function testCloneWithColors(): void + { + $bezier = new Bezier([new Point(10, 20)]); + $bgColor = new Color(255, 0, 0); + $borderColor = new Color(0, 255, 0); + $bezier->setBackgroundColor($bgColor); + $bezier->setBorder($borderColor, 2); + + $clone = clone $bezier; + + // Colors should be deep-cloned + $this->assertNotSame($bezier->backgroundColor(), $clone->backgroundColor()); + $this->assertNotSame($bezier->borderColor(), $clone->borderColor()); + } +} diff --git a/tests/Unit/Geometry/CircleTest.php b/tests/Unit/Geometry/CircleTest.php new file mode 100644 index 000000000..33c6df446 --- /dev/null +++ b/tests/Unit/Geometry/CircleTest.php @@ -0,0 +1,52 @@ +assertInstanceOf(Circle::class, $circle); + $this->assertEquals(100, $circle->diameter()); + $this->assertInstanceOf(Point::class, $circle->pivot()); + } + + public function testSetGetDiameter(): void + { + $circle = new Circle(100, new Point(1, 2)); + $this->assertEquals(100, $circle->diameter()); + $result = $circle->setDiameter(200); + $this->assertInstanceOf(Circle::class, $result); + $this->assertEquals(200, $result->diameter()); + $this->assertEquals(200, $circle->diameter()); + } + + public function testSetGetRadius(): void + { + $circle = new Circle(100, new Point(1, 2)); + $this->assertEquals(50, $circle->radius()); + $result = $circle->setRadius(200); + $this->assertInstanceOf(Circle::class, $result); + $this->assertEquals(400, $result->diameter()); + $this->assertEquals(400, $circle->diameter()); + $this->assertEquals(200, $result->radius()); + $this->assertEquals(200, $circle->radius()); + } + + public function testFactory(): void + { + $circle = new Circle(100, new Point(1, 2)); + $factory = $circle->factory(); + $this->assertInstanceOf(CircleFactory::class, $factory); + } +} diff --git a/tests/Unit/Geometry/EllipseTest.php b/tests/Unit/Geometry/EllipseTest.php new file mode 100644 index 000000000..f1194a73f --- /dev/null +++ b/tests/Unit/Geometry/EllipseTest.php @@ -0,0 +1,117 @@ +assertInstanceOf(Ellipse::class, $ellipse); + $this->assertEquals(10, $ellipse->width()); + $this->assertEquals(20, $ellipse->height()); + } + + public function testPosition(): void + { + $ellipse = new Ellipse(10, 20, new Point(100, 200)); + $this->assertInstanceOf(Point::class, $ellipse->position()); + $this->assertEquals(100, $ellipse->position()->x()); + $this->assertEquals(200, $ellipse->position()->y()); + + $this->assertInstanceOf(Point::class, $ellipse->pivot()); + $this->assertEquals(100, $ellipse->pivot()->x()); + $this->assertEquals(200, $ellipse->pivot()->y()); + } + + public function testSetPosition(): void + { + $ellipse = new Ellipse(10, 20); + $this->assertEquals(0, $ellipse->position()->x()); + $this->assertEquals(0, $ellipse->position()->y()); + + $result = $ellipse->setPosition(new Point(50, 60)); + $this->assertInstanceOf(Ellipse::class, $result); + $this->assertEquals(50, $ellipse->position()->x()); + $this->assertEquals(60, $ellipse->position()->y()); + } + + public function testSetSize(): void + { + $ellipse = new Ellipse(10, 20, new Point(100, 200)); + $this->assertEquals(10, $ellipse->width()); + $this->assertEquals(20, $ellipse->height()); + $result = $ellipse->setSize(100, 200); + $this->assertInstanceOf(Ellipse::class, $result); + $this->assertEquals(100, $ellipse->width()); + $this->assertEquals(200, $ellipse->height()); + } + + public function testSetWidthHeight(): void + { + $ellipse = new Ellipse(10, 20, new Point(100, 200)); + $this->assertEquals(10, $ellipse->width()); + $this->assertEquals(20, $ellipse->height()); + $result = $ellipse->setWidth(100); + $this->assertInstanceOf(Ellipse::class, $result); + $this->assertEquals(100, $ellipse->width()); + $this->assertEquals(20, $ellipse->height()); + $result = $ellipse->setHeight(200); + $this->assertInstanceOf(Ellipse::class, $result); + $this->assertEquals(100, $ellipse->width()); + $this->assertEquals(200, $ellipse->height()); + } + + public function testFactory(): void + { + $ellipse = new Ellipse(10, 20); + $factory = $ellipse->factory(); + $this->assertInstanceOf(EllipseFactory::class, $factory); + } + + public function testAdjust(): void + { + $ellipse = new Ellipse(10, 10); + $this->assertEquals(null, $ellipse->backgroundColor()); + $adjusted = $ellipse->adjust(fn(EllipseFactory $factory) => $factory->background('f50')); + $this->assertEquals(null, $ellipse->backgroundColor()); + $this->assertEquals('f50', $adjusted->backgroundColor()); + } + + public function testClone(): void + { + $ellipse = new Ellipse(10, 20, new Point(100, 200)); + $clone = clone $ellipse; + + $this->assertEquals(100, $clone->pivot()->x()); + $this->assertEquals(200, $clone->pivot()->y()); + + // verify deep copy — changing clone should not affect original + $clone->pivot()->setX(99); + $this->assertEquals(100, $ellipse->pivot()->x()); + $this->assertEquals(99, $clone->pivot()->x()); + } + + public function testCloneWithColors(): void + { + $ellipse = new Ellipse(10, 20, new Point(100, 200)); + $ellipse->setBackgroundColor(new Color(255, 0, 0)); + $ellipse->setBorderColor(new Color(0, 0, 255)); + + $clone = clone $ellipse; + + // AbstractColor instances are deep cloned + $this->assertNotSame($ellipse->backgroundColor(), $clone->backgroundColor()); + $this->assertNotSame($ellipse->borderColor(), $clone->borderColor()); + } +} diff --git a/tests/Unit/Geometry/Factories/BezierFactoryTest.php b/tests/Unit/Geometry/Factories/BezierFactoryTest.php new file mode 100644 index 000000000..db81e275b --- /dev/null +++ b/tests/Unit/Geometry/Factories/BezierFactoryTest.php @@ -0,0 +1,51 @@ +background('f00'); + $bezier->border('ff0', 10); + $bezier->point(300, 260); + $bezier->point(150, 335); + $bezier->point(300, 410); + }); + + $bezier = $factory->drawable(); + $this->assertInstanceOf(Bezier::class, $bezier); + $this->assertTrue($bezier->hasBackgroundColor()); + $this->assertEquals('f00', $bezier->backgroundColor()); + $this->assertEquals('ff0', $bezier->borderColor()); + $this->assertEquals(10, $bezier->borderSize()); + $this->assertEquals(3, $bezier->count()); + } + + public function testBuild(): void + { + $bezier = BezierFactory::build(function (BezierFactory $bezier): void { + $bezier->background('f00'); + $bezier->border('ff0', 10); + $bezier->point(300, 260); + $bezier->point(150, 335); + $bezier->point(300, 410); + }); + + $this->assertInstanceOf(Bezier::class, $bezier); + $this->assertTrue($bezier->hasBackgroundColor()); + $this->assertEquals('f00', $bezier->backgroundColor()); + $this->assertEquals('ff0', $bezier->borderColor()); + $this->assertEquals(10, $bezier->borderSize()); + $this->assertEquals(3, $bezier->count()); + } +} diff --git a/tests/Unit/Geometry/Factories/CircleFactoryTest.php b/tests/Unit/Geometry/Factories/CircleFactoryTest.php new file mode 100644 index 000000000..9c0ed2fb8 --- /dev/null +++ b/tests/Unit/Geometry/Factories/CircleFactoryTest.php @@ -0,0 +1,57 @@ +background('fff'); + $circle->border('ccc', 10); + $circle->radius(100); + $circle->diameter(1000); + $circle->at(20, 30); + }); + + $circle = $factory->drawable(); + $this->assertInstanceOf(Ellipse::class, $circle); + $this->assertTrue($circle->hasBackgroundColor()); + $this->assertEquals('fff', $circle->backgroundColor()); + $this->assertEquals('ccc', $circle->borderColor()); + $this->assertEquals(10, $circle->borderSize()); + $this->assertEquals(1000, $circle->width()); + $this->assertEquals(1000, $circle->height()); + $this->assertEquals(20, $circle->position()->x()); + $this->assertEquals(30, $circle->position()->y()); + } + + public function testBuild(): void + { + $circle = CircleFactory::build(function (CircleFactory $circle): void { + $circle->background('fff'); + $circle->border('ccc', 10); + $circle->radius(100); + $circle->diameter(1000); + $circle->at(20, 30); + }); + + $this->assertInstanceOf(Ellipse::class, $circle); + $this->assertTrue($circle->hasBackgroundColor()); + $this->assertEquals('fff', $circle->backgroundColor()); + $this->assertEquals('ccc', $circle->borderColor()); + $this->assertEquals(10, $circle->borderSize()); + $this->assertEquals(1000, $circle->width()); + $this->assertEquals(1000, $circle->height()); + $this->assertEquals(20, $circle->position()->x()); + $this->assertEquals(30, $circle->position()->y()); + } +} diff --git a/tests/Unit/Geometry/Factories/DrawableTest.php b/tests/Unit/Geometry/Factories/DrawableTest.php new file mode 100644 index 000000000..00e933352 --- /dev/null +++ b/tests/Unit/Geometry/Factories/DrawableTest.php @@ -0,0 +1,50 @@ +assertInstanceOf(Bezier::class, Drawable::bezier()); + } + + public function testCircle(): void + { + $this->assertInstanceOf(Circle::class, Drawable::circle()); + } + + public function testEllipse(): void + { + $this->assertInstanceOf(Ellipse::class, Drawable::ellipse()); + } + + public function testLine(): void + { + $this->assertInstanceOf(Line::class, Drawable::line()); + } + + public function testPolygon(): void + { + $this->assertInstanceOf(Polygon::class, Drawable::polygon()); + } + + public function testRectangle(): void + { + $this->assertInstanceOf(Rectangle::class, Drawable::rectangle()); + } +} diff --git a/tests/Unit/Geometry/Factories/EllipseFactoryTest.php b/tests/Unit/Geometry/Factories/EllipseFactoryTest.php new file mode 100644 index 000000000..7f08217ff --- /dev/null +++ b/tests/Unit/Geometry/Factories/EllipseFactoryTest.php @@ -0,0 +1,59 @@ +background('fff'); + $ellipse->border('ccc', 10); + $ellipse->width(100); + $ellipse->height(200); + $ellipse->size(1000, 2000); + $ellipse->at(20, 30); + }); + + $ellipse = $factory->drawable(); + $this->assertInstanceOf(Ellipse::class, $ellipse); + $this->assertTrue($ellipse->hasBackgroundColor()); + $this->assertEquals('fff', $ellipse->backgroundColor()); + $this->assertEquals('ccc', $ellipse->borderColor()); + $this->assertEquals(10, $ellipse->borderSize()); + $this->assertEquals(1000, $ellipse->width()); + $this->assertEquals(2000, $ellipse->height()); + $this->assertEquals(20, $ellipse->position()->x()); + $this->assertEquals(30, $ellipse->position()->y()); + } + + public function testBuild(): void + { + $ellipse = EllipseFactory::build(function (EllipseFactory $ellipse): void { + $ellipse->background('fff'); + $ellipse->border('ccc', 10); + $ellipse->width(100); + $ellipse->height(200); + $ellipse->size(1000, 2000); + $ellipse->at(20, 30); + }); + + $this->assertInstanceOf(Ellipse::class, $ellipse); + $this->assertTrue($ellipse->hasBackgroundColor()); + $this->assertEquals('fff', $ellipse->backgroundColor()); + $this->assertEquals('ccc', $ellipse->borderColor()); + $this->assertEquals(10, $ellipse->borderSize()); + $this->assertEquals(1000, $ellipse->width()); + $this->assertEquals(2000, $ellipse->height()); + $this->assertEquals(20, $ellipse->position()->x()); + $this->assertEquals(30, $ellipse->position()->y()); + } +} diff --git a/tests/Unit/Geometry/Factories/LineFactoryTest.php b/tests/Unit/Geometry/Factories/LineFactoryTest.php new file mode 100644 index 000000000..a910985b2 --- /dev/null +++ b/tests/Unit/Geometry/Factories/LineFactoryTest.php @@ -0,0 +1,57 @@ +color('fff'); + $line->background('fff'); + $line->border('fff', 10); + $line->width(10); + $line->from(100, 200); + $line->to(300, 400); + }); + + $line = $factory->drawable(); + $this->assertInstanceOf(Line::class, $line); + $this->assertTrue($line->hasBackgroundColor()); + $this->assertEquals('fff', $line->backgroundColor()); + $this->assertEquals(100, $line->start()->x()); + $this->assertEquals(200, $line->start()->y()); + $this->assertEquals(300, $line->end()->x()); + $this->assertEquals(400, $line->end()->y()); + $this->assertEquals(10, $line->width()); + } + + public function testBuild(): void + { + $line = LineFactory::build(function (LineFactory $line): void { + $line->color('fff'); + $line->background('fff'); + $line->border('fff', 10); + $line->width(10); + $line->from(100, 200); + $line->to(300, 400); + }); + + $this->assertInstanceOf(Line::class, $line); + $this->assertTrue($line->hasBackgroundColor()); + $this->assertEquals('fff', $line->backgroundColor()); + $this->assertEquals(100, $line->start()->x()); + $this->assertEquals(200, $line->start()->y()); + $this->assertEquals(300, $line->end()->x()); + $this->assertEquals(400, $line->end()->y()); + $this->assertEquals(10, $line->width()); + } +} diff --git a/tests/Unit/Geometry/Factories/PolygonFactoryTest.php b/tests/Unit/Geometry/Factories/PolygonFactoryTest.php new file mode 100644 index 000000000..4b46e4323 --- /dev/null +++ b/tests/Unit/Geometry/Factories/PolygonFactoryTest.php @@ -0,0 +1,51 @@ +background('fff'); + $polygon->border('ccc', 10); + $polygon->point(1, 2); + $polygon->point(3, 4); + $polygon->point(5, 6); + }); + + $polygon = $factory->drawable(); + $this->assertInstanceOf(Polygon::class, $polygon); + $this->assertTrue($polygon->hasBackgroundColor()); + $this->assertEquals('fff', $polygon->backgroundColor()); + $this->assertEquals('ccc', $polygon->borderColor()); + $this->assertEquals(10, $polygon->borderSize()); + $this->assertEquals(3, $polygon->count()); + } + + public function testBuild(): void + { + $polygon = PolygonFactory::build(function (PolygonFactory $polygon): void { + $polygon->background('fff'); + $polygon->border('ccc', 10); + $polygon->point(1, 2); + $polygon->point(3, 4); + $polygon->point(5, 6); + }); + + $this->assertInstanceOf(Polygon::class, $polygon); + $this->assertTrue($polygon->hasBackgroundColor()); + $this->assertEquals('fff', $polygon->backgroundColor()); + $this->assertEquals('ccc', $polygon->borderColor()); + $this->assertEquals(10, $polygon->borderSize()); + $this->assertEquals(3, $polygon->count()); + } +} diff --git a/tests/Unit/Geometry/Factories/RectangleFactoryTest.php b/tests/Unit/Geometry/Factories/RectangleFactoryTest.php new file mode 100644 index 000000000..0eeff1ce4 --- /dev/null +++ b/tests/Unit/Geometry/Factories/RectangleFactoryTest.php @@ -0,0 +1,59 @@ +background('fff'); + $rectangle->border('ccc', 10); + $rectangle->width(100); + $rectangle->height(200); + $rectangle->size(1000, 2000); + $rectangle->at(20, 30); + }); + + $rectangle = $factory->drawable(); + $this->assertInstanceOf(Rectangle::class, $rectangle); + $this->assertTrue($rectangle->hasBackgroundColor()); + $this->assertEquals('fff', $rectangle->backgroundColor()); + $this->assertEquals('ccc', $rectangle->borderColor()); + $this->assertEquals(10, $rectangle->borderSize()); + $this->assertEquals(1000, $rectangle->width()); + $this->assertEquals(2000, $rectangle->height()); + $this->assertEquals(20, $rectangle->position()->x()); + $this->assertEquals(30, $rectangle->position()->y()); + } + + public function testBuild(): void + { + $rectangle = RectangleFactory::build(function (RectangleFactory $rectangle): void { + $rectangle->background('fff'); + $rectangle->border('ccc', 10); + $rectangle->width(100); + $rectangle->height(200); + $rectangle->size(1000, 2000); + $rectangle->at(20, 30); + }); + + $this->assertInstanceOf(Rectangle::class, $rectangle); + $this->assertTrue($rectangle->hasBackgroundColor()); + $this->assertEquals('fff', $rectangle->backgroundColor()); + $this->assertEquals('ccc', $rectangle->borderColor()); + $this->assertEquals(10, $rectangle->borderSize()); + $this->assertEquals(1000, $rectangle->width()); + $this->assertEquals(2000, $rectangle->height()); + $this->assertEquals(20, $rectangle->position()->x()); + $this->assertEquals(30, $rectangle->position()->y()); + } +} diff --git a/tests/Unit/Geometry/LineTest.php b/tests/Unit/Geometry/LineTest.php new file mode 100644 index 000000000..3b8daf27e --- /dev/null +++ b/tests/Unit/Geometry/LineTest.php @@ -0,0 +1,158 @@ +assertInstanceOf(Line::class, $line); + } + + public function testPosition(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $this->assertEquals(1, $line->position()->x()); + $this->assertEquals(2, $line->position()->y()); + } + + public function testSetGetStart(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $this->assertEquals(1, $line->start()->x()); + $this->assertEquals(2, $line->start()->y()); + $result = $line->setStart(new Point(10, 20)); + $this->assertInstanceOf(Line::class, $result); + $this->assertEquals(10, $line->start()->x()); + $this->assertEquals(20, $line->start()->y()); + } + + public function testSetGetEnd(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $this->assertEquals(3, $line->end()->x()); + $this->assertEquals(4, $line->end()->y()); + $result = $line->setEnd(new Point(30, 40)); + $this->assertInstanceOf(Line::class, $result); + $this->assertEquals(30, $line->end()->x()); + $this->assertEquals(40, $line->end()->y()); + } + + public function testFrom(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $this->assertEquals(1, $line->start()->x()); + $this->assertEquals(2, $line->start()->y()); + $result = $line->from(10, 20); + $this->assertInstanceOf(Line::class, $result); + $this->assertEquals(10, $line->start()->x()); + $this->assertEquals(20, $line->start()->y()); + } + + public function testTo(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $this->assertEquals(3, $line->end()->x()); + $this->assertEquals(4, $line->end()->y()); + $result = $line->to(30, 40); + $this->assertInstanceOf(Line::class, $result); + $this->assertEquals(30, $line->end()->x()); + $this->assertEquals(40, $line->end()->y()); + } + + public function testSetGetWidth(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $this->assertEquals(10, $line->width()); + $result = $line->setWidth(20); + $this->assertInstanceOf(Line::class, $result); + $this->assertEquals(20, $line->width()); + } + + public function testSetPosition(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $this->assertEquals(1, $line->position()->x()); + $this->assertEquals(2, $line->position()->y()); + $result = $line->setPosition(new Point(50, 60)); + $this->assertInstanceOf(Line::class, $result); + $this->assertEquals(50, $line->position()->x()); + $this->assertEquals(60, $line->position()->y()); + // setPosition also changes start + $this->assertEquals(50, $line->start()->x()); + $this->assertEquals(60, $line->start()->y()); + } + + public function testFactory(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $factory = $line->factory(); + $this->assertInstanceOf(LineFactory::class, $factory); + } + + public function testAdjust(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $this->assertEquals(null, $line->backgroundColor()); + $adjusted = $line->adjust(fn(LineFactory $factory) => $factory->background('f50')); + $this->assertEquals(null, $line->backgroundColor()); + $this->assertEquals('f50', $adjusted->backgroundColor()); + } + + public function testCloneWithColors(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $bgColor = new Color(255, 0, 0); + $borderColor = new Color(0, 255, 0); + $line->setBackgroundColor($bgColor); + $line->setBorderColor($borderColor); + + $clone = clone $line; + + // colors are cloned (independent instances) + $this->assertNotSame($bgColor, $clone->backgroundColor()); + $this->assertNotSame($borderColor, $clone->borderColor()); + $this->assertInstanceOf(Color::class, $clone->backgroundColor()); + $this->assertInstanceOf(Color::class, $clone->borderColor()); + } + + public function testClone(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $clone = clone $line; + + $this->assertEquals(1, $clone->start()->x()); + $this->assertEquals(2, $clone->start()->y()); + $this->assertEquals(3, $clone->end()->x()); + $this->assertEquals(4, $clone->end()->y()); + + // verify deep copy — changing clone should not affect original + $clone->start()->setX(99); + $this->assertEquals(1, $line->start()->x()); + $this->assertEquals(99, $clone->start()->x()); + } + + public function testCloneWithStringColors(): void + { + $line = new Line(new Point(1, 2), new Point(3, 4), 10); + $line->setBackgroundColor('ff0000'); + $line->setBorderColor('0000ff'); + + $clone = clone $line; + + // string colors are preserved after clone + $this->assertEquals('ff0000', $clone->backgroundColor()); + $this->assertEquals('0000ff', $clone->borderColor()); + } +} diff --git a/tests/Unit/Geometry/PixelTest.php b/tests/Unit/Geometry/PixelTest.php new file mode 100644 index 000000000..00594e381 --- /dev/null +++ b/tests/Unit/Geometry/PixelTest.php @@ -0,0 +1,23 @@ +setBackgroundColor($color); + $this->assertEquals($color, $pixel->backgroundColor()); + $this->assertInstanceOf(Pixel::class, $result); + } +} diff --git a/tests/Unit/Geometry/PointTest.php b/tests/Unit/Geometry/PointTest.php new file mode 100644 index 000000000..31c38a893 --- /dev/null +++ b/tests/Unit/Geometry/PointTest.php @@ -0,0 +1,108 @@ +assertInstanceOf(Point::class, $point); + $this->assertEquals(0, $point->x()); + $this->assertEquals(0, $point->y()); + } + + public function testConstructorWithParameters(): void + { + $point = new Point(40, 50); + $this->assertInstanceOf(Point::class, $point); + $this->assertEquals(40, $point->x()); + $this->assertEquals(50, $point->y()); + } + + public function testIteration(): void + { + $point = new Point(40, 50); + foreach ($point as $value) { + $this->assertIsInt($value); + } + } + + public function testGetSetX(): void + { + $point = new Point(0, 0); + $point->setX(100); + $this->assertEquals(100, $point->x()); + $this->assertEquals(0, $point->y()); + } + + public function testGetSetY(): void + { + $point = new Point(0, 0); + $point->setY(100); + $this->assertEquals(0, $point->x()); + $this->assertEquals(100, $point->y()); + } + + public function testmoveX(): void + { + $point = new Point(50, 50); + $point->moveX(100); + $this->assertEquals(150, $point->x()); + $this->assertEquals(50, $point->y()); + } + + public function testmoveY(): void + { + $point = new Point(50, 50); + $point->moveY(100); + $this->assertEquals(50, $point->x()); + $this->assertEquals(150, $point->y()); + } + + public function testMove(): void + { + $point = new Point(50, 50); + $result = $point->move(10, 20); + $this->assertInstanceOf(Point::class, $result); + $this->assertEquals(60, $point->x()); + $this->assertEquals(70, $point->y()); + } + + public function testSetPosition(): void + { + $point = new Point(0, 0); + $point->setPosition(100, 200); + $this->assertEquals(100, $point->x()); + $this->assertEquals(200, $point->y()); + } + + public function testRotate(): void + { + $point = new Point(30, 0); + $point->rotate(90, new Point(0, 0)); + $this->assertEquals(0, $point->x()); + $this->assertEquals(30, $point->y()); + + $point->rotate(90, new Point(0, 0)); + $this->assertEquals(-30, $point->x()); + $this->assertEquals(0, $point->y()); + + $point = new Point(300, 200); + $point->rotate(90, new Point(0, 0)); + $this->assertEquals(-200, $point->x()); + $this->assertEquals(300, $point->y()); + + $point = new Point(0, 74); + $point->rotate(45, new Point(0, 0)); + $this->assertEquals(-52, $point->x()); + $this->assertEquals(52, $point->y()); + } +} diff --git a/tests/Unit/Geometry/PolygonTest.php b/tests/Unit/Geometry/PolygonTest.php new file mode 100644 index 000000000..492a0ed7d --- /dev/null +++ b/tests/Unit/Geometry/PolygonTest.php @@ -0,0 +1,712 @@ +assertInstanceOf(Polygon::class, $poly); + $this->assertEquals(0, $poly->count()); + } + + public function testCount(): void + { + $poly = new Polygon([new Point(), new Point()]); + $this->assertEquals(2, $poly->count()); + } + + public function testArrayAccess(): void + { + $poly = new Polygon([new Point(), new Point()]); + $this->assertInstanceOf(Point::class, $poly[0]); + $this->assertInstanceOf(Point::class, $poly[1]); + } + + public function testAddPoint(): void + { + $poly = new Polygon([new Point(), new Point()]); + $this->assertEquals(2, $poly->count()); + $result = $poly->addPoint(new Point()); + $this->assertEquals(3, $poly->count()); + $this->assertInstanceOf(Polygon::class, $result); + } + + public function testGetCenterPoint(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(20, 0), + new Point(20, -20), + new Point(0, -20), + ]); + + $result = $poly->centerPoint(); + $this->assertEquals(10, $result->x()); + $this->assertEquals(-10, $result->y()); + + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(0, 0)); + + $result = $poly->centerPoint(); + $this->assertEquals(150, $result->x()); + $this->assertEquals(-100, $result->y()); + } + + public function testGetWidth(): void + { + $poly = new Polygon([ + new Point(12, 45), + new Point(-23, -49), + new Point(3, 566), + ]); + + $this->assertEquals($poly->width(), 35); + } + + public function testGetHeight(): void + { + $poly = new Polygon([ + new Point(12, 45), + new Point(-23, -49), + new Point(3, 566), + ]); + + $this->assertEquals(615, $poly->height()); + + $poly = new Polygon([ + new Point(250, 207), + new Point(473, 207), + new Point(473, 250), + new Point(250, 250), + ], new Point(250, 250)); + + $this->assertEquals(43, $poly->height()); + } + + public function testFirst(): void + { + $poly = new Polygon([ + new Point(12, 45), + new Point(-23, -49), + new Point(3, 566), + ]); + + $this->assertEquals(12, $poly->first()->x()); + $this->assertEquals(45, $poly->first()->y()); + } + + public function testFirstEmpty(): void + { + $poly = new Polygon(); + $this->assertNull($poly->first()); + } + + public function testLast(): void + { + $poly = new Polygon([ + new Point(12, 45), + new Point(-23, -49), + new Point(3, 566), + ]); + + $this->assertEquals(3, $poly->last()->x()); + $this->assertEquals(566, $poly->last()->y()); + } + + public function testLastEmpty(): void + { + $poly = new Polygon(); + $this->assertNull($poly->last()); + } + + public function testOffsetExists(): void + { + $poly = new Polygon(); + $this->assertFalse($poly->offsetExists(0)); + $this->assertFalse($poly->offsetExists(1)); + $poly->addPoint(new Point(0, 0)); + $this->assertTrue($poly->offsetExists(0)); + $this->assertFalse($poly->offsetExists(1)); + } + + public function testOffsetSetUnset(): void + { + $poly = new Polygon(); + $poly->offsetSet(0, new Point()); + $poly->offsetSet(2, new Point()); + $this->assertTrue($poly->offsetExists(0)); + $this->assertFalse($poly->offsetExists(1)); + $this->assertTrue($poly->offsetExists(2)); + $poly->offsetUnset(2); + $this->assertTrue($poly->offsetExists(0)); + $this->assertFalse($poly->offsetExists(1)); + $this->assertFalse($poly->offsetExists(2)); + } + + public function testOffsetGet(): void + { + $point = new Point(10, 20); + $poly = new Polygon([$point]); + $result = $poly->offsetGet(0); + $this->assertInstanceOf(Point::class, $result); + $this->assertEquals(10, $result->x()); + $this->assertEquals(20, $result->y()); + } + + public function testGetSetPivotPoint(): void + { + $poly = new Polygon(); + $this->assertInstanceOf(Point::class, $poly->pivot()); + $this->assertEquals(0, $poly->pivot()->x()); + $this->assertEquals(0, $poly->pivot()->y()); + $result = $poly->setPivot(new Point(12, 34)); + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(12, $poly->pivot()->x()); + $this->assertEquals(34, $poly->pivot()->y()); + } + + public function testGetMostLeftPoint(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(-32, -200), + ], new Point(0, 0)); + + $result = $poly->mostLeftPoint(); + $this->assertEquals(-32, $result->x()); + $this->assertEquals(-200, $result->y()); + } + + public function testGetMostRightPoint(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(350, 0), + new Point(300, -200), + new Point(-32, -200), + ], new Point(0, 0)); + + $result = $poly->mostRightPoint(); + $this->assertEquals(350, $result->x()); + $this->assertEquals(0, $result->y()); + } + + public function testGetMostTopPoint(): void + { + $poly = new Polygon([ + new Point(0, 100), + new Point(350, 0), + new Point(300, -200), + new Point(-32, 200), + ], new Point(0, 0)); + + $result = $poly->mostTopPoint(); + $this->assertEquals(-32, $result->x()); + $this->assertEquals(200, $result->y()); + } + + public function testGetMostBottomPoint(): void + { + $poly = new Polygon([ + new Point(0, 100), + new Point(350, 0), + new Point(300, -200), + new Point(-32, 200), + ], new Point(0, 0)); + + $result = $poly->mostBottomPoint(); + $this->assertEquals(300, $result->x()); + $this->assertEquals(-200, $result->y()); + } + + public function testAlignHorizontallyCenter(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(0, 0)); + + $result = $poly->alignHorizontally(Alignment::CENTER); + + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(-150, $result[0]->x()); + $this->assertEquals(0, $result[0]->y()); + $this->assertEquals(150, $result[1]->x()); + $this->assertEquals(0, $result[1]->y()); + $this->assertEquals(150, $result[2]->x()); + $this->assertEquals(-200, $result[2]->y()); + $this->assertEquals(-150, $result[3]->x()); + $this->assertEquals(-200, $result[3]->y()); + + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(-1000, -1000)); + + $result = $poly->alignHorizontally(Alignment::CENTER); + + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(-1150, $result[0]->x()); + $this->assertEquals(0, $result[0]->y()); + $this->assertEquals(-850, $result[1]->x()); + $this->assertEquals(0, $result[1]->y()); + $this->assertEquals(-850, $result[2]->x()); + $this->assertEquals(-200, $result[2]->y()); + $this->assertEquals(-1150, $result[3]->x()); + $this->assertEquals(-200, $result[3]->y()); + } + + public function testAlignHorizontallyLeft(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(100, 100)); + + $result = $poly->alignHorizontally(Alignment::LEFT); + + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(100, $result[0]->x()); + $this->assertEquals(0, $result[0]->y()); + $this->assertEquals(400, $result[1]->x()); + $this->assertEquals(0, $result[1]->y()); + $this->assertEquals(400, $result[2]->x()); + $this->assertEquals(-200, $result[2]->y()); + $this->assertEquals(100, $result[3]->x()); + $this->assertEquals(-200, $result[3]->y()); + + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(-1000, -1000)); + + $result = $poly->alignHorizontally(Alignment::LEFT); + + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(-1000, $result[0]->x()); + $this->assertEquals(0, $result[0]->y()); + $this->assertEquals(-700, $result[1]->x()); + $this->assertEquals(0, $result[1]->y()); + $this->assertEquals(-700, $result[2]->x()); + $this->assertEquals(-200, $result[2]->y()); + $this->assertEquals(-1000, $result[3]->x()); + $this->assertEquals(-200, $result[3]->y()); + } + + public function testAlignHorizontallyRight(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(100, 100)); + + $result = $poly->alignHorizontally(Alignment::RIGHT); + + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(-200, $result[0]->x()); + $this->assertEquals(0, $result[0]->y()); + $this->assertEquals(100, $result[1]->x()); + $this->assertEquals(0, $result[1]->y()); + $this->assertEquals(100, $result[2]->x()); + $this->assertEquals(-200, $result[2]->y()); + $this->assertEquals(-200, $result[3]->x()); + $this->assertEquals(-200, $result[3]->y()); + + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(-1000, -1000)); + + $result = $poly->alignHorizontally(Alignment::RIGHT); + + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(-1300, $result[0]->x()); + $this->assertEquals(0, $result[0]->y()); + $this->assertEquals(-1000, $result[1]->x()); + $this->assertEquals(0, $result[1]->y()); + $this->assertEquals(-1000, $result[2]->x()); + $this->assertEquals(-200, $result[2]->y()); + $this->assertEquals(-1300, $result[3]->x()); + $this->assertEquals(-200, $result[3]->y()); + } + + public function testAlignVerticallyMiddle(): void + { + $poly = new Polygon([ + new Point(-21, -22), + new Point(91, -135), + new Point(113, -113), + new Point(0, 0), + ], new Point(250, 250)); + + $result = $poly->alignVertically('middle'); + + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(-21, $result[0]->x()); + $this->assertEquals(296, $result[0]->y()); + $this->assertEquals(91, $result[1]->x()); + $this->assertEquals(183, $result[1]->y()); + $this->assertEquals(113, $result[2]->x()); + $this->assertEquals(205, $result[2]->y()); + $this->assertEquals(0, $result[3]->x()); + $this->assertEquals(318, $result[3]->y()); + } + + public function testAlignVerticallyTop(): void + { + $poly = new Polygon([ + new Point(-21, -22), + new Point(91, -135), + new Point(113, -113), + new Point(0, 0), + ], new Point(250, 250)); + + $result = $poly->alignVertically(Alignment::TOP); + + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(-21, $result[0]->x()); + $this->assertEquals(363, $result[0]->y()); + $this->assertEquals(91, $result[1]->x()); + $this->assertEquals(250, $result[1]->y()); + $this->assertEquals(113, $result[2]->x()); + $this->assertEquals(272, $result[2]->y()); + $this->assertEquals(0, $result[3]->x()); + $this->assertEquals(385, $result[3]->y()); + } + + public function testMovePoints(): void + { + $poly = new Polygon([ + new Point(10, 20), + new Point(30, 40) + ]); + + $result = $poly->movePointsX(100); + $this->assertEquals(110, $result[0]->x()); + $this->assertEquals(20, $result[0]->y()); + $this->assertEquals(130, $result[1]->x()); + $this->assertEquals(40, $result[1]->y()); + + $result = $poly->movePointsY(200); + $this->assertEquals(110, $result[0]->x()); + $this->assertEquals(220, $result[0]->y()); + $this->assertEquals(130, $result[1]->x()); + $this->assertEquals(240, $result[1]->y()); + } + + public function testRotate(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(50, 0), + new Point(50, -50), + new Point(0, -50), + ]); + + $result = $poly->rotate(45); + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(0, $result[0]->x()); + $this->assertEquals(0, $result[0]->y()); + $this->assertEquals(35, $result[1]->x()); + $this->assertEquals(35, $result[1]->y()); + $this->assertEquals(70, $result[2]->x()); + $this->assertEquals(0, $result[2]->y()); + $this->assertEquals(35, $result[3]->x()); + $this->assertEquals(-35, $result[3]->y()); + } + + public function testToArray(): void + { + $poly = new Polygon([new Point(0, 0), new Point(50, 0), new Point(50, -50), new Point(0, -50)]); + $this->assertEquals([0, 0, 50, 0, 50, -50, 0, -50], $poly->toArray()); + } + + public function testAlignVerticallyBottom(): void + { + $poly = new Polygon([ + new Point(-21, -22), + new Point(91, -135), + new Point(113, -113), + new Point(0, 0), + ], new Point(250, 250)); + + $result = $poly->alignVertically(Alignment::BOTTOM); + + $this->assertInstanceOf(Polygon::class, $result); + // After bottom alignment, the most bottom point should be shifted relative to pivot + height + } + + public function testAlignVerticallyBottomRight(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(100, 100)); + + $result = $poly->alignVertically(Alignment::BOTTOM_RIGHT); + $this->assertInstanceOf(Polygon::class, $result); + } + + public function testAlignVerticallyTopLeft(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(100, 100)); + + $result = $poly->alignVertically(Alignment::TOP_LEFT); + $this->assertInstanceOf(Polygon::class, $result); + } + + public function testAlignVerticallyTopRight(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(100, 100)); + + $result = $poly->alignVertically(Alignment::TOP_RIGHT); + $this->assertInstanceOf(Polygon::class, $result); + } + + public function testAlignVerticallyBottomLeft(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(100, 100)); + + $result = $poly->alignVertically(Alignment::BOTTOM_LEFT); + $this->assertInstanceOf(Polygon::class, $result); + } + + public function testAlignHorizontallyTopLeft(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(100, 100)); + + $result = $poly->alignHorizontally(Alignment::TOP_LEFT); + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(100, $result[0]->x()); + } + + public function testAlignHorizontallyBottomLeft(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(100, 100)); + + $result = $poly->alignHorizontally(Alignment::BOTTOM_LEFT); + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(100, $result[0]->x()); + } + + public function testAlignHorizontallyTopRight(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(100, 100)); + + $result = $poly->alignHorizontally(Alignment::TOP_RIGHT); + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(-200, $result[0]->x()); + } + + public function testAlignHorizontallyBottomRight(): void + { + $poly = new Polygon([ + new Point(0, 0), + new Point(300, 0), + new Point(300, -200), + new Point(0, -200), + ], new Point(100, 100)); + + $result = $poly->alignHorizontally(Alignment::BOTTOM_RIGHT); + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(-200, $result[0]->x()); + } + + public function testGetIterator(): void + { + $poly = new Polygon([new Point(10, 20), new Point(30, 40)]); + $points = iterator_to_array($poly); + $this->assertCount(2, $points); + $this->assertEquals(10, $points[0]->x()); + $this->assertEquals(30, $points[1]->x()); + } + + public function testPosition(): void + { + $poly = new Polygon([], new Point(50, 60)); + $pos = $poly->position(); + $this->assertEquals(50, $pos->x()); + $this->assertEquals(60, $pos->y()); + } + + public function testSetPosition(): void + { + $poly = new Polygon(); + $result = $poly->setPosition(new Point(100, 200)); + $this->assertSame($poly, $result); + $this->assertEquals(100, $poly->position()->x()); + $this->assertEquals(200, $poly->position()->y()); + } + + public function testFactory(): void + { + $poly = new Polygon([new Point(0, 0), new Point(100, 0), new Point(50, -50)]); + $factory = $poly->factory(); + $this->assertInstanceOf(PolygonFactory::class, $factory); + } + + public function testAdjust(): void + { + $polygon = new Polygon(); + $this->assertEquals(null, $polygon->backgroundColor()); + $adjusted = $polygon->adjust(fn(PolygonFactory $factory) => $factory->background('f50')); + $this->assertEquals(null, $polygon->backgroundColor()); + $this->assertEquals('f50', $adjusted->backgroundColor()); + } + + public function testClone(): void + { + $poly = new Polygon([new Point(10, 20), new Point(30, 40)], new Point(5, 5)); + $clone = clone $poly; + + // Points should be independent + $this->assertEquals(10, $clone[0]->x()); + $clone[0]->setX(999); + $this->assertEquals(10, $poly[0]->x()); + + // Pivot should be independent + $this->assertEquals(5, $poly->pivot()->x()); + $clone->setPivot(new Point(888, 888)); + $this->assertEquals(5, $poly->pivot()->x()); + } + + public function testCloneWithBackgroundColor(): void + { + $poly = new Polygon([new Point(10, 20)]); + $color = new RgbColor( + new Red(255), + new Green(0), + new Blue(0), + new Alpha(1) + ); + $poly->setBackgroundColor($color); + $clone = clone $poly; + + // Background color should be cloned independently + $this->assertNotSame($poly->backgroundColor(), $clone->backgroundColor()); + } + + public function testCloneWithBorderColor(): void + { + $poly = new Polygon([new Point(10, 20)]); + $color = new RgbColor( + new Red(0), + new Green(255), + new Blue(0), + new Alpha(1) + ); + $poly->setBorderColor($color); + $clone = clone $poly; + + // Border color should be cloned independently + $this->assertNotSame($poly->borderColor(), $clone->borderColor()); + } + + public function testSetBorder(): void + { + $poly = new Polygon([new Point(0, 0)]); + $result = $poly->setBorder('ff0000', 3); + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals('ff0000', $poly->borderColor()); + $this->assertEquals(3, $poly->borderSize()); + } + + public function testSetGetBorderSize(): void + { + $poly = new Polygon([new Point(0, 0)]); + $this->assertEquals(0, $poly->borderSize()); + $result = $poly->setBorderSize(5); + $this->assertInstanceOf(Polygon::class, $result); + $this->assertEquals(5, $poly->borderSize()); + } + + public function testHasBorder(): void + { + $poly = new Polygon([new Point(0, 0)]); + $this->assertFalse($poly->hasBorder()); + + // Size alone is not enough + $poly->setBorderSize(3); + $this->assertFalse($poly->hasBorder()); + + // Need both size and color + $poly->setBorderColor('ff0000'); + $this->assertTrue($poly->hasBorder()); + } + + public function testHasBackgroundColor(): void + { + $poly = new Polygon([new Point(0, 0)]); + $this->assertFalse($poly->hasBackgroundColor()); + + $poly->setBackgroundColor('ff0000'); + $this->assertTrue($poly->hasBackgroundColor()); + } +} diff --git a/tests/Unit/Geometry/RectangleTest.php b/tests/Unit/Geometry/RectangleTest.php new file mode 100644 index 000000000..da03bf0c1 --- /dev/null +++ b/tests/Unit/Geometry/RectangleTest.php @@ -0,0 +1,333 @@ +assertEquals(0, $rectangle[0]->x()); + $this->assertEquals(0, $rectangle[0]->y()); + $this->assertEquals(300, $rectangle[1]->x()); + $this->assertEquals(0, $rectangle[1]->y()); + $this->assertEquals(300, $rectangle[2]->x()); + $this->assertEquals(-200, $rectangle[2]->y()); + $this->assertEquals(0, $rectangle[3]->x()); + $this->assertEquals(-200, $rectangle[3]->y()); + $this->assertEquals(300, $rectangle->width()); + $this->assertEquals(200, $rectangle->height()); + } + + public function testFactory(): void + { + $rectangle = new Rectangle(300, 200); + $factory = $rectangle->factory(); + $this->assertInstanceOf(RectangleFactory::class, $factory); + } + + public function testAdjust(): void + { + $rectangle = new Rectangle(300, 200); + $this->assertEquals(null, $rectangle->backgroundColor()); + $adjusted = $rectangle->adjust(fn(RectangleFactory $factory) => $factory->background('f50')); + $this->assertEquals(null, $rectangle->backgroundColor()); + $this->assertEquals('f50', $adjusted->backgroundColor()); + } + + public function testSetSize(): void + { + $rectangle = new Rectangle(300, 200); + $rectangle->setSize(12, 34); + $this->assertEquals(12, $rectangle->width()); + $this->assertEquals(34, $rectangle->height()); + } + + public function testSetWidth(): void + { + $rectangle = new Rectangle(300, 200); + $this->assertEquals(300, $rectangle->width()); + $rectangle->setWidth(400); + $this->assertEquals(400, $rectangle->width()); + } + + public function testSetHeight(): void + { + $rectangle = new Rectangle(300, 200); + $this->assertEquals(200, $rectangle->height()); + $rectangle->setHeight(800); + $this->assertEquals(800, $rectangle->height()); + } + + public function testGetAspectRatio(): void + { + $size = new Rectangle(800, 600); + $this->assertEquals(1.333, round($size->aspectRatio(), 3)); + + $size = new Rectangle(100, 100); + $this->assertEquals(1, $size->aspectRatio()); + + $size = new Rectangle(1920, 1080); + $this->assertEquals(1.778, round($size->aspectRatio(), 3)); + } + + public function testFitsInto(): void + { + $box = new Rectangle(800, 600); + $fits = $box->fitsWithin(new Rectangle(100, 100)); + $this->assertFalse($fits); + + $box = new Rectangle(800, 600); + $fits = $box->fitsWithin(new Rectangle(1000, 100)); + $this->assertFalse($fits); + + $box = new Rectangle(800, 600); + $fits = $box->fitsWithin(new Rectangle(100, 1000)); + $this->assertFalse($fits); + + $box = new Rectangle(800, 600); + $fits = $box->fitsWithin(new Rectangle(800, 600)); + $this->assertTrue($fits); + + $box = new Rectangle(800, 600); + $fits = $box->fitsWithin(new Rectangle(1000, 1000)); + $this->assertTrue($fits); + + $box = new Rectangle(100, 100); + $fits = $box->fitsWithin(new Rectangle(800, 600)); + $this->assertTrue($fits); + + $box = new Rectangle(100, 100); + $fits = $box->fitsWithin(new Rectangle(80, 60)); + $this->assertFalse($fits); + } + + public function testIsLandscape(): void + { + $box = new Rectangle(100, 100); + $this->assertFalse($box->isLandscape()); + + $box = new Rectangle(100, 200); + $this->assertFalse($box->isLandscape()); + + $box = new Rectangle(300, 200); + $this->assertTrue($box->isLandscape()); + } + + public function testIsPortrait(): void + { + $box = new Rectangle(100, 100); + $this->assertFalse($box->isPortrait()); + + $box = new Rectangle(200, 100); + $this->assertFalse($box->isPortrait()); + + $box = new Rectangle(200, 300); + $this->assertTrue($box->isPortrait()); + } + + public function testSetGetPivot(): void + { + $box = new Rectangle(800, 600); + $pivot = $box->pivot(); + $this->assertInstanceOf(Point::class, $pivot); + $this->assertEquals(0, $pivot->x()); + $result = $box->setPivot(new Point(10, 0)); + $this->assertInstanceOf(Rectangle::class, $result); + $this->assertEquals(10, $box->pivot()->x()); + } + + public function testAlignPivot(): void + { + $box = new Rectangle(640, 480); + $this->assertEquals(0, $box->pivot()->x()); + $this->assertEquals(0, $box->pivot()->y()); + + $box->movePivot(Alignment::TOP_LEFT, 3, 3); + $this->assertEquals(3, $box->pivot()->x()); + $this->assertEquals(3, $box->pivot()->y()); + + $box->movePivot(Alignment::TOP, 3, 3); + $this->assertEquals(323, $box->pivot()->x()); + $this->assertEquals(3, $box->pivot()->y()); + + $box->movePivot(Alignment::TOP_RIGHT, 3, 3); + $this->assertEquals(637, $box->pivot()->x()); + $this->assertEquals(3, $box->pivot()->y()); + + $box->movePivot(Alignment::LEFT, 3, 3); + $this->assertEquals(3, $box->pivot()->x()); + $this->assertEquals(243, $box->pivot()->y()); + + $box->movePivot(Alignment::CENTER, 3, 3); + $this->assertEquals(323, $box->pivot()->x()); + $this->assertEquals(243, $box->pivot()->y()); + + $box->movePivot(Alignment::RIGHT, 3, 3); + $this->assertEquals(637, $box->pivot()->x()); + $this->assertEquals(243, $box->pivot()->y()); + + $box->movePivot(Alignment::BOTTOM_LEFT, 3, 3); + $this->assertEquals(3, $box->pivot()->x()); + $this->assertEquals(477, $box->pivot()->y()); + + $box->movePivot(Alignment::BOTTOM, 3, 3); + $this->assertEquals(323, $box->pivot()->x()); + $this->assertEquals(477, $box->pivot()->y()); + + $result = $box->movePivot(Alignment::BOTTOM_RIGHT, 3, 3); + $this->assertEquals(637, $box->pivot()->x()); + $this->assertEquals(477, $box->pivot()->y()); + + $this->assertInstanceOf(Rectangle::class, $result); + } + + public function testAlignPivotTo(): void + { + $container = new Rectangle(800, 600); + $size = new Rectangle(200, 100); + $size->alignPivotTo($container, Alignment::CENTER); + $this->assertEquals(300, $size->pivot()->x()); + $this->assertEquals(250, $size->pivot()->y()); + + $container = new Rectangle(800, 600); + $size = new Rectangle(100, 100); + $size->alignPivotTo($container, Alignment::CENTER); + $this->assertEquals(350, $size->pivot()->x()); + $this->assertEquals(250, $size->pivot()->y()); + + $container = new Rectangle(800, 600); + $size = new Rectangle(800, 600); + $size->alignPivotTo($container, Alignment::CENTER); + $this->assertEquals(0, $size->pivot()->x()); + $this->assertEquals(0, $size->pivot()->y()); + + $container = new Rectangle(100, 100); + $size = new Rectangle(800, 600); + $size->alignPivotTo($container, Alignment::CENTER); + $this->assertEquals(-350, $size->pivot()->x()); + $this->assertEquals(-250, $size->pivot()->y()); + + $container = new Rectangle(100, 100); + $size = new Rectangle(800, 600); + $size->alignPivotTo($container, Alignment::BOTTOM_RIGHT); + $this->assertEquals(-700, $size->pivot()->x()); + $this->assertEquals(-500, $size->pivot()->y()); + } + + public function testOffsetTo(): void + { + $container = new Rectangle(800, 600); + $input = new Rectangle(200, 100); + $container->movePivot(Alignment::TOP_LEFT); + $input->movePivot(Alignment::TOP_LEFT); + $pos = $container->offsetTo($input); + $this->assertEquals(0, $pos->x()); + $this->assertEquals(0, $pos->y()); + + $container = new Rectangle(800, 600); + $input = new Rectangle(200, 100); + $container->movePivot(Alignment::CENTER); + $input->movePivot(Alignment::TOP_LEFT); + $pos = $container->offsetTo($input); + $this->assertEquals(400, $pos->x()); + $this->assertEquals(300, $pos->y()); + + $container = new Rectangle(800, 600); + $input = new Rectangle(200, 100); + $container->movePivot(Alignment::BOTTOM_RIGHT); + $input->movePivot(Alignment::TOP_RIGHT); + $pos = $container->offsetTo($input); + $this->assertEquals(600, $pos->x()); + $this->assertEquals(600, $pos->y()); + + $container = new Rectangle(800, 600); + $input = new Rectangle(200, 100); + $container->movePivot(Alignment::CENTER); + $input->movePivot(Alignment::CENTER); + $pos = $container->offsetTo($input); + $this->assertEquals(300, $pos->x()); + $this->assertEquals(250, $pos->y()); + + $container = new Rectangle(100, 200); + $input = new Rectangle(100, 100); + $container->movePivot(Alignment::CENTER); + $input->movePivot(Alignment::CENTER); + $pos = $container->offsetTo($input); + $this->assertEquals(0, $pos->x()); + $this->assertEquals(50, $pos->y()); + } + + public function testResize(): void + { + $rectangle = new Rectangle(800, 600); + $result = $rectangle->resize(300, 200); + $this->assertEquals(300, $result->width()); + $this->assertEquals(200, $result->height()); + } + + public function testResizeDown(): void + { + $rectangle = new Rectangle(800, 600); + $result = $rectangle->resizeDown(3000, 200); + $this->assertEquals(800, $result->width()); + $this->assertEquals(200, $result->height()); + } + + public function testScale(): void + { + $rectangle = new Rectangle(800, 600); + $result = $rectangle->scale(height: 1200); + $this->assertEquals(800 * 2, $result->width()); + $this->assertEquals(600 * 2, $result->height()); + } + + public function testScaleDown(): void + { + $rectangle = new Rectangle(800, 600); + $result = $rectangle->scaleDown(height: 1200); + $this->assertEquals(800, $result->width()); + $this->assertEquals(600, $result->height()); + } + + public function testCover(): void + { + $rectangle = new Rectangle(800, 600); + $result = $rectangle->cover(400, 100); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testContain(): void + { + $rectangle = new Rectangle(800, 600); + $result = $rectangle->contain(1600, 1200); + $this->assertEquals(1600, $result->width()); + $this->assertEquals(1200, $result->height()); + } + + public function testContainDown(): void + { + $rectangle = new Rectangle(800, 600); + $result = $rectangle->containDown(1600, 1200); + $this->assertEquals(800, $result->width()); + $this->assertEquals(600, $result->height()); + } + + public function testDebugInfo(): void + { + $info = (new Rectangle(800, 600))->__debugInfo(); + $this->assertEquals(800, $info['width']); + $this->assertEquals(600, $info['height']); + } +} diff --git a/tests/Unit/Geometry/ResizerTest.php b/tests/Unit/Geometry/ResizerTest.php new file mode 100644 index 000000000..cb7825b0d --- /dev/null +++ b/tests/Unit/Geometry/ResizerTest.php @@ -0,0 +1,133 @@ +assertInstanceOf(Resizer::class, $resizer); + + $resizer = Resizer::to(height: 100); + $this->assertInstanceOf(Resizer::class, $resizer); + + $resizer = Resizer::to(100); + $this->assertInstanceOf(Resizer::class, $resizer); + + $resizer = Resizer::to(100, 100); + $this->assertInstanceOf(Resizer::class, $resizer); + } + + public function testToWidth(): void + { + $resizer = new Resizer(); + $result = $resizer->toWidth(100); + $this->assertInstanceOf(Resizer::class, $result); + } + + public function testToHeight(): void + { + $resizer = new Resizer(); + $result = $resizer->toHeight(100); + $this->assertInstanceOf(Resizer::class, $result); + } + + public function testToSize(): void + { + $resizer = new Resizer(); + $resizer = $resizer->toSize(new Size(200, 100)); + $this->assertInstanceOf(Resizer::class, $resizer); + } + + /** + * @param $resizeParameters array + */ + #[DataProviderExternal(ResizeDataProvider::class, 'resizeDataProvider')] + public function testResize(Size $input, array $resizeParameters, Size $result): void + { + $resizer = new Resizer(...$resizeParameters); + $resized = $resizer->resize($input); + $this->assertEquals($result->width(), $resized->width()); + $this->assertEquals($result->height(), $resized->height()); + } + + /** + * @param $resizeParameters array + */ + #[DataProviderExternal(ResizeDataProvider::class, 'resizeDownDataProvider')] + public function testResizeDown(Size $input, array $resizeParameters, Size $result): void + { + $resizer = new Resizer(...$resizeParameters); + $resized = $resizer->resizeDown($input); + $this->assertEquals($result->width(), $resized->width()); + $this->assertEquals($result->height(), $resized->height()); + } + + /** + * @param $resizeParameters array + */ + #[DataProviderExternal(ResizeDataProvider::class, 'scaleDataProvider')] + public function testScale(Size $input, array $resizeParameters, Size $result): void + { + $resizer = new Resizer(...$resizeParameters); + $resized = $resizer->scale($input); + $this->assertEquals($result->width(), $resized->width()); + $this->assertEquals($result->height(), $resized->height()); + } + + /** + * @param $resizeParameters array + */ + #[DataProviderExternal(ResizeDataProvider::class, 'scaleDownDataProvider')] + public function testScaleDown(Size $input, array $resizeParameters, Size $result): void + { + $resizer = new Resizer(...$resizeParameters); + $resized = $resizer->scaleDown($input); + $this->assertEquals($result->width(), $resized->width()); + $this->assertEquals($result->height(), $resized->height()); + } + + #[DataProviderExternal(ResizeDataProvider::class, 'coverDataProvider')] + public function testCover(Size $origin, Size $target, Size $result): void + { + $resizer = new Resizer(); + $resizer->toSize($target); + $resized = $resizer->cover($origin); + $this->assertEquals($result->width(), $resized->width()); + $this->assertEquals($result->height(), $resized->height()); + } + + #[DataProviderExternal(ResizeDataProvider::class, 'containDataProvider')] + public function testContain(Size $origin, Size $target, Size $result): void + { + $resizer = new Resizer(); + $resizer->toSize($target); + $resized = $resizer->contain($origin); + $this->assertEquals($result->width(), $resized->width()); + $this->assertEquals($result->height(), $resized->height()); + } + + #[DataProviderExternal(ResizeDataProvider::class, 'cropDataProvider')] + public function testCrop(Size $origin, Size $target, string|Alignment $position, Size $result): void + { + $resizer = new Resizer(); + $resizer->toSize($target); + $resized = $resizer->crop($origin, $position); + $this->assertEquals($result->width(), $resized->width()); + $this->assertEquals($result->height(), $resized->height()); + $this->assertEquals($result->pivot()->x(), $resized->pivot()->x()); + $this->assertEquals($result->pivot()->y(), $resized->pivot()->y()); + } +} diff --git a/tests/Unit/Geometry/Tools/ResizerTest.php b/tests/Unit/Geometry/Tools/ResizerTest.php new file mode 100644 index 000000000..9ad10a101 --- /dev/null +++ b/tests/Unit/Geometry/Tools/ResizerTest.php @@ -0,0 +1,398 @@ +assertInstanceOf(Resizer::class, $resizer); + } + + public function testConstructorWidthOnly(): void + { + $resizer = new Resizer(100); + $this->assertInstanceOf(Resizer::class, $resizer); + } + + public function testConstructorHeightOnly(): void + { + $resizer = new Resizer(height: 200); + $this->assertInstanceOf(Resizer::class, $resizer); + } + + public function testConstructorNoArgs(): void + { + $resizer = new Resizer(); + $this->assertInstanceOf(Resizer::class, $resizer); + } + + public function testConstructorInvalidWidth(): void + { + $this->expectException(InvalidArgumentException::class); + new Resizer(0); + } + + public function testConstructorInvalidWidthNegative(): void + { + $this->expectException(InvalidArgumentException::class); + new Resizer(-1); + } + + public function testConstructorInvalidHeight(): void + { + $this->expectException(InvalidArgumentException::class); + new Resizer(height: 0); + } + + public function testConstructorInvalidHeightNegative(): void + { + $this->expectException(InvalidArgumentException::class); + new Resizer(height: -5); + } + + public function testStaticTo(): void + { + $resizer = Resizer::to(100, 200); + $this->assertInstanceOf(Resizer::class, $resizer); + } + + public function testToWidth(): void + { + $resizer = new Resizer(); + $result = $resizer->toWidth(300); + $this->assertSame($resizer, $result); + } + + public function testToHeight(): void + { + $resizer = new Resizer(); + $result = $resizer->toHeight(400); + $this->assertSame($resizer, $result); + } + + public function testToSize(): void + { + $resizer = new Resizer(); + $size = new Size(500, 300); + $result = $resizer->toSize($size); + $this->assertSame($resizer, $result); + } + + public function testResizeWithBothDimensions(): void + { + $resizer = new Resizer(200, 100); + $original = new Size(800, 600); + $result = $resizer->resize($original); + $this->assertEquals(200, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testResizeWidthOnly(): void + { + $resizer = new Resizer(200); + $original = new Size(800, 600); + $result = $resizer->resize($original); + $this->assertEquals(200, $result->width()); + $this->assertEquals(600, $result->height()); + } + + public function testResizeHeightOnly(): void + { + $resizer = new Resizer(height: 100); + $original = new Size(800, 600); + $result = $resizer->resize($original); + $this->assertEquals(800, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testResizeNoTarget(): void + { + $resizer = new Resizer(); + $original = new Size(800, 600); + $result = $resizer->resize($original); + $this->assertEquals(800, $result->width()); + $this->assertEquals(600, $result->height()); + } + + public function testResizeDown(): void + { + $resizer = new Resizer(200, 100); + $original = new Size(800, 600); + $result = $resizer->resizeDown($original); + $this->assertEquals(200, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testResizeDownDoesNotUpscale(): void + { + $resizer = new Resizer(1000, 800); + $original = new Size(400, 300); + $result = $resizer->resizeDown($original); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testResizeDownWidthOnly(): void + { + $resizer = new Resizer(200); + $original = new Size(800, 600); + $result = $resizer->resizeDown($original); + $this->assertEquals(200, $result->width()); + $this->assertEquals(600, $result->height()); + } + + public function testResizeDownHeightOnly(): void + { + $resizer = new Resizer(height: 100); + $original = new Size(800, 600); + $result = $resizer->resizeDown($original); + $this->assertEquals(800, $result->width()); + $this->assertEquals(100, $result->height()); + } + + public function testScaleWithBothDimensions(): void + { + $resizer = new Resizer(400, 300); + $original = new Size(800, 600); + $result = $resizer->scale($original); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleWidthOnly(): void + { + $resizer = new Resizer(400); + $original = new Size(800, 600); + $result = $resizer->scale($original); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleHeightOnly(): void + { + $resizer = new Resizer(height: 300); + $original = new Size(800, 600); + $result = $resizer->scale($original); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleNoTarget(): void + { + $resizer = new Resizer(); + $original = new Size(800, 600); + $result = $resizer->scale($original); + $this->assertEquals(800, $result->width()); + $this->assertEquals(600, $result->height()); + } + + public function testScaleDownWithBothDimensions(): void + { + $resizer = new Resizer(400, 300); + $original = new Size(800, 600); + $result = $resizer->scaleDown($original); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleDownDoesNotUpscale(): void + { + $resizer = new Resizer(1000, 800); + $original = new Size(400, 300); + $result = $resizer->scaleDown($original); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleDownWidthOnly(): void + { + $resizer = new Resizer(200); + $original = new Size(800, 600); + $result = $resizer->scaleDown($original); + $this->assertEquals(200, $result->width()); + $this->assertEquals(150, $result->height()); + } + + public function testScaleDownWidthOnlyDoesNotUpscale(): void + { + $resizer = new Resizer(1000); + $original = new Size(400, 300); + $result = $resizer->scaleDown($original); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleDownHeightOnly(): void + { + $resizer = new Resizer(height: 150); + $original = new Size(800, 600); + $result = $resizer->scaleDown($original); + $this->assertEquals(200, $result->width()); + $this->assertEquals(150, $result->height()); + } + + public function testScaleDownHeightOnlyDoesNotUpscale(): void + { + $resizer = new Resizer(height: 1000); + $original = new Size(400, 300); + $result = $resizer->scaleDown($original); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleDownNoTarget(): void + { + $resizer = new Resizer(); + $original = new Size(800, 600); + $result = $resizer->scaleDown($original); + $this->assertEquals(800, $result->width()); + $this->assertEquals(600, $result->height()); + } + + public function testCover(): void + { + $resizer = new Resizer(200, 200); + $original = new Size(800, 600); + $result = $resizer->cover($original); + // cover should scale up so the image covers the target + $this->assertGreaterThanOrEqual(200, $result->width()); + $this->assertGreaterThanOrEqual(200, $result->height()); + } + + public function testCoverLandscapeIntoPortrait(): void + { + $resizer = new Resizer(100, 200); + $original = new Size(800, 600); + $result = $resizer->cover($original); + $this->assertGreaterThanOrEqual(100, $result->width()); + $this->assertGreaterThanOrEqual(200, $result->height()); + } + + public function testContain(): void + { + $resizer = new Resizer(200, 200); + $original = new Size(800, 600); + $result = $resizer->contain($original); + // contain should fit into target + $this->assertLessThanOrEqual(200, $result->width()); + $this->assertLessThanOrEqual(200, $result->height()); + } + + public function testContainLandscape(): void + { + $resizer = new Resizer(400, 200); + $original = new Size(800, 600); + $result = $resizer->contain($original); + $this->assertLessThanOrEqual(400, $result->width()); + $this->assertLessThanOrEqual(200, $result->height()); + } + + public function testContainDown(): void + { + $resizer = new Resizer(200, 200); + $original = new Size(800, 600); + $result = $resizer->containDown($original); + $this->assertLessThanOrEqual(200, $result->width()); + $this->assertLessThanOrEqual(200, $result->height()); + } + + public function testContainDownDoesNotUpscale(): void + { + $resizer = new Resizer(1000, 1000); + $original = new Size(400, 300); + $result = $resizer->containDown($original); + $this->assertLessThanOrEqual(400, $result->width()); + $this->assertLessThanOrEqual(300, $result->height()); + } + + public function testContainDownTallTarget(): void + { + // Force the branch where auto-height doesn't fit into target + $resizer = new Resizer(400, 100); + $original = new Size(800, 600); + $result = $resizer->containDown($original); + $this->assertLessThanOrEqual(400, $result->width()); + $this->assertLessThanOrEqual(100, $result->height()); + } + + public function testCrop(): void + { + $resizer = new Resizer(200, 200); + $original = new Size(800, 600); + $result = $resizer->crop($original); + $this->assertEquals(200, $result->width()); + $this->assertEquals(200, $result->height()); + } + + public function testCropWithAlignment(): void + { + $resizer = new Resizer(200, 200); + $original = new Size(800, 600); + $result = $resizer->crop($original, Alignment::CENTER); + $this->assertEquals(200, $result->width()); + $this->assertEquals(200, $result->height()); + } + + public function testCropWithStringAlignment(): void + { + $resizer = new Resizer(200, 200); + $original = new Size(800, 600); + $result = $resizer->crop($original, 'bottom-right'); + $this->assertEquals(200, $result->width()); + $this->assertEquals(200, $result->height()); + } + + public function testTargetSizeThrowsWithoutBothDimensions(): void + { + // cover() calls targetSize() internally, so calling cover without both dimensions throws + $resizer = new Resizer(200); + $original = new Size(800, 600); + $this->expectException(StateException::class); + $resizer->cover($original); + } + + public function testScaleNonProportional(): void + { + // Test where both target dimensions constrain, and proportional width differs from target width + $resizer = new Resizer(100, 200); + $original = new Size(800, 400); + $result = $resizer->scale($original); + // aspect ratio is 2:1, target is 100x200 + // proportionalWidth = 200 * 2 = 400, min(400, 100) = 100 + // proportionalHeight = 100 / 2 = 50, min(50, 200) = 50 + $this->assertEquals(100, $result->width()); + $this->assertEquals(50, $result->height()); + } + + public function testResizeDownNoTarget(): void + { + $resizer = new Resizer(); + $original = new Size(800, 600); + $result = $resizer->resizeDown($original); + $this->assertEquals(800, $result->width()); + $this->assertEquals(600, $result->height()); + } + + public function testScaleDownBothConstrainedSmaller(): void + { + // Both dimensions given, but original is already smaller — scaleDown should not upscale + $resizer = new Resizer(500, 500); + $original = new Size(200, 100); + $result = $resizer->scaleDown($original); + $this->assertEquals(200, $result->width()); + $this->assertEquals(100, $result->height()); + } +} diff --git a/tests/Unit/Geometry/Traits/HasBackgroundColorTest.php b/tests/Unit/Geometry/Traits/HasBackgroundColorTest.php new file mode 100644 index 000000000..71210fc77 --- /dev/null +++ b/tests/Unit/Geometry/Traits/HasBackgroundColorTest.php @@ -0,0 +1,45 @@ +getTestObject(); + $this->assertNull($object->backgroundColor()); + $this->assertFalse($object->hasBackgroundColor()); + $object->setBackgroundColor('fff'); + $this->assertEquals('fff', $object->backgroundColor()); + $this->assertTrue($object->hasBackgroundColor()); + } + + public function testSetBackgroundColorWithColorInterface(): void + { + $object = $this->getTestObject(); + $color = new RgbColor(255, 0, 0); + $object->setBackgroundColor($color); + $this->assertSame($color, $object->backgroundColor()); + $this->assertTrue($object->hasBackgroundColor()); + } + + public function testSetBackgroundColorReturnsSelf(): void + { + $object = $this->getTestObject(); + $result = $object->setBackgroundColor('fff'); + $this->assertSame($object, $result); + } +} diff --git a/tests/Unit/Geometry/Traits/HasBorderTest.php b/tests/Unit/Geometry/Traits/HasBorderTest.php new file mode 100644 index 000000000..4c5a8d1ef --- /dev/null +++ b/tests/Unit/Geometry/Traits/HasBorderTest.php @@ -0,0 +1,109 @@ +getTestObject(); + $this->assertNull($object->borderColor()); + $this->assertEquals(0, $object->borderSize()); + $this->assertFalse($object->hasBorder()); + $object->setBorder('fff', 10); + $this->assertEquals('fff', $object->borderColor()); + $this->assertEquals(10, $object->borderSize()); + $this->assertTrue($object->hasBorder()); + } + + public function testSetBorderSize(): void + { + $object = $this->getTestObject(); + $this->assertEquals(0, $object->borderSize()); + $object->setBorderSize(10); + $this->assertEquals(10, $object->borderSize()); + } + + public function testSetBorderSizeNegative(): void + { + $object = $this->getTestObject(); + $this->expectException(InvalidArgumentException::class); + $object->setBorderSize(-1); + } + + public function testSetBorderSizeZero(): void + { + $object = $this->getTestObject(); + $object->setBorderSize(0); + $this->assertEquals(0, $object->borderSize()); + } + + public function testSetBorderColor(): void + { + $object = $this->getTestObject(); + $this->assertNull($object->borderColor()); + $object->setBorderColor('fff'); + $this->assertEquals('fff', $object->borderColor()); + $this->assertFalse($object->hasBorder()); + } + + public function testSetBorderColorWithColorInterface(): void + { + $object = $this->getTestObject(); + $color = new RgbColor(255, 0, 0); + $object->setBorderColor($color); + $this->assertSame($color, $object->borderColor()); + } + + public function testHasBorder(): void + { + $object = $this->getTestObject(); + $this->assertFalse($object->hasBorder()); + $object->setBorderColor('fff'); + $this->assertFalse($object->hasBorder()); + $object->setBorderSize(1); + $this->assertTrue($object->hasBorder()); + } + + public function testHasBorderWithSizeButNoColor(): void + { + $object = $this->getTestObject(); + $object->setBorderSize(5); + $this->assertFalse($object->hasBorder()); + } + + public function testSetBorderReturnsSelf(): void + { + $object = $this->getTestObject(); + $result = $object->setBorder('fff', 1); + $this->assertSame($object, $result); + } + + public function testSetBorderSizeReturnsSelf(): void + { + $object = $this->getTestObject(); + $result = $object->setBorderSize(1); + $this->assertSame($object, $result); + } + + public function testSetBorderColorReturnsSelf(): void + { + $object = $this->getTestObject(); + $result = $object->setBorderColor('fff'); + $this->assertSame($object, $result); + } +} diff --git a/tests/Unit/ImageManagerTest.php b/tests/Unit/ImageManagerTest.php new file mode 100644 index 000000000..f4211396b --- /dev/null +++ b/tests/Unit/ImageManagerTest.php @@ -0,0 +1,194 @@ +assertInstanceOf(ImageManagerInterface::class, $manager); + $this->assertInstanceOf(DriverInterface::class, $manager->driver); + } + + #[DataProviderExternal(DriverProvider::class, 'driverClassnames')] + public function testConstructorString(string $driver): void + { + $manager = new ImageManager($driver); + $this->assertInstanceOf(ImageManagerInterface::class, $manager); + $this->assertInstanceOf(DriverInterface::class, $manager->driver); + } + + public function testConstructorUnkownClass(): void + { + $this->expectException(InvalidArgumentException::class); + new ImageManager('foobar'); + } + + public function testConstructorNonDriverClass(): void + { + $this->expectException(InvalidArgumentException::class); + new ImageManager(DataUri::class); + } + + #[DataProviderExternal(DriverProvider::class, 'drivers')] + #[DataProviderExternal(DriverProvider::class, 'driverClassnames')] + public function testConstructorWithOptions(string|DriverInterface $driver): void + { + $manager = new ImageManager($driver, backgroundColor: 'ff5500'); + $this->assertInstanceOf(ImageManagerInterface::class, $manager); + $this->assertInstanceOf(DriverInterface::class, $manager->driver); + $this->assertEquals('ff5500', $manager->driver->config()->backgroundColor); + } + + #[DataProviderExternal(DriverProvider::class, 'drivers')] + #[DataProviderExternal(DriverProvider::class, 'driverClassnames')] + public function testUsingDriver(string|DriverInterface $driver): void + { + $manager = ImageManager::usingDriver($driver); + $this->assertInstanceOf(ImageManagerInterface::class, $manager); + $this->assertInstanceOf(DriverInterface::class, $manager->driver); + } + + #[DataProviderExternal(DriverProvider::class, 'driverClassnames')] + public function testUsingDriverOptions(string $driverClassname): void + { + $manager = ImageManager::usingDriver(new $driverClassname(new Config(strip: true)), strip: false); + $this->assertInstanceOf(ImageManagerInterface::class, $manager); + $this->assertInstanceOf(DriverInterface::class, $manager->driver); + $this->assertFalse($manager->driver->config()->strip); + } + + #[DataProviderExternal(DriverProvider::class, 'drivers')] + #[DataProviderExternal(DriverProvider::class, 'driverClassnames')] + public function testCreateImage(string|DriverInterface $driver): void + { + $manager = new ImageManager($driver); + $image = $manager->createImage(3, 2); + $this->assertEquals(3, $image->width()); + $this->assertEquals(2, $image->height()); + $this->assertColor(255, 255, 255, 0, $image->colorAt(0, 0)); + } + + #[DataProviderExternal(DriverProvider::class, 'drivers')] + #[DataProviderExternal(DriverProvider::class, 'driverClassnames')] + public function testCreateImageAnimated(string|DriverInterface $driver): void + { + $manager = new ImageManager($driver); + $image = $manager->createImage(3, 2, function (AnimationFactoryInterface $animation): void { + $animation->add('f00'); + $animation->add('0f0'); + $animation->add('00f'); + }); + $this->assertEquals(3, $image->width()); + $this->assertEquals(2, $image->height()); + $this->assertEquals(3, $image->count()); + $this->assertEquals( + ['ff0000', '00ff00', '0000ff'], + $image->colorsAt(1, 1)->map(fn(ColorInterface $color): string => $color->toHex())->toArray(), + ); + } + + #[DataProviderExternal(DriverProvider::class, 'drivers')] + #[DataProviderExternal(DriverProvider::class, 'driverClassnames')] + public function testCreateImageWithAnimationFactory(string|DriverInterface $driver): void + { + $manager = new ImageManager($driver); + $factory = new AnimationFactory(3, 2, function (AnimationFactoryInterface $animation): void { + $animation->add(Resource::create('red.gif')->path()); + }); + $image = $manager->createImage(3, 2, $factory); + $this->assertInstanceOf(ImageInterface::class, $image); + $this->assertEquals(3, $image->width()); + $this->assertEquals(2, $image->height()); + } + + #[DataProviderExternal(ImageSourceProvider::class, 'filePaths')] + #[DataProviderExternal(ImageSourceProvider::class, 'binaryData')] + #[DataProviderExternal(ImageSourceProvider::class, 'splFileInfoObjects')] + #[DataProviderExternal(ImageSourceProvider::class, 'base64Data')] + public function testDecode(mixed $source): void + { + $this->assertInstanceOf( + ImageInterface::class, + ImageManager::usingDriver(Driver::class)->decode($source), + ); + } + + #[DataProviderExternal(ImageSourceProvider::class, 'filePaths')] + public function testDecodePath(string $path): void + { + $this->assertInstanceOf( + ImageInterface::class, + ImageManager::usingDriver(Driver::class)->decodePath($path), + ); + } + + #[DataProviderExternal(ImageSourceProvider::class, 'binaryData')] + public function testDecodeBinary(string $binary): void + { + $this->assertInstanceOf( + ImageInterface::class, + ImageManager::usingDriver(Driver::class)->decodeBinary($binary), + ); + } + + #[DataProviderExternal(ImageSourceProvider::class, 'splFileInfoObjects')] + public function testDecodeSplFileInfo(SplFileInfo $splFileInfo): void + { + $this->assertInstanceOf( + ImageInterface::class, + ImageManager::usingDriver(Driver::class)->decodeSplFileInfo($splFileInfo), + ); + } + + #[DataProviderExternal(ImageSourceProvider::class, 'base64Data')] + public function testDecodeBase64(string $base64Data): void + { + $this->assertInstanceOf( + ImageInterface::class, + ImageManager::usingDriver(Driver::class)->decodeBase64($base64Data), + ); + } + + #[DataProviderExternal(ImageSourceProvider::class, 'dataUriStrings')] + #[DataProviderExternal(ImageSourceProvider::class, 'dataUriObjects')] + public function testDecodeDataUri(string|DataUriInterface $datauri): void + { + $this->assertInstanceOf( + ImageInterface::class, + ImageManager::usingDriver(Driver::class)->decodeDataUri($datauri), + ); + } + + #[DataProviderExternal(ImageSourceProvider::class, 'streams')] + public function testDecodeStream(mixed $stream): void + { + $this->assertInstanceOf( + ImageInterface::class, + ImageManager::usingDriver(Driver::class)->decodeStream($stream), + ); + } +} diff --git a/tests/Unit/ImageTest.php b/tests/Unit/ImageTest.php new file mode 100644 index 000000000..cb37e1dc4 --- /dev/null +++ b/tests/Unit/ImageTest.php @@ -0,0 +1,747 @@ +createImage($width, $height); + } + + public function testConstructor(): void + { + $image = $this->createImage(); + $this->assertInstanceOf(Image::class, $image); + } + + public function testDriver(): void + { + $image = $this->createImage(); + $this->assertInstanceOf(GdDriver::class, $image->driver()); + } + + public function testCore(): void + { + $image = $this->createImage(); + $this->assertNotNull($image->core()); + } + + public function testOrigin(): void + { + $image = $this->createImage(); + $this->assertInstanceOf(Origin::class, $image->origin()); + } + + public function testSetOrigin(): void + { + $image = $this->createImage(); + $origin = new Origin(); + $origin->setFilePath('/tmp/test.jpg'); + $result = $image->setOrigin($origin); + $this->assertSame($image, $result); + $this->assertSame($origin, $image->origin()); + $this->assertEquals('/tmp/test.jpg', $image->origin()->filePath()); + } + + public function testCount(): void + { + $image = $this->createImage(); + $this->assertEquals(1, $image->count()); + } + + public function testGetIterator(): void + { + $image = $this->createImage(); + $count = 0; + foreach ($image as $frame) { + $this->assertInstanceOf(\Intervention\Image\Interfaces\FrameInterface::class, $frame); + $count++; + } + $this->assertEquals(1, $count); + } + + public function testIsAnimated(): void + { + $image = $this->createImage(); + $this->assertFalse($image->isAnimated()); + } + + public function testLoops(): void + { + $image = $this->createImage(); + $this->assertEquals(0, $image->loops()); + } + + public function testSetLoops(): void + { + $image = $this->createImage(); + $result = $image->setLoops(5); + $this->assertSame($image, $result); + $this->assertEquals(5, $image->loops()); + } + + public function testExif(): void + { + $image = $this->createImage(); + $exif = $image->exif(); + $this->assertInstanceOf(\Intervention\Image\Interfaces\CollectionInterface::class, $exif); + } + + public function testExifWithQuery(): void + { + $image = $this->createImage(); + $this->assertNull($image->exif('nonexistent')); + } + + public function testSetExif(): void + { + $image = $this->createImage(); + $exif = new Collection(['foo' => 'bar']); + $result = $image->setExif($exif); + $this->assertSame($image, $result); + $this->assertEquals('bar', $image->exif('foo')); + } + + public function testWidth(): void + { + $image = $this->createImage(200, 100); + $this->assertEquals(200, $image->width()); + } + + public function testHeight(): void + { + $image = $this->createImage(200, 100); + $this->assertEquals(100, $image->height()); + } + + public function testSize(): void + { + $image = $this->createImage(200, 100); + $size = $image->size(); + $this->assertInstanceOf(SizeInterface::class, $size); + $this->assertEquals(200, $size->width()); + $this->assertEquals(100, $size->height()); + } + + public function testResize(): void + { + $image = $this->createImage(100, 100); + $result = $image->resize(50, 50); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + } + + public function testResizeWithFraction(): void + { + $image = $this->createImage(100, 100); + $result = $image->resize(Fraction::HALF, null); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(50, $image->width()); + } + + public function testResizeDown(): void + { + $image = $this->createImage(100, 100); + $result = $image->resizeDown(50, 50); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + } + + public function testScale(): void + { + $image = $this->createImage(100, 100); + $result = $image->scale(width: 50); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + } + + public function testScaleDown(): void + { + $image = $this->createImage(100, 100); + $result = $image->scaleDown(width: 50); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + } + + public function testCover(): void + { + $image = $this->createImage(100, 100); + $result = $image->cover(50, 50); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + } + + public function testCoverDown(): void + { + $image = $this->createImage(100, 100); + $result = $image->coverDown(50, 50); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + } + + public function testCrop(): void + { + $image = $this->createImage(100, 100); + $result = $image->crop(50, 50); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + } + + public function testContainDown(): void + { + $image = $this->createImage(50, 50); + $result = $image->containDown(100, 100, 'ffffff'); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(100, $image->width()); + $this->assertEquals(100, $image->height()); + } + + public function testContain(): void + { + $image = $this->createImage(100, 50); + $result = $image->contain(50, 50, 'ffffff'); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testResizeCanvas(): void + { + $image = $this->createImage(100, 100); + $result = $image->resizeCanvas(150, 150, 'ffffff'); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(150, $image->width()); + $this->assertEquals(150, $image->height()); + } + + public function testResizeCanvasRelative(): void + { + $image = $this->createImage(100, 100); + $result = $image->resizeCanvasRelative(50, 50, 'ffffff'); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals(150, $image->width()); + $this->assertEquals(150, $image->height()); + } + + public function testFlip(): void + { + $image = $this->createImage(); + $result = $image->flip(); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testRotate(): void + { + $image = $this->createImage(); + $result = $image->rotate(90); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testGamma(): void + { + $image = $this->createImage(); + $result = $image->gamma(1.5); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testBrightness(): void + { + $image = $this->createImage(); + $result = $image->brightness(10); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testContrast(): void + { + $image = $this->createImage(); + $result = $image->contrast(10); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testColorize(): void + { + $image = $this->createImage(); + $result = $image->colorize(10, 0, 0); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testSharpen(): void + { + $image = $this->createImage(); + $result = $image->sharpen(10); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testBlur(): void + { + $image = $this->createImage(); + $result = $image->blur(5); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testInvert(): void + { + $image = $this->createImage(); + $result = $image->invert(); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testPixelate(): void + { + $image = $this->createImage(); + $result = $image->pixelate(10); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testGrayscale(): void + { + $image = $this->createImage(); + $result = $image->grayscale(); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testOrient(): void + { + $image = $this->createImage(); + $result = $image->orient(); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testFill(): void + { + $image = $this->createImage(); + $result = $image->fill('ff0000'); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testFillWithPosition(): void + { + $image = $this->createImage(); + $result = $image->fill('ff0000', 0, 0); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawPixel(): void + { + $image = $this->createImage(); + $result = $image->drawPixel(10, 10, 'ff0000'); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawRectangle(): void + { + $image = $this->createImage(); + $result = $image->drawRectangle(function ($rectangle): void { + $rectangle->size(50, 50); + $rectangle->at(10, 10); + $rectangle->background('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawRectangleWithObject(): void + { + $image = $this->createImage(); + $rectangle = new Rectangle(50, 50, new Point(10, 10)); + $rectangle->setBackgroundColor('ff0000'); + $result = $image->drawRectangle($rectangle); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawEllipse(): void + { + $image = $this->createImage(); + $result = $image->drawEllipse(function ($ellipse): void { + $ellipse->size(50, 30); + $ellipse->at(50, 50); + $ellipse->background('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawCircle(): void + { + $image = $this->createImage(); + $result = $image->drawCircle(function ($circle): void { + $circle->radius(25); + $circle->at(50, 50); + $circle->background('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawLine(): void + { + $image = $this->createImage(); + $result = $image->drawLine(function ($line): void { + $line->from(0, 0); + $line->to(100, 100); + $line->color('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawPolygon(): void + { + $image = $this->createImage(); + $result = $image->drawPolygon(function ($polygon): void { + $polygon->point(10, 10); + $polygon->point(90, 10); + $polygon->point(50, 90); + $polygon->background('ff0000'); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawBezier(): void + { + $image = $this->createImage(); + $bezier = new Bezier([ + new Point(10, 10), + new Point(50, 50), + new Point(90, 10), + ]); + $result = $image->drawBezier($bezier); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithRectangle(): void + { + $image = $this->createImage(); + $rectangle = new Rectangle(50, 50, new Point(10, 10)); + $rectangle->setBackgroundColor('ff0000'); + $result = $image->draw($rectangle); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithEllipse(): void + { + $image = $this->createImage(); + $ellipse = new Ellipse(50, 30, new Point(50, 50)); + $ellipse->setBackgroundColor('ff0000'); + $result = $image->draw($ellipse); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithCircle(): void + { + $image = $this->createImage(); + $circle = new Circle(50, new Point(50, 50)); + $circle->setBackgroundColor('ff0000'); + $result = $image->draw($circle); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithLine(): void + { + $image = $this->createImage(); + $line = new Line(new Point(0, 0), new Point(100, 100)); + $result = $image->draw($line); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithPolygon(): void + { + $image = $this->createImage(); + $polygon = new Polygon([ + new Point(10, 10), + new Point(90, 10), + new Point(50, 90), + ]); + $polygon->setBackgroundColor('ff0000'); + $result = $image->draw($polygon); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDrawWithBezier(): void + { + $image = $this->createImage(); + $bezier = new Bezier([ + new Point(10, 10), + new Point(50, 50), + new Point(90, 10), + ]); + $result = $image->draw($bezier); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testColorAt(): void + { + $image = $this->createImage(); + $color = $image->colorAt(0, 0); + $this->assertInstanceOf(ColorInterface::class, $color); + } + + public function testRemoveAnimation(): void + { + $image = $this->createImage(); + $result = $image->removeAnimation(); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testTrim(): void + { + $image = $this->createImage(); + $result = $image->trim(0); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testReduceColors(): void + { + $image = $this->createImage(); + $result = $image->reduceColors(16); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testInsert(): void + { + $image = $this->createImage(); + $other = $this->createImage(50, 50); + $result = $image->insert($other); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testBackgroundColor(): void + { + $image = $this->createImage(); + $color = $image->backgroundColor(); + $this->assertInstanceOf(ColorInterface::class, $color); + } + + public function testSetBackgroundColor(): void + { + $image = $this->createImage(); + $result = $image->setBackgroundColor('ff0000'); + $this->assertSame($image, $result); + } + + public function testEncode(): void + { + $image = $this->createImage(); + $encoded = $image->encode(new \Intervention\Image\Encoders\PngEncoder()); + $this->assertNotEmpty($encoded->toString()); + } + + public function testEncodeUsingFormat(): void + { + $image = $this->createImage(); + $encoded = $image->encodeUsingFormat(\Intervention\Image\Format::PNG); + $this->assertNotEmpty($encoded->toString()); + } + + public function testEncodeUsingMediaType(): void + { + $image = $this->createImage(); + $encoded = $image->encodeUsingMediaType('image/png'); + $this->assertNotEmpty($encoded->toString()); + } + + public function testEncodeUsingFileExtension(): void + { + $image = $this->createImage(); + $encoded = $image->encodeUsingFileExtension('png'); + $this->assertNotEmpty($encoded->toString()); + } + + public function testEncodeUsingPath(): void + { + $image = $this->createImage(); + $encoded = $image->encodeUsingPath('/tmp/test.png'); + $this->assertNotEmpty($encoded->toString()); + } + + public function testSave(): void + { + $image = $this->createImage(); + $path = sys_get_temp_dir() . '/intervention_test_' . hrtime(true) . '.png'; + + try { + $result = $image->save($path); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertFileExists($path); + } finally { + if (is_file($path)) { + unlink($path); + } + } + } + + public function testSaveWithEmptyString(): void + { + $image = $this->createImage(); + $this->expectException(InvalidArgumentException::class); + $image->save(''); + } + + public function testSaveWithoutPathAndNoOrigin(): void + { + $image = $this->createImage(); + $this->expectException(EncoderException::class); + $image->save(); + } + + public function testSaveWithOriginPath(): void + { + $image = $this->createImage(); + $path = sys_get_temp_dir() . '/intervention_test_' . hrtime(true) . '.png'; + $origin = new Origin(); + $origin->setFilePath($path); + $image->setOrigin($origin); + + try { + $result = $image->save(); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertFileExists($path); + } finally { + if (is_file($path)) { + unlink($path); + } + } + } + + public function testFillTransparentAreas(): void + { + $image = $this->createImage(); + $result = $image->fillTransparentAreas('ffffff'); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testDebugInfo(): void + { + $image = $this->createImage(200, 100); + $info = $image->__debugInfo(); + $this->assertEquals(['width' => 200, 'height' => 100], $info); + } + + public function testClone(): void + { + $image = $this->createImage(100, 100); + $clone = clone $image; + + // They should be separate instances + $this->assertNotSame($image->driver(), $clone->driver()); + $this->assertNotSame($image->core(), $clone->core()); + + // But same dimensions + $this->assertEquals($image->width(), $clone->width()); + $this->assertEquals($image->height(), $clone->height()); + + // Modifying clone should not affect original + $clone->resize(50, 50); + $this->assertEquals(100, $image->width()); + $this->assertEquals(50, $clone->width()); + } + + public function testSliceAnimation(): void + { + $image = $this->createImage(); + $result = $image->sliceAnimation(0); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testSetResolution(): void + { + $image = $this->createImage(); + $result = $image->setResolution(150, 150); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testColorspace(): void + { + $image = $this->createImage(); + $colorspace = $image->colorspace(); + $this->assertInstanceOf(\Intervention\Image\Interfaces\ColorspaceInterface::class, $colorspace); + } + + public function testResolution(): void + { + $image = $this->createImage(); + $resolution = $image->resolution(); + $this->assertInstanceOf(\Intervention\Image\Interfaces\ResolutionInterface::class, $resolution); + } + + public function testSetProfile(): void + { + $image = $this->createImage(); + $this->expectException(NotSupportedException::class); + $image->setProfile(new Profile(fopen('php://temp', 'r'))); + } + + public function testRemoveProfile(): void + { + $image = $this->createImage(); + $result = $image->removeProfile(); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testSetColorspace(): void + { + $image = $this->createImage(); + $result = $image->setColorspace(RgbColorspace::class); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testSetColorspaceWithObject(): void + { + $image = $this->createImage(); + $result = $image->setColorspace(new RgbColorspace()); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testColorsAt(): void + { + $image = $this->createImage(); + $result = $image->colorsAt(0, 0); + $this->assertInstanceOf(CollectionInterface::class, $result); + } + + public function testProfile(): void + { + $image = $this->createImage(); + $this->expectException(NotSupportedException::class); + $image->profile(); + } + + public function testText(): void + { + $image = $this->createImage(); + $result = $image->text('Hello', 10, 10, function ($font): void { + $font->size(20); + }); + $this->assertInstanceOf(ImageInterface::class, $result); + } +} diff --git a/tests/Unit/InputHandlerTest.php b/tests/Unit/InputHandlerTest.php new file mode 100644 index 000000000..04f9df1a2 --- /dev/null +++ b/tests/Unit/InputHandlerTest.php @@ -0,0 +1,137 @@ + $decoders + */ + #[DataProvider('handleProvider')] + public function testHandleDefaultDecoders( + string $driver, + array $decoders, + mixed $input, + string $outputClassname, + ): void { + $handler = new InputHandler(decoders: $decoders, driver: new $driver()); + if ($outputClassname === ImageInterface::class || $outputClassname === ColorInterface::class) { + $this->assertInstanceOf($outputClassname, $handler->handle($input)); + } else { + $this->expectException($outputClassname); + $handler->handle($input); + } + } + + public static function handleProvider(): Generator + { + $base = [ + [InputHandler::COLOR_DECODERS, null, InvalidArgumentException::class], + [InputHandler::COLOR_DECODERS, '', InvalidArgumentException::class], + [InputHandler::COLOR_DECODERS, 'fff', ColorInterface::class], + [InputHandler::COLOR_DECODERS, 'rgba(0, 0, 0, 0)', ColorInterface::class], + [InputHandler::COLOR_DECODERS, 'cmyk(0, 0, 0, 0)', ColorInterface::class], + [InputHandler::COLOR_DECODERS, 'hsv(0, 0, 0)', ColorInterface::class], + [InputHandler::COLOR_DECODERS, 'hsl(0, 0, 0)', ColorInterface::class], + [InputHandler::COLOR_DECODERS, 'steelblue', ColorInterface::class], + [InputHandler::IMAGE_DECODERS, Resource::create()->path(), ImageInterface::class], + [InputHandler::IMAGE_DECODERS, Resource::create()->data(), ImageInterface::class], + ]; + + $drivers = [GdDriver::class, ImagickDriver::class]; + foreach ($drivers as $driver) { + foreach ($base as $line) { + array_unshift($line, $driver); // prepend driver + yield $line; + } + } + } + + public function testResolveWithoutDriver(): void + { + $handler = new InputHandler([new HexColorDecoder()]); + $result = $handler->handle('fff'); + $this->assertInstanceOf(ColorInterface::class, $result); + + $handler = new InputHandler([HexColorDecoder::class]); + $result = $handler->handle('fff'); + $this->assertInstanceOf(ColorInterface::class, $result); + } + + public function testUsingDecodersStaticFactory(): void + { + $handler = InputHandler::usingDecoders([HexColorDecoder::class]); + $result = $handler->handle('fff'); + $this->assertInstanceOf(ColorInterface::class, $result); + } + + public function testUsingDecodersStaticFactoryWithDriver(): void + { + $handler = InputHandler::usingDecoders([HexColorDecoder::class], new GdDriver()); + $result = $handler->handle('fff'); + $this->assertInstanceOf(ColorInterface::class, $result); + } + + public function testInvalidDecoderClass(): void + { + $this->expectException(InvalidArgumentException::class); + $handler = new InputHandler([self::class]); + $handler->handle('fff'); + } + + public function testHandleNull(): void + { + $handler = new InputHandler([HexColorDecoder::class]); + $this->expectException(InvalidArgumentException::class); + $handler->handle(null); + } + + public function testHandleEmptyString(): void + { + $handler = new InputHandler([HexColorDecoder::class]); + $this->expectException(InvalidArgumentException::class); + $handler->handle(''); + } + + public function testHandleUnsupportedInput(): void + { + $handler = new InputHandler(InputHandler::COLOR_DECODERS, new GdDriver()); + $this->expectException(\Intervention\Image\Exceptions\NotSupportedException::class); + $handler->handle(new \stdClass()); + } + + public function testSpecializeDecoderThrowsDriverException(): void + { + $driver = Mockery::mock(DriverInterface::class); + $driver->shouldReceive('specializeDecoder') + ->andThrow(new NotSupportedException('Not supported')); + + $handler = new InputHandler([FilePathImageDecoder::class], $driver); + $this->expectException(DriverException::class); + $handler->handle('/some/path'); + } +} diff --git a/tests/Unit/MediaTypeTest.php b/tests/Unit/MediaTypeTest.php new file mode 100644 index 000000000..3069b0553 --- /dev/null +++ b/tests/Unit/MediaTypeTest.php @@ -0,0 +1,222 @@ +assertEquals(MediaType::IMAGE_JPEG, MediaType::create(MediaType::IMAGE_JPEG)); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::create(Format::JPEG)); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::create(FileExtension::JPG)); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::create('jpg')); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::create('jpeg')); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::create('image/jpeg')); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::create('JPG')); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::create('JPEG')); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::create('IMAGE/JPEG')); + } + + public function testCreateFromFileExtensionString(): void + { + $this->assertEquals(MediaType::IMAGE_PNG, MediaType::create('png')); + $this->assertEquals(MediaType::IMAGE_GIF, MediaType::create('gif')); + $this->assertEquals(MediaType::IMAGE_BMP, MediaType::create('bmp')); + $this->assertEquals(MediaType::IMAGE_WEBP, MediaType::create('webp')); + $this->assertEquals(MediaType::IMAGE_AVIF, MediaType::create('avif')); + $this->assertEquals(MediaType::IMAGE_TIFF, MediaType::create('tiff')); + } + + public function testCreateUnknown(): void + { + $this->expectException(InvalidArgumentException::class); + MediaType::create('foo'); + } + + public function testTryCreate(): void + { + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::tryCreate(MediaType::IMAGE_JPEG)); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::tryCreate(Format::JPEG)); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::tryCreate(FileExtension::JPG)); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::tryCreate('jpg')); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::tryCreate('jpeg')); + $this->assertEquals(MediaType::IMAGE_JPEG, MediaType::tryCreate('image/jpeg')); + $this->assertNull(Format::tryCreate('no-format')); + } + + public function testFormatJpeg(): void + { + $mime = MediaType::IMAGE_JPEG; + $this->assertEquals(Format::JPEG, $mime->format()); + + $mime = MediaType::IMAGE_PJPEG; + $this->assertEquals(Format::JPEG, $mime->format()); + + $mime = MediaType::IMAGE_JPG; + $this->assertEquals(Format::JPEG, $mime->format()); + + $mime = MediaType::IMAGE_X_JPEG; + $this->assertEquals(Format::JPEG, $mime->format()); + } + + public function testFormatWebp(): void + { + $mime = MediaType::IMAGE_WEBP; + $this->assertEquals(Format::WEBP, $mime->format()); + + $mime = MediaType::IMAGE_X_WEBP; + $this->assertEquals(Format::WEBP, $mime->format()); + } + + public function testFormatGif(): void + { + $mime = MediaType::IMAGE_GIF; + $this->assertEquals(Format::GIF, $mime->format()); + } + + public function testFormatPng(): void + { + $mime = MediaType::IMAGE_PNG; + $this->assertEquals(Format::PNG, $mime->format()); + + $mime = MediaType::IMAGE_X_PNG; + $this->assertEquals(Format::PNG, $mime->format()); + } + + public function testFormatAvif(): void + { + $mime = MediaType::IMAGE_AVIF; + $this->assertEquals(Format::AVIF, $mime->format()); + + $mime = MediaType::IMAGE_X_AVIF; + $this->assertEquals(Format::AVIF, $mime->format()); + } + + public function testFormatBmp(): void + { + $mime = MediaType::IMAGE_BMP; + $this->assertEquals(Format::BMP, $mime->format()); + + $mime = MediaType::IMAGE_X_BMP; + $this->assertEquals(Format::BMP, $mime->format()); + + $mime = MediaType::IMAGE_X_BITMAP; + $this->assertEquals(Format::BMP, $mime->format()); + + $mime = MediaType::IMAGE_X_WIN_BITMAP; + $this->assertEquals(Format::BMP, $mime->format()); + + $mime = MediaType::IMAGE_X_WINDOWS_BMP; + $this->assertEquals(Format::BMP, $mime->format()); + + $mime = MediaType::IMAGE_X_BMP3; + $this->assertEquals(Format::BMP, $mime->format()); + + $mime = MediaType::IMAGE_X_MS_BMP; + $this->assertEquals(Format::BMP, $mime->format()); + + $mime = MediaType::IMAGE_X_XBITMAP; + $this->assertEquals(Format::BMP, $mime->format()); + + $mime = MediaType::IMAGE_MS_BMP; + $this->assertEquals(Format::BMP, $mime->format()); + } + + public function testFormatTiff(): void + { + $mime = MediaType::IMAGE_TIFF; + $this->assertEquals(Format::TIFF, $mime->format()); + } + + public function testFormatJpeg2000(): void + { + $mime = MediaType::IMAGE_JPM; + $this->assertEquals(Format::JP2, $mime->format()); + + $mime = MediaType::IMAGE_JPX; + $this->assertEquals(Format::JP2, $mime->format()); + + $mime = MediaType::IMAGE_JP2; + $this->assertEquals(Format::JP2, $mime->format()); + + $mime = MediaType::IMAGE_X_JP2_CODESTREAM; + $this->assertEquals(Format::JP2, $mime->format()); + } + + public function testFormatIco(): void + { + $mime = MediaType::IMAGE_X_ICON; + $this->assertEquals(Format::ICO, $mime->format()); + + $mime = MediaType::IMAGE_VND_MICROSOFT_ICON; + $this->assertEquals(Format::ICO, $mime->format()); + } + + public function testFormatHeic(): void + { + $mime = MediaType::IMAGE_HEIC; + $this->assertEquals(Format::HEIC, $mime->format()); + + $mime = MediaType::IMAGE_X_HEIC; + $this->assertEquals(Format::HEIC, $mime->format()); + + $mime = MediaType::IMAGE_HEIF; + $this->assertEquals(Format::HEIC, $mime->format()); + } + + #[DataProvider('fileExtensionsDataProvider')] + public function testFileExtensions( + MediaType $mediaType, + int $fileExtensionCount, + FileExtension $fileExtension + ): void { + $this->assertCount($fileExtensionCount, $mediaType->fileExtensions()); + $this->assertEquals($fileExtension, $mediaType->fileExtension()); + } + + public static function fileExtensionsDataProvider(): Generator + { + yield [MediaType::IMAGE_JPEG, 4, FileExtension::JPG]; + yield [MediaType::IMAGE_JPG, 4, FileExtension::JPG]; + yield [MediaType::IMAGE_PJPEG, 4, FileExtension::JPG]; + yield [MediaType::IMAGE_X_JPEG, 4, FileExtension::JPG]; + yield [MediaType::IMAGE_WEBP, 1, FileExtension::WEBP]; + yield [MediaType::IMAGE_X_WEBP, 1, FileExtension::WEBP]; + yield [MediaType::IMAGE_GIF, 1, FileExtension::GIF]; + yield [MediaType::IMAGE_PNG, 1, FileExtension::PNG]; + yield [MediaType::IMAGE_X_PNG, 1, FileExtension::PNG]; + yield [MediaType::IMAGE_AVIF, 1, FileExtension::AVIF]; + yield [MediaType::IMAGE_X_AVIF, 1, FileExtension::AVIF]; + yield [MediaType::IMAGE_BMP, 1, FileExtension::BMP]; + yield [MediaType::IMAGE_MS_BMP, 1, FileExtension::BMP]; + yield [MediaType::IMAGE_X_BITMAP, 1, FileExtension::BMP]; + yield [MediaType::IMAGE_X_BMP, 1, FileExtension::BMP]; + yield [MediaType::IMAGE_X_MS_BMP, 1, FileExtension::BMP]; + yield [MediaType::IMAGE_X_WINDOWS_BMP, 1, FileExtension::BMP]; + yield [MediaType::IMAGE_X_WIN_BITMAP, 1, FileExtension::BMP]; + yield [MediaType::IMAGE_X_XBITMAP, 1, FileExtension::BMP]; + yield [MediaType::IMAGE_X_BMP3, 1, FileExtension::BMP]; + yield [MediaType::IMAGE_TIFF, 2, FileExtension::TIF]; + yield [MediaType::IMAGE_JP2, 9, FileExtension::JP2]; + yield [MediaType::IMAGE_JPX, 9, FileExtension::JP2]; + yield [MediaType::IMAGE_JPM, 9, FileExtension::JP2]; + yield [MediaType::IMAGE_HEIC, 2, FileExtension::HEIC]; + yield [MediaType::IMAGE_X_HEIC, 2, FileExtension::HEIC]; + yield [MediaType::IMAGE_HEIF, 2, FileExtension::HEIC]; + yield [MediaType::IMAGE_X_JP2_CODESTREAM, 9, FileExtension::JP2]; + yield [MediaType::IMAGE_X_ICON, 1, FileExtension::ICO]; + yield [MediaType::IMAGE_VND_MICROSOFT_ICON, 1, FileExtension::ICO]; + } +} diff --git a/tests/Unit/ModifierStackTest.php b/tests/Unit/ModifierStackTest.php new file mode 100644 index 000000000..6555c822f --- /dev/null +++ b/tests/Unit/ModifierStackTest.php @@ -0,0 +1,45 @@ +assertInstanceOf(ModifierStack::class, $stack); + } + + public function testPush(): void + { + $stack = new ModifierStack([]); + $result = $stack->push(new GrayscaleModifier()); + $this->assertInstanceOf(ModifierStack::class, $result); + } + + public function testApply(): void + { + $image = Mockery::mock(ImageInterface::class); + + $modifier1 = Mockery::mock(ModifierInterface::class)->makePartial(); + $modifier1->shouldReceive('apply')->once()->with($image); + + $modifier2 = Mockery::mock(ModifierInterface::class)->makePartial(); + $modifier2->shouldReceive('apply')->once()->with($image); + + $stack = new ModifierStack([$modifier1, $modifier2]); + $result = $stack->apply($image); + $this->assertInstanceOf(ImageInterface::class, $result); + } +} diff --git a/tests/Unit/Modifiers/ColorspaceModifierTest.php b/tests/Unit/Modifiers/ColorspaceModifierTest.php new file mode 100644 index 000000000..b4d684974 --- /dev/null +++ b/tests/Unit/Modifiers/ColorspaceModifierTest.php @@ -0,0 +1,147 @@ +assertInstanceOf( + Rgb::class, + $this->colorspaceModifier(new Rgb())->getTargetColorspace(), + ); + } + + public function testTargetColorspaceRgb(): void + { + $this->assertInstanceOf( + Rgb::class, + $this->colorspaceModifier('rgb')->getTargetColorspace(), + ); + } + + public function testTargetColorspaceSrgb(): void + { + $this->assertInstanceOf( + Rgb::class, + $this->colorspaceModifier('srgb')->getTargetColorspace(), + ); + } + + public function testTargetColorspaceRgba(): void + { + $this->assertInstanceOf( + Rgb::class, + $this->colorspaceModifier('rgba')->getTargetColorspace(), + ); + } + + public function testTargetColorspaceSrgba(): void + { + $this->assertInstanceOf( + Rgb::class, + $this->colorspaceModifier('srgba')->getTargetColorspace(), + ); + } + + public function testTargetColorspaceCmyk(): void + { + $this->assertInstanceOf( + Cmyk::class, + $this->colorspaceModifier('cmyk')->getTargetColorspace(), + ); + } + + public function testTargetColorspaceHsl(): void + { + $this->assertInstanceOf( + Hsl::class, + $this->colorspaceModifier('hsl')->getTargetColorspace(), + ); + } + + public function testTargetColorspaceHsv(): void + { + $this->assertInstanceOf( + Hsv::class, + $this->colorspaceModifier('hsv')->getTargetColorspace(), + ); + + $this->assertInstanceOf( + Hsv::class, + $this->colorspaceModifier('hsb')->getTargetColorspace(), + ); + } + + public function testTargetColorspaceOklab(): void + { + $this->assertInstanceOf( + Oklab::class, + $this->colorspaceModifier('oklab')->getTargetColorspace(), + ); + } + + public function testTargetColorspaceOklch(): void + { + $this->assertInstanceOf( + Oklch::class, + $this->colorspaceModifier('oklch')->getTargetColorspace(), + ); + } + + public function testTargetColorspaceSrgbAliases(): void + { + $this->assertInstanceOf( + Rgb::class, + $this->colorspaceModifier('srgb')->getTargetColorspace(), + ); + + $this->assertInstanceOf( + Rgb::class, + $this->colorspaceModifier('rgba')->getTargetColorspace(), + ); + + $this->assertInstanceOf( + Rgb::class, + $this->colorspaceModifier('srgba')->getTargetColorspace(), + ); + } + + public function testTargetColorspaceFail(): void + { + $this->expectException(NotSupportedException::class); + $this->colorspaceModifier('not_existing')->getTargetColorspace(); + } + + public function testTargetColorspaceClassExistsButNotColorspace(): void + { + $this->expectException(NotSupportedException::class); + $this->colorspaceModifier(\stdClass::class)->getTargetColorspace(); + } + + private function colorspaceModifier(string|ColorspaceInterface $colorspace): ColorspaceModifier + { + return new class ($colorspace) extends ColorspaceModifier + { + public function getTargetColorspace(): ColorspaceInterface + { + return parent::targetColorspace(); + } + }; + } +} diff --git a/tests/Unit/Modifiers/ContainDownModifierTest.php b/tests/Unit/Modifiers/ContainDownModifierTest.php new file mode 100644 index 000000000..89138cd6f --- /dev/null +++ b/tests/Unit/Modifiers/ContainDownModifierTest.php @@ -0,0 +1,33 @@ +shouldReceive('size')->andReturn($size); + $this->assertInstanceOf(SizeInterface::class, $modifier->getCropSize($image)); + } +} diff --git a/tests/Unit/Modifiers/DrawPolygonModifierTest.php b/tests/Unit/Modifiers/DrawPolygonModifierTest.php new file mode 100644 index 000000000..b7a61a5c8 --- /dev/null +++ b/tests/Unit/Modifiers/DrawPolygonModifierTest.php @@ -0,0 +1,30 @@ +expectException(InvalidArgumentException::class); + new DrawPolygonModifier(new Polygon([new Point(0, 0), new Point(1, 1)])); + } + + public function testConstructorWithThreePoints(): void + { + $modifier = new DrawPolygonModifier( + new Polygon([new Point(0, 0), new Point(1, 1), new Point(2, 0)]) + ); + $this->assertInstanceOf(DrawPolygonModifier::class, $modifier); + } +} diff --git a/tests/Unit/Modifiers/RemoveAnimationModifierTest.php b/tests/Unit/Modifiers/RemoveAnimationModifierTest.php new file mode 100644 index 000000000..cb92e896c --- /dev/null +++ b/tests/Unit/Modifiers/RemoveAnimationModifierTest.php @@ -0,0 +1,44 @@ +makePartial(); + $image->shouldReceive('count')->andReturn($frames); + + return $this->normalizePosition($image); + } + }; + + $this->assertEquals($normalized, $modifier->testResult($frames)); + } + + public static function normalizePositionProvider(): Generator + { + yield [0, 100, 0]; + yield [10, 100, 10]; + yield ['10', 100, 10]; + yield ['0%', 100, 0]; + yield ['50%', 100, 50]; + yield ['100%', 100, 99]; + } +} diff --git a/tests/Unit/Modifiers/ResizeModifierTest.php b/tests/Unit/Modifiers/ResizeModifierTest.php new file mode 100644 index 000000000..7573720a8 --- /dev/null +++ b/tests/Unit/Modifiers/ResizeModifierTest.php @@ -0,0 +1,38 @@ +expectException(InvalidArgumentException::class); + new ResizeModifier(); + } + + public function testConstructorWithWidth(): void + { + $modifier = new ResizeModifier(width: 100); + $this->assertInstanceOf(ResizeModifier::class, $modifier); + } + + public function testConstructorWithHeight(): void + { + $modifier = new ResizeModifier(height: 100); + $this->assertInstanceOf(ResizeModifier::class, $modifier); + } + + public function testConstructorWithBoth(): void + { + $modifier = new ResizeModifier(width: 100, height: 200); + $this->assertInstanceOf(ResizeModifier::class, $modifier); + } +} diff --git a/tests/Unit/Modifiers/TextModifierTest.php b/tests/Unit/Modifiers/TextModifierTest.php new file mode 100644 index 000000000..c58f9a290 --- /dev/null +++ b/tests/Unit/Modifiers/TextModifierTest.php @@ -0,0 +1,189 @@ + + */ + public function testStrokeOffsets(FontInterface $font): array + { + return $this->strokeOffsets($font); + } + }; + + $this->assertEquals([], $modifier->testStrokeOffsets(new Font())); + + $this->assertEquals([ + new Point(-1, -1), + new Point(-1, 0), + new Point(-1, 1), + new Point(0, -1), + new Point(0, 0), + new Point(0, 1), + new Point(1, -1), + new Point(1, 0), + new Point(1, 1), + ], $modifier->testStrokeOffsets((new Font())->setStrokeWidth(1))); + } + + public function testStrokeOffsetsWithWidth2(): void + { + $modifier = new class ('test', new Point(), new Font()) extends TextModifier + { + /** + * @return array + */ + public function testStrokeOffsets(FontInterface $font): array + { + return $this->strokeOffsets($font); + } + }; + + $offsets = $modifier->testStrokeOffsets((new Font())->setStrokeWidth(2)); + $this->assertCount(25, $offsets); + $this->assertEquals(new Point(-2, -2), $offsets[0]); + $this->assertEquals(new Point(2, 2), $offsets[24]); + } + + public function testConstructor(): void + { + $position = new Point(10, 20); + $font = new Font(); + $modifier = new class ('hello world', $position, $font) extends TextModifier + { + }; + + $this->assertEquals('hello world', $modifier->text); + $this->assertSame($position, $modifier->position); + $this->assertSame($font, $modifier->font); + } + + public function testTextColorOpaque(): void + { + $font = new Font(); + $modifier = new class ('test', new Point(), $font) extends TextModifier + { + /** + * Expose protected textColor method for testing. + */ + public function testTextColor(): ColorInterface + { + return $this->textColor(); + } + }; + + $color = Mockery::mock(ColorInterface::class); + $color->shouldReceive('isTransparent')->andReturn(false); + + $driver = Mockery::mock(DriverInterface::class); + $driver->shouldReceive('decodeColor')->andReturn($color); + + $property = new ReflectionProperty(TextModifier::class, 'driver'); + $property->setValue($modifier, $driver); + + $result = $modifier->testTextColor(); + $this->assertSame($color, $result); + } + + public function testTextColorTransparentWithStrokeThrowsException(): void + { + $font = (new Font())->setStrokeWidth(2); + $modifier = new class ('test', new Point(), $font) extends TextModifier + { + /** + * Expose protected textColor method for testing. + */ + public function testTextColor(): ColorInterface + { + return $this->textColor(); + } + }; + + $color = Mockery::mock(ColorInterface::class); + $color->shouldReceive('isTransparent')->andReturn(true); + + $driver = Mockery::mock(DriverInterface::class); + $driver->shouldReceive('decodeColor')->andReturn($color); + + $property = new ReflectionProperty(TextModifier::class, 'driver'); + $property->setValue($modifier, $driver); + + $this->expectException(StateException::class); + $modifier->testTextColor(); + } + + public function testStrokeColorOpaque(): void + { + $font = new Font(); + $modifier = new class ('test', new Point(), $font) extends TextModifier + { + /** + * Expose protected strokeColor method for testing. + */ + public function testStrokeColor(): ColorInterface + { + return $this->strokeColor(); + } + }; + + $color = Mockery::mock(ColorInterface::class); + $color->shouldReceive('isTransparent')->andReturn(false); + + $driver = Mockery::mock(DriverInterface::class); + $driver->shouldReceive('decodeColor')->andReturn($color); + + $property = new ReflectionProperty(TextModifier::class, 'driver'); + $property->setValue($modifier, $driver); + + $result = $modifier->testStrokeColor(); + $this->assertSame($color, $result); + } + + public function testStrokeColorTransparentThrowsException(): void + { + $font = new Font(); + $modifier = new class ('test', new Point(), $font) extends TextModifier + { + /** + * Expose protected strokeColor method for testing. + */ + public function testStrokeColor(): ColorInterface + { + return $this->strokeColor(); + } + }; + + $color = Mockery::mock(ColorInterface::class); + $color->shouldReceive('isTransparent')->andReturn(true); + + $driver = Mockery::mock(DriverInterface::class); + $driver->shouldReceive('decodeColor')->andReturn($color); + + $property = new ReflectionProperty(TextModifier::class, 'driver'); + $property->setValue($modifier, $driver); + + $this->expectException(StateException::class); + $modifier->testStrokeColor(); + } +} diff --git a/tests/Unit/OriginTest.php b/tests/Unit/OriginTest.php new file mode 100644 index 000000000..abe8cfa22 --- /dev/null +++ b/tests/Unit/OriginTest.php @@ -0,0 +1,111 @@ +path()); + $this->assertEquals(Resource::create('example.jpg')->path(), $origin->filePath()); + } + + public function testFilePathNull(): void + { + $origin = new Origin('image/jpeg'); + $this->assertNull($origin->filePath()); + } + + public function testFileExtension(): void + { + $origin = new Origin('image/jpeg', Resource::create('example.jpg')->path()); + $this->assertEquals('jpg', $origin->fileExtension()); + } + + public function testFileExtensionNull(): void + { + $origin = new Origin('image/jpeg'); + $this->assertNull($origin->fileExtension()); + } + + public function testFileExtensionWithoutExtension(): void + { + $origin = new Origin('image/jpeg', '/path/to/file'); + $this->assertNull($origin->fileExtension()); + } + + public function testSetGetMediaType(): void + { + $origin = new Origin(); + $this->assertEquals('application/octet-stream', $origin->mediaType()); + + $origin = new Origin('image/gif'); + $this->assertEquals('image/gif', $origin->mediaType()); + $this->assertEquals('image/gif', $origin->mimetype()); + $result = $origin->setMediaType('image/jpeg'); + $this->assertEquals('image/jpeg', $origin->mediaType()); + $this->assertEquals('image/jpeg', $result->mediaType()); + } + + public function testSetMediaTypeWithEnum(): void + { + $origin = new Origin(); + $result = $origin->setMediaType(MediaType::IMAGE_PNG); + $this->assertEquals('image/png', $origin->mediaType()); + $this->assertSame($origin, $result); + } + + public function testSetFilePath(): void + { + $origin = new Origin('image/jpeg'); + $this->assertNull($origin->filePath()); + $result = $origin->setFilePath('/some/path/image.jpg'); + $this->assertEquals('/some/path/image.jpg', $origin->filePath()); + $this->assertSame($origin, $result); + } + + public function testFormatJpeg(): void + { + $this->assertEquals(Format::JPEG, (new Origin('image/jpeg'))->format()); + } + + public function testFormatGif(): void + { + $this->assertEquals(Format::GIF, (new Origin('image/gif'))->format()); + } + + public function testFormatFail(): void + { + $this->expectException(NotSupportedException::class); + (new Origin())->format(); + } + + public function testDebugInfo(): void + { + $origin = new Origin('image/jpeg', '/path/to/image.jpg'); + $debug = $origin->__debugInfo(); + $this->assertArrayHasKey('mediaType', $debug); + $this->assertArrayHasKey('filePath', $debug); + $this->assertEquals('image/jpeg', $debug['mediaType']); + $this->assertEquals('/path/to/image.jpg', $debug['filePath']); + } + + public function testDebugInfoWithoutFilePath(): void + { + $origin = new Origin('image/png'); + $debug = $origin->__debugInfo(); + $this->assertEquals('image/png', $debug['mediaType']); + $this->assertNull($debug['filePath']); + } +} diff --git a/tests/Unit/ResolutionTest.php b/tests/Unit/ResolutionTest.php new file mode 100644 index 000000000..fc1848243 --- /dev/null +++ b/tests/Unit/ResolutionTest.php @@ -0,0 +1,157 @@ +assertInstanceOf(Resolution::class, $resolution); + } + + public function testConstructorNegativeX(): void + { + $this->expectException(InvalidArgumentException::class); + new Resolution(-1, 1); + } + + public function testConstructorNegativeY(): void + { + $this->expectException(InvalidArgumentException::class); + new Resolution(1, -1); + } + + public function testIteration(): void + { + $resolution = new Resolution(1.2, 3.4); + foreach ($resolution as $value) { + $this->assertIsFloat($value); + } + } + + public function testXY(): void + { + $resolution = new Resolution(1.2, 3.4); + $this->assertEquals(1.2, $resolution->x()); + $this->assertEquals(3.4, $resolution->y()); + } + + public function testLength(): void + { + $resolution = new Resolution(1, 1); + $this->assertEquals(Length::INCH, $resolution->length()); + + $resolution = new Resolution(1, 1, Length::CM); + $this->assertEquals(Length::CM, $resolution->length()); + } + + public function testDpiFactory(): void + { + $resolution = Resolution::dpi(300, 150); + $this->assertInstanceOf(Resolution::class, $resolution); + $this->assertEquals(300, $resolution->x()); + $this->assertEquals(150, $resolution->y()); + $this->assertEquals(Length::INCH, $resolution->length()); + } + + public function testPpiFactory(): void + { + $resolution = Resolution::ppi(300, 150); + $this->assertInstanceOf(Resolution::class, $resolution); + $this->assertEquals(300, $resolution->x()); + $this->assertEquals(150, $resolution->y()); + $this->assertEquals(Length::CM, $resolution->length()); + } + + public function testConversion(): void + { + $resolution = new Resolution(300, 150); // per inch + $this->assertEquals(300, $resolution->perInch()->x()); + $this->assertEquals(150, $resolution->perInch()->y()); + + $resolution = new Resolution(300, 150); // per inch + $this->assertEquals(118.11, round($resolution->perCm()->x(), 2)); + $this->assertEquals(59.06, round($resolution->perCm()->y(), 2)); + + $resolution = new Resolution(118.11024, 59.06, Length::CM); // per cm + $this->assertEquals(300, round($resolution->perInch()->x())); + $this->assertEquals(150, round($resolution->perInch()->y())); + } + + public function testPerCmWhenAlreadyCm(): void + { + $resolution = new Resolution(100, 200, Length::CM); + $result = $resolution->perCm(); + $this->assertSame($resolution, $result); + $this->assertEquals(100, $result->x()); + $this->assertEquals(200, $result->y()); + $this->assertEquals(Length::CM, $result->length()); + } + + public function testPerInchWhenAlreadyInch(): void + { + $resolution = new Resolution(300, 150, Length::INCH); + $result = $resolution->perInch(); + $this->assertSame($resolution, $result); + $this->assertEquals(300, $result->x()); + $this->assertEquals(150, $result->y()); + $this->assertEquals(Length::INCH, $result->length()); + } + + public function testToString(): void + { + $resolution = new Resolution(300, 150, Length::CM); + $this->assertEquals('300.00 x 150.00 dpcm', $resolution->toString()); + + $resolution = new Resolution(300, 150, Length::INCH); + $this->assertEquals('300.00 x 150.00 dpi', $resolution->toString()); + $this->assertEquals('300.00 x 150.00 dpi', (string) $resolution); + } + + public function testDpiStaticFactory(): void + { + $resolution = Resolution::dpi(300, 150); + $this->assertInstanceOf(Resolution::class, $resolution); + $this->assertEquals(300, $resolution->x()); + $this->assertEquals(150, $resolution->y()); + $this->assertEquals(Length::INCH, $resolution->length()); + } + + public function testPpiStaticFactory(): void + { + $resolution = Resolution::ppi(118, 59); + $this->assertInstanceOf(Resolution::class, $resolution); + $this->assertEquals(118, $resolution->x()); + $this->assertEquals(59, $resolution->y()); + $this->assertEquals(Length::CM, $resolution->length()); + } + + public function testNegativeXThrowsException(): void + { + $this->expectException(\Intervention\Image\Exceptions\InvalidArgumentException::class); + new Resolution(-1, 100); + } + + public function testNegativeYThrowsException(): void + { + $this->expectException(\Intervention\Image\Exceptions\InvalidArgumentException::class); + new Resolution(100, -1); + } + + public function testZeroValuesAllowed(): void + { + $resolution = new Resolution(0, 0); + $this->assertEquals(0, $resolution->x()); + $this->assertEquals(0, $resolution->y()); + } +} diff --git a/tests/Unit/SizeTest.php b/tests/Unit/SizeTest.php new file mode 100644 index 000000000..dbac6292f --- /dev/null +++ b/tests/Unit/SizeTest.php @@ -0,0 +1,542 @@ +assertInstanceOf(Size::class, $size); + $this->assertEquals(800, $size->width()); + $this->assertEquals(600, $size->height()); + } + + public function testConstructorWithPivot(): void + { + $pivot = new Point(10, 20); + $size = new Size(800, 600, $pivot); + $this->assertEquals(800, $size->width()); + $this->assertEquals(600, $size->height()); + $this->assertEquals(10, $size->pivot()->x()); + $this->assertEquals(20, $size->pivot()->y()); + } + + public function testConstructorNegativeWidth(): void + { + $this->expectException(InvalidArgumentException::class); + new Size(-1, 600); + } + + public function testConstructorNegativeHeight(): void + { + $this->expectException(InvalidArgumentException::class); + new Size(800, -1); + } + + public function testConstructorZeroDimensions(): void + { + $size = new Size(0, 0); + $this->assertEquals(0, $size->width()); + $this->assertEquals(0, $size->height()); + } + + public function testCreate(): void + { + $size = Size::create(300, 200, new Point(10, 20)); + $this->assertEquals(300, $size->width()); + $this->assertEquals(200, $size->height()); + $this->assertEquals(10, $size->pivot()->x()); + $this->assertEquals(20, $size->pivot()->y()); + } + + public function testSetSize(): void + { + $size = new Size(800, 600); + $result = $size->setSize(400, 300); + $this->assertSame($size, $result); + $this->assertEquals(400, $size->width()); + $this->assertEquals(300, $size->height()); + } + + public function testSetWidth(): void + { + $size = new Size(800, 600); + $result = $size->setWidth(400); + $this->assertSame($size, $result); + $this->assertEquals(400, $size->width()); + $this->assertEquals(600, $size->height()); + } + + public function testSetHeight(): void + { + $size = new Size(800, 600); + $result = $size->setHeight(300); + $this->assertSame($size, $result); + $this->assertEquals(800, $size->width()); + $this->assertEquals(300, $size->height()); + } + + public function testPivot(): void + { + $size = new Size(800, 600); + $this->assertInstanceOf(Point::class, $size->pivot()); + $this->assertEquals(0, $size->pivot()->x()); + $this->assertEquals(0, $size->pivot()->y()); + } + + public function testSetPivot(): void + { + $size = new Size(800, 600); + $pivot = new Point(100, 200); + $result = $size->setPivot($pivot); + $this->assertSame($size, $result); + $this->assertSame($pivot, $size->pivot()); + } + + public function testSetPosition(): void + { + $size = new Size(800, 600); + $position = new Point(50, 50); + $result = $size->setPosition($position); + $this->assertSame($size, $result); + } + + public function testMovePivotTopLeft(): void + { + $size = new Size(800, 600); + $result = $size->movePivot(Alignment::TOP_LEFT); + $this->assertSame($size, $result); + $this->assertEquals(0, $size->pivot()->x()); + $this->assertEquals(0, $size->pivot()->y()); + } + + public function testMovePivotTop(): void + { + $size = new Size(800, 600); + $size->movePivot(Alignment::TOP); + $this->assertEquals(400, $size->pivot()->x()); + $this->assertEquals(0, $size->pivot()->y()); + } + + public function testMovePivotTopRight(): void + { + $size = new Size(800, 600); + $size->movePivot(Alignment::TOP_RIGHT); + $this->assertEquals(800, $size->pivot()->x()); + $this->assertEquals(0, $size->pivot()->y()); + } + + public function testMovePivotLeft(): void + { + $size = new Size(800, 600); + $size->movePivot(Alignment::LEFT); + $this->assertEquals(0, $size->pivot()->x()); + $this->assertEquals(300, $size->pivot()->y()); + } + + public function testMovePivotCenter(): void + { + $size = new Size(800, 600); + $size->movePivot(Alignment::CENTER); + $this->assertEquals(400, $size->pivot()->x()); + $this->assertEquals(300, $size->pivot()->y()); + } + + public function testMovePivotRight(): void + { + $size = new Size(800, 600); + $size->movePivot(Alignment::RIGHT); + $this->assertEquals(800, $size->pivot()->x()); + $this->assertEquals(300, $size->pivot()->y()); + } + + public function testMovePivotBottomLeft(): void + { + $size = new Size(800, 600); + $size->movePivot(Alignment::BOTTOM_LEFT); + $this->assertEquals(0, $size->pivot()->x()); + $this->assertEquals(600, $size->pivot()->y()); + } + + public function testMovePivotBottom(): void + { + $size = new Size(800, 600); + $size->movePivot(Alignment::BOTTOM); + $this->assertEquals(400, $size->pivot()->x()); + $this->assertEquals(600, $size->pivot()->y()); + } + + public function testMovePivotBottomRight(): void + { + $size = new Size(800, 600); + $size->movePivot(Alignment::BOTTOM_RIGHT); + $this->assertEquals(800, $size->pivot()->x()); + $this->assertEquals(600, $size->pivot()->y()); + } + + public function testMovePivotWithOffsets(): void + { + $size = new Size(800, 600); + $size->movePivot(Alignment::TOP_LEFT, 10, 20); + $this->assertEquals(10, $size->pivot()->x()); + $this->assertEquals(20, $size->pivot()->y()); + } + + public function testMovePivotWithString(): void + { + $size = new Size(800, 600); + $size->movePivot('center'); + $this->assertEquals(400, $size->pivot()->x()); + $this->assertEquals(300, $size->pivot()->y()); + } + + public function testMovePivotWithInvalidAlignmentFallsToDefault(): void + { + $size = new Size(800, 600); + $this->expectException(InvalidArgumentException::class); + $size->movePivot('invalid-alignment', 5, 10); + } + + public function testAlignPivotTo(): void + { + $size = new Size(200, 100); + $reference = new Size(800, 600); + $result = $size->alignPivotTo($reference, Alignment::CENTER); + $this->assertSame($size, $result); + $this->assertEquals(300, $size->pivot()->x()); + $this->assertEquals(250, $size->pivot()->y()); + } + + public function testAlignPivotToTopLeft(): void + { + $size = new Size(200, 100); + $reference = new Size(800, 600); + $size->alignPivotTo($reference, Alignment::TOP_LEFT); + $this->assertEquals(0, $size->pivot()->x()); + $this->assertEquals(0, $size->pivot()->y()); + } + + public function testAlignPivotToBottomRight(): void + { + $size = new Size(200, 100); + $reference = new Size(800, 600); + $size->alignPivotTo($reference, Alignment::BOTTOM_RIGHT); + $this->assertEquals(600, $size->pivot()->x()); + $this->assertEquals(500, $size->pivot()->y()); + } + + public function testOffsetTo(): void + { + $size1 = new Size(800, 600); + $size1->movePivot(Alignment::CENTER); + + $size2 = new Size(200, 100); + $size2->movePivot(Alignment::CENTER); + + $position = $size1->offsetTo($size2); + $this->assertEquals(300, $position->x()); + $this->assertEquals(250, $position->y()); + } + + public function testAspectRatio(): void + { + $size = new Size(800, 600); + $this->assertEqualsWithDelta(1.333, $size->aspectRatio(), 0.001); + + $size = new Size(600, 600); + $this->assertEquals(1.0, $size->aspectRatio()); + + $size = new Size(400, 800); + $this->assertEquals(0.5, $size->aspectRatio()); + } + + public function testFitsInto(): void + { + $size = new Size(200, 100); + $container = new Size(800, 600); + $this->assertTrue($size->fitsWithin($container)); + + $size = new Size(800, 600); + $container = new Size(800, 600); + $this->assertTrue($size->fitsWithin($container)); + + $size = new Size(900, 600); + $container = new Size(800, 600); + $this->assertFalse($size->fitsWithin($container)); + + $size = new Size(800, 700); + $container = new Size(800, 600); + $this->assertFalse($size->fitsWithin($container)); + } + + public function testIsLandscape(): void + { + $this->assertTrue((new Size(800, 600))->isLandscape()); + $this->assertFalse((new Size(600, 800))->isLandscape()); + $this->assertFalse((new Size(600, 600))->isLandscape()); + } + + public function testIsPortrait(): void + { + $this->assertTrue((new Size(600, 800))->isPortrait()); + $this->assertFalse((new Size(800, 600))->isPortrait()); + $this->assertFalse((new Size(600, 600))->isPortrait()); + } + + public function testResize(): void + { + $size = new Size(800, 600); + $result = $size->resize(400, 300); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testResizeWidthOnly(): void + { + $size = new Size(800, 600); + $result = $size->resize(400); + $this->assertEquals(400, $result->width()); + $this->assertEquals(600, $result->height()); + } + + public function testResizeHeightOnly(): void + { + $size = new Size(800, 600); + $result = $size->resize(height: 300); + $this->assertEquals(800, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testResizeDown(): void + { + $size = new Size(800, 600); + $result = $size->resizeDown(400, 300); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testResizeDownDoesNotUpscale(): void + { + $size = new Size(400, 300); + $result = $size->resizeDown(800, 600); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScale(): void + { + $size = new Size(800, 600); + $result = $size->scale(400); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleByHeight(): void + { + $size = new Size(800, 600); + $result = $size->scale(height: 300); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleDown(): void + { + $size = new Size(800, 600); + $result = $size->scaleDown(400); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleDownDoesNotUpscale(): void + { + $size = new Size(400, 300); + $result = $size->scaleDown(800); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testCover(): void + { + $size = new Size(800, 600); + $result = $size->cover(400, 400); + $this->assertEquals(533, $result->width()); + $this->assertEquals(400, $result->height()); + } + + public function testContain(): void + { + $size = new Size(800, 600); + $result = $size->contain(400, 400); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testContainDown(): void + { + $size = new Size(800, 600); + $result = $size->containDown(400, 400); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testContainDownDoesNotUpscale(): void + { + $size = new Size(200, 150); + $result = $size->containDown(400, 400); + $this->assertEquals(200, $result->width()); + $this->assertEquals(150, $result->height()); + } + + public function testGetIterator(): void + { + $size = new Size(800, 600); + $values = iterator_to_array($size->getIterator()); + $this->assertEquals([800, 600], $values); + } + + public function testDebugInfo(): void + { + $size = new Size(800, 600); + $info = $size->__debugInfo(); + $this->assertArrayHasKey('width', $info); + $this->assertArrayHasKey('height', $info); + $this->assertArrayHasKey('pivot', $info); + $this->assertEquals(800, $info['width']); + $this->assertEquals(600, $info['height']); + } + + public function testResizeInvalid(): void + { + $size = new Size(800, 600); + $this->expectException(InvalidArgumentException::class); + $size->resize(-1, -1); + } + + public function testResizeDownInvalid(): void + { + $size = new Size(800, 600); + $this->expectException(InvalidArgumentException::class); + $size->resizeDown(-1, -1); + } + + public function testScaleInvalid(): void + { + $size = new Size(800, 600); + $this->expectException(InvalidArgumentException::class); + $size->scale(-1); + } + + public function testScaleDownInvalid(): void + { + $size = new Size(800, 600); + $this->expectException(InvalidArgumentException::class); + $size->scaleDown(-1); + } + + public function testCoverInvalid(): void + { + $size = new Size(800, 600); + $this->expectException(InvalidArgumentException::class); + $size->cover(-1, -1); + } + + public function testContainInvalid(): void + { + $size = new Size(800, 600); + $this->expectException(InvalidArgumentException::class); + $size->contain(-1, -1); + } + + public function testContainDownInvalid(): void + { + $size = new Size(800, 600); + $this->expectException(InvalidArgumentException::class); + $size->containDown(-1, -1); + } + + public function testCoverDown(): void + { + $size = new Size(800, 600); + $result = $size->cover(400, 400); + $this->assertGreaterThanOrEqual(400, $result->width()); + $this->assertGreaterThanOrEqual(400, $result->height()); + } + + public function testScaleBothWidthAndHeight(): void + { + $size = new Size(800, 600); + $result = $size->scale(400, 300); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleDownBothWidthAndHeight(): void + { + $size = new Size(800, 600); + $result = $size->scaleDown(400, 300); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testScaleDownHeightOnly(): void + { + $size = new Size(800, 600); + $result = $size->scaleDown(height: 300); + $this->assertEquals(400, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testResizeDownWidthOnly(): void + { + $size = new Size(800, 600); + $result = $size->resizeDown(400); + $this->assertEquals(400, $result->width()); + $this->assertEquals(600, $result->height()); + } + + public function testResizeDownHeightOnly(): void + { + $size = new Size(800, 600); + $result = $size->resizeDown(height: 300); + $this->assertEquals(800, $result->width()); + $this->assertEquals(300, $result->height()); + } + + public function testCoverPortraitSource(): void + { + // Cover where proportional height check triggers the auto-width branch + $size = new Size(400, 800); + $result = $size->cover(200, 200); + $this->assertGreaterThanOrEqual(200, $result->width()); + $this->assertGreaterThanOrEqual(200, $result->height()); + } + + public function testContainPortrait(): void + { + // Contain where auto-height exceeds target so auto-width branch is taken + $size = new Size(400, 800); + $result = $size->contain(200, 200); + $this->assertLessThanOrEqual(200, $result->width()); + $this->assertLessThanOrEqual(200, $result->height()); + } + + public function testContainDownPortrait(): void + { + // ContainDown where auto-height exceeds target so auto-width branch is taken + $size = new Size(400, 800); + $result = $size->containDown(200, 200); + $this->assertLessThanOrEqual(200, $result->width()); + $this->assertLessThanOrEqual(200, $result->height()); + } +} diff --git a/tests/Unit/Traits/CanBeDriverSpecializedTest.php b/tests/Unit/Traits/CanBeDriverSpecializedTest.php new file mode 100644 index 000000000..640f73d26 --- /dev/null +++ b/tests/Unit/Traits/CanBeDriverSpecializedTest.php @@ -0,0 +1,120 @@ +specializationArguments(); + $this->assertIsArray($result); + $this->assertEquals(['amount' => 10, 'name' => 'foo'], $result); + } + + public function testSpecializableWithoutConstructor(): void + { + $object = new class () implements SpecializableInterface { + use CanBeDriverSpecialized; + }; + + $result = $object->specializationArguments(); + $this->assertIsArray($result); + $this->assertEmpty($result); + } + + public function testDriverThrowsWhenNotSet(): void + { + $object = new class () implements SpecializableInterface { + use CanBeDriverSpecialized; + }; + + $this->expectException(StateException::class); + $object->driver(); + } + + public function testSetDriverAndDriver(): void + { + $driver = Mockery::mock(DriverInterface::class); + $driver->shouldReceive('id')->andReturn('Test'); + + $object = new class () implements SpecializableInterface { + use CanBeDriverSpecialized; + + /** + * Override belongsToDriver to always return true for testing. + */ + protected function belongsToDriver(object $driver): bool + { + return true; + } + }; + + $result = $object->setDriver($driver); + $this->assertSame($object, $result); + $this->assertSame($driver, $object->driver()); + } + + public function testSetDriverFailsWhenNotBelonging(): void + { + $driver = Mockery::mock(DriverInterface::class); + $driver->shouldReceive('id')->andReturn('Test'); + + $object = new class () implements SpecializableInterface { + use CanBeDriverSpecialized; + + /** + * Override belongsToDriver to always return false for testing. + */ + protected function belongsToDriver(object $driver): bool + { + return false; + } + }; + + $this->expectException(NotSupportedException::class); + $object->setDriver($driver); + } + + public function testBelongsToDriver(): void + { + // Use a real driver-namespaced modifier to test belongsToDriver + $modifier = new \Intervention\Image\Drivers\Gd\Modifiers\BlurModifier(5); + $driver = new \Intervention\Image\Drivers\Gd\Driver(); + + // setDriver should succeed because modifier and driver share namespace + $result = $modifier->setDriver($driver); + $this->assertSame($modifier, $result); + } + + public function testBelongsToDriverFails(): void + { + // Use a Gd-namespaced modifier with an Imagick driver. + // The modifier's belongsToDriver uses str_starts_with on namespaces, + // so a Gd modifier should not belong to an Imagick driver. + $modifier = new \Intervention\Image\Drivers\Gd\Modifiers\BlurModifier(5); + $driver = new \Intervention\Image\Drivers\Imagick\Driver(); + + $this->expectException(NotSupportedException::class); + $modifier->setDriver($driver); + } +} diff --git a/tests/Unit/Traits/CanBuildStreamTest.php b/tests/Unit/Traits/CanBuildStreamTest.php new file mode 100644 index 000000000..b206374f2 --- /dev/null +++ b/tests/Unit/Traits/CanBuildStreamTest.php @@ -0,0 +1,57 @@ +createBuilder(); + $result = $builder::buildStreamOrFail(null); + $this->assertIsResource($result); + fclose($result); + } + + public function testBuildStreamFromString(): void + { + $builder = $this->createBuilder(); + $result = $builder::buildStreamOrFail('test data'); + $this->assertIsResource($result); + $this->assertEquals('test data', stream_get_contents($result)); + fclose($result); + } + + public function testBuildStreamFromResource(): void + { + $builder = $this->createBuilder(); + $resource = fopen('php://temp', 'r+'); + fwrite($resource, 'resource data'); + $result = $builder::buildStreamOrFail($resource); + $this->assertIsResource($result); + $this->assertEquals('resource data', stream_get_contents($result)); + fclose($result); + } + + public function testBuildStreamFailsWithInvalidType(): void + { + $builder = $this->createBuilder(); + $this->expectException(InvalidArgumentException::class); + $builder::buildStreamOrFail(12345); + } + + /** + * Create an anonymous class that exposes the trait method. + */ + private function createBuilder(): object + { + return new class () { + use CanBuildStream; + }; + } +} diff --git a/tests/Unit/Traits/CanDetectImageSourcesTest.php b/tests/Unit/Traits/CanDetectImageSourcesTest.php new file mode 100644 index 000000000..4aa563e72 --- /dev/null +++ b/tests/Unit/Traits/CanDetectImageSourcesTest.php @@ -0,0 +1,190 @@ +createDetector(); + $this->assertTrue($detector->callCouldBeBase64Data(Resource::create('test.jpg')->base64())); + } + + public function testCouldBeBase64DataWithPaddedBase64(): void + { + $detector = $this->createDetector(); + $this->assertTrue($detector->callCouldBeBase64Data('dGVzdA==')); + } + + public function testCouldBeBase64DataWithNonString(): void + { + $detector = $this->createDetector(); + $this->assertFalse($detector->callCouldBeBase64Data(12345)); + $this->assertFalse($detector->callCouldBeBase64Data(null)); + $this->assertFalse($detector->callCouldBeBase64Data([])); + } + + public function testCouldBeBase64DataWithNonPaddedBase64(): void + { + $detector = $this->createDetector(); + // "YWJj" is base64 for "abc" — no padding, passes through the final + // base64_encode($decoded) === $input check + $this->assertTrue($detector->callCouldBeBase64Data('YWJj')); + } + + public function testCouldBeBase64DataWithInvalidBase64(): void + { + $detector = $this->createDetector(); + $this->assertFalse($detector->callCouldBeBase64Data('not base64 content!@#')); + } + + public function testCouldBeBase64DataWithStringable(): void + { + $detector = $this->createDetector(); + $stringable = new class () implements Stringable { + public function __toString(): string + { + return 'dGVzdA=='; + } + }; + $this->assertTrue($detector->callCouldBeBase64Data($stringable)); + } + + public function testCouldBeBinaryDataWithBinaryContent(): void + { + $detector = $this->createDetector(); + $this->assertTrue($detector->callCouldBeBinaryData(Resource::create('test.jpg')->data())); + } + + public function testCouldBeBinaryDataWithPlainText(): void + { + $detector = $this->createDetector(); + $this->assertFalse($detector->callCouldBeBinaryData('Hello World')); + } + + public function testCouldBeBinaryDataWithNonString(): void + { + $detector = $this->createDetector(); + $this->assertFalse($detector->callCouldBeBinaryData(12345)); + $this->assertFalse($detector->callCouldBeBinaryData(null)); + } + + public function testCouldBeBinaryDataWithEmptyString(): void + { + $detector = $this->createDetector(); + $this->assertTrue($detector->callCouldBeBinaryData('')); + } + + public function testCouldBeBinaryDataWithStringable(): void + { + $detector = $this->createDetector(); + $stringable = Resource::create('test.jpg')->stringableData(); + $this->assertTrue($detector->callCouldBeBinaryData($stringable)); + } + + public function testCouldBeDataUrlWithValidDataUrl(): void + { + $detector = $this->createDetector(); + $this->assertTrue($detector->callCouldBeDataUrl('data:image/jpeg;base64,/9j/4AAQ')); + } + + public function testCouldBeDataUrlWithDataUriInterface(): void + { + $detector = $this->createDetector(); + $dataUri = new DataUri('test', 'image/jpeg'); + $this->assertTrue($detector->callCouldBeDataUrl($dataUri)); + } + + public function testCouldBeDataUrlWithNonDataUrl(): void + { + $detector = $this->createDetector(); + $this->assertFalse($detector->callCouldBeDataUrl('http://example.com')); + $this->assertFalse($detector->callCouldBeDataUrl('/path/to/file.jpg')); + $this->assertFalse($detector->callCouldBeDataUrl(12345)); + } + + public function testCouldBeFilePathWithValidPath(): void + { + $detector = $this->createDetector(); + $this->assertTrue($detector->callCouldBeFilePath('/path/to/file.jpg')); + $this->assertTrue($detector->callCouldBeFilePath('relative/path/file.jpg')); + $this->assertTrue($detector->callCouldBeFilePath('file.jpg')); + } + + public function testCouldBeFilePathWithNonString(): void + { + $detector = $this->createDetector(); + $this->assertFalse($detector->callCouldBeFilePath(12345)); + $this->assertFalse($detector->callCouldBeFilePath(null)); + } + + public function testCouldBeFilePathWithBinaryData(): void + { + $detector = $this->createDetector(); + $this->assertFalse($detector->callCouldBeFilePath("\x00\x01\x02binary")); + } + + public function testCouldBeFilePathWithTooLongPath(): void + { + $detector = $this->createDetector(); + $longPath = str_repeat('a', PHP_MAXPATHLEN + 1); + $this->assertFalse($detector->callCouldBeFilePath($longPath)); + } + + public function testCouldBeFilePathWithAbsolutePath(): void + { + $detector = $this->createDetector(); + $path = DIRECTORY_SEPARATOR . 'absolute' . DIRECTORY_SEPARATOR . 'path'; + $this->assertTrue($detector->callCouldBeFilePath($path)); + } + + public function testCouldBeFilePathWithStringable(): void + { + $detector = $this->createDetector(); + $stringable = new class () implements Stringable { + public function __toString(): string + { + return '/path/to/file.jpg'; + } + }; + $this->assertTrue($detector->callCouldBeFilePath($stringable)); + } + + /** + * Create an anonymous class that exposes the protected trait methods. + */ + private function createDetector(): object + { + return new class () { + use CanDetectImageSources; + + public function callCouldBeBase64Data(mixed $input): bool + { + return $this->couldBeBase64Data($input); + } + + public function callCouldBeBinaryData(mixed $input): bool + { + return $this->couldBeBinaryData($input); + } + + public function callCouldBeDataUrl(mixed $input): bool + { + return $this->couldBeDataUrl($input); + } + + public function callCouldBeFilePath(mixed $input): bool + { + return $this->couldBeFilePath($input); + } + }; + } +} diff --git a/tests/Unit/Traits/CanParseFilePathTest.php b/tests/Unit/Traits/CanParseFilePathTest.php new file mode 100644 index 000000000..b109f307e --- /dev/null +++ b/tests/Unit/Traits/CanParseFilePathTest.php @@ -0,0 +1,135 @@ +createParser(); + $result = $parser->callReadableFilePathOrFail(Resource::create('test.jpg')->path()); + $this->assertIsString($result); + $this->assertFileExists($result); + } + + public function testReadableFilePathOrFailWithNonStringInput(): void + { + $parser = $this->createParser(); + $this->expectException(InvalidArgumentException::class); + $parser->callReadableFilePathOrFail(12345); + } + + public function testReadableFilePathOrFailWithEmptyString(): void + { + $parser = $this->createParser(); + $this->expectException(InvalidArgumentException::class); + $parser->callReadableFilePathOrFail(''); + } + + public function testReadableFilePathOrFailWithNonExistentDirectory(): void + { + $parser = $this->createParser(); + $this->expectException(DirectoryNotFoundException::class); + $parser->callReadableFilePathOrFail('/nonexistent_dir_abc123/file.jpg'); + } + + public function testReadableFilePathOrFailWithNonExistentFile(): void + { + $parser = $this->createParser(); + $this->expectException(FileNotFoundException::class); + $parser->callReadableFilePathOrFail(__DIR__ . '/nonexistent_file_abc123.jpg'); + } + + public function testReadableFilePathOrFailWithDirectory(): void + { + $parser = $this->createParser(); + $this->expectException(FileNotFoundException::class); + $parser->callReadableFilePathOrFail(__DIR__); + } + + public function testReadableFilePathOrFailWithStringableInput(): void + { + $parser = $this->createParser(); + $stringable = Resource::create('test.jpg')->stringablePath(); + $result = $parser->callReadableFilePathOrFail($stringable); + $this->assertIsString($result); + $this->assertFileExists($result); + } + + public function testFilePathFromSplFileInfoOrFailWithValidFile(): void + { + $parser = $this->createParser(); + $splFileInfo = new SplFileInfo(Resource::create('test.jpg')->path()); + $result = $parser->callFilePathFromSplFileInfoOrFail($splFileInfo); + $this->assertIsString($result); + $this->assertFileExists($result); + } + + public function testFilePathFromSplFileInfoOrFailWithNonExistentDirectory(): void + { + $parser = $this->createParser(); + $splFileInfo = new SplFileInfo('/nonexistent_dir_abc123/file.jpg'); + $this->expectException(DirectoryNotFoundException::class); + $parser->callFilePathFromSplFileInfoOrFail($splFileInfo); + } + + public function testFilePathFromSplFileInfoOrFailWithNonExistentFile(): void + { + $parser = $this->createParser(); + $splFileInfo = new SplFileInfo(__DIR__ . '/nonexistent_file_abc123.jpg'); + $this->expectException(FileNotFoundException::class); + $parser->callFilePathFromSplFileInfoOrFail($splFileInfo); + } + + public function testFilePathFromSplFileInfoOrFailWithDirectory(): void + { + $parser = $this->createParser(); + $splFileInfo = new SplFileInfo(__DIR__); + $this->expectException(FileNotFoundException::class); + $parser->callFilePathFromSplFileInfoOrFail($splFileInfo); + } + + public function testReadableFilePathOrFailWithPathExceedingMaxLength(): void + { + $parser = $this->createParser(); + $this->expectException(InvalidArgumentException::class); + $parser->callReadableFilePathOrFail(str_repeat('a', PHP_MAXPATHLEN + 1)); + } + + public function testFilePathFromSplFileInfoOrFailWithEmptyPath(): void + { + $parser = $this->createParser(); + $this->expectException(InvalidArgumentException::class); + $parser->callFilePathFromSplFileInfoOrFail(new SplFileInfo('')); + } + + /** + * Create an anonymous class that exposes the protected trait methods. + */ + private function createParser(): object + { + return new class () { + use CanParseFilePath; + + public function callReadableFilePathOrFail(mixed $path): string + { + return self::readableFilePathOrFail($path); + } + + public function callFilePathFromSplFileInfoOrFail(SplFileInfo $splFileInfo): string + { + return self::filePathFromSplFileInfoOrFail($splFileInfo); + } + }; + } +} diff --git a/tests/Unit/Typography/FontFactoryTest.php b/tests/Unit/Typography/FontFactoryTest.php new file mode 100644 index 000000000..8266b3611 --- /dev/null +++ b/tests/Unit/Typography/FontFactoryTest.php @@ -0,0 +1,118 @@ +font(); + $this->assertInstanceOf(FontInterface::class, $result); + } + + public function testCreateWithFont(): void + { + $fontFile = Resource::create('test.ttf')->path(); + $factory = new FontFactory(new Font($fontFile)); + $result = $factory->font(); + $this->assertInstanceOf(FontInterface::class, $result); + $this->assertEquals($fontFile, $result->filepath()); + } + + public function testCreateWithCallback(): void + { + $factory = new FontFactory(function (FontFactory $font): void { + $font->filename(Resource::create('test.ttf')->path()); + $font->color('#b01735'); + $font->size(70); + $font->align(Alignment::CENTER, Alignment::TOP); + $font->lineHeight(1.6); + $font->angle(10); + $font->wrap(100); + $font->stroke('ff5500', 4); + }); + + $result = $factory->font(); + $this->assertInstanceOf(FontInterface::class, $result); + $this->assertEquals(Resource::create('test.ttf')->path(), $result->filepath()); + $this->assertEquals('#b01735', $result->color()); + $this->assertEquals(70, $result->size()); + $this->assertEquals(Alignment::CENTER, $result->alignmentHorizontal()); + $this->assertEquals(Alignment::TOP, $result->alignmentVertical()); + $this->assertEquals(1.6, $result->lineHeight()); + $this->assertEquals(10, $result->angle()); + $this->assertEquals(100, $result->wrapWidth()); + $this->assertEquals(4, $result->strokeWidth()); + $this->assertEquals('ff5500', $result->strokeColor()); + } + + public function testBuild(): void + { + $font = FontFactory::build(function (FontFactory $font): void { + $font->filename(Resource::create('test.ttf')->path()); + $font->color('#b01735'); + $font->size(70); + $font->align(Alignment::CENTER, Alignment::TOP); + $font->lineHeight(1.6); + $font->angle(10); + $font->wrap(100); + $font->stroke('ff5500', 4); + }); + + $this->assertInstanceOf(FontInterface::class, $font); + $this->assertEquals(Resource::create('test.ttf')->path(), $font->filepath()); + $this->assertEquals('#b01735', $font->color()); + $this->assertEquals(70, $font->size()); + $this->assertEquals(Alignment::CENTER, $font->alignmentHorizontal()); + $this->assertEquals(Alignment::TOP, $font->alignmentVertical()); + $this->assertEquals(1.6, $font->lineHeight()); + $this->assertEquals(10, $font->angle()); + $this->assertEquals(100, $font->wrapWidth()); + $this->assertEquals(4, $font->strokeWidth()); + $this->assertEquals('ff5500', $font->strokeColor()); + } + + public function testFile(): void + { + $factory = new FontFactory(); + $result = $factory->file(Resource::create('test.ttf')->path()); + $this->assertInstanceOf(FontFactory::class, $result); + $this->assertEquals(Resource::create('test.ttf')->path(), $factory->font()->filepath()); + } + + public function testFilepath(): void + { + $factory = new FontFactory(); + $result = $factory->filepath(Resource::create('test.ttf')->path()); + $this->assertInstanceOf(FontFactory::class, $result); + $this->assertEquals(Resource::create('test.ttf')->path(), $factory->font()->filepath()); + } + + public function testAlignHorizontalOnly(): void + { + $factory = new FontFactory(); + $result = $factory->align(Alignment::CENTER); + $this->assertInstanceOf(FontFactory::class, $result); + $this->assertEquals(Alignment::CENTER, $factory->font()->alignmentHorizontal()); + } + + public function testAlignVerticalOnly(): void + { + $factory = new FontFactory(); + $result = $factory->align(null, Alignment::TOP); + $this->assertInstanceOf(FontFactory::class, $result); + $this->assertEquals(Alignment::TOP, $factory->font()->alignmentVertical()); + } +} diff --git a/tests/Unit/Typography/FontTest.php b/tests/Unit/Typography/FontTest.php new file mode 100644 index 000000000..3ce96a43b --- /dev/null +++ b/tests/Unit/Typography/FontTest.php @@ -0,0 +1,168 @@ +assertInstanceOf(Font::class, $font); + $this->assertNull($font->filepath()); + + $font = new Font(Resource::create('test.ttf')->path()); + $this->assertInstanceOf(Font::class, $font); + $this->assertEquals('test.ttf', basename($font->filepath())); + } + + public function testSetGetSize(): void + { + $font = new Font(); + $this->assertEquals(12, $font->size()); + $result = $font->setSize(123); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals(123, $font->size()); + } + + public function testSetGetAngle(): void + { + $font = new Font(); + $this->assertEquals(0, $font->angle()); + $result = $font->setAngle(123); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals(123, $font->angle()); + } + + public function testSetGetFilename(): void + { + $font = new Font(); + $this->assertEquals(null, $font->filepath()); + $this->assertFalse($font->hasFile()); + $filename = Resource::create()->path(); + $result = $font->setFilepath($filename); + $this->assertTrue($font->hasFile()); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals($filename, $font->filepath()); + } + + public function testSetGetColor(): void + { + $font = new Font(); + $this->assertEquals('000000', $font->color()); + $result = $font->setColor('fff'); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals('fff', $font->color()); + } + + public function testSetGetAlignment(): void + { + $font = new Font(); + $this->assertEquals(Alignment::LEFT, $font->alignmentHorizontal()); + + $result = $font->setAlignmentHorizontal(Alignment::CENTER); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals(Alignment::CENTER, $font->alignmentHorizontal()); + + $result = $font->setAlignmentHorizontal(Alignment::BOTTOM); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals(Alignment::BOTTOM, $font->alignmentHorizontal()); + } + + public function testSetAlignmentHorizontalWithString(): void + { + $font = new Font(); + $result = $font->setAlignmentHorizontal('center'); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals(Alignment::CENTER, $font->alignmentHorizontal()); + } + + public function testSetGetVerticalAlignment(): void + { + $font = new Font(); + $this->assertEquals(Alignment::BOTTOM, $font->alignmentVertical()); + + $result = $font->setAlignmentVertical(Alignment::CENTER); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals(Alignment::CENTER, $font->alignmentVertical()); + + $result = $font->setAlignmentVertical(Alignment::RIGHT); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals(Alignment::RIGHT, $font->alignmentVertical()); + } + + public function testSetAlignmentVerticalWithString(): void + { + $font = new Font(); + $result = $font->setAlignmentVertical('top'); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals(Alignment::TOP, $font->alignmentVertical()); + } + + public function testSetGetLineHeight(): void + { + $font = new Font(); + $this->assertEquals(1.25, $font->lineHeight()); + $result = $font->setLineHeight(3.2); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals(3.2, $font->lineHeight()); + } + + public function testSetGetStrokeColor(): void + { + $font = new Font(); + $this->assertEquals('ffffff', $font->strokeColor()); + $result = $font->setStrokeColor('000000'); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals('000000', $font->strokeColor()); + } + + public function testSetGetStrokeWidth(): void + { + $font = new Font(); + $this->assertEquals(0, $font->strokeWidth()); + $result = $font->setStrokeWidth(4); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals(4, $font->strokeWidth()); + } + + public function testSetStrokeWidthOutOfRange(): void + { + $font = new Font(); + $this->expectException(InvalidArgumentException::class); + $font->setStrokeWidth(11); + } + + public function testHasStrokeEffect(): void + { + $font = new Font(); + $this->assertFalse($font->hasStrokeEffect()); + + $font->setStrokeWidth(1); + $this->assertTrue($font->hasStrokeEffect()); + + $font->setStrokeWidth(5); + $this->assertTrue($font->hasStrokeEffect()); + } + + public function testSetGetWrapWidth(): void + { + $font = new Font(); + $this->assertNull($font->wrapWidth()); + $result = $font->setWrapWidth(200); + $this->assertInstanceOf(Font::class, $result); + $this->assertEquals(200, $font->wrapWidth()); + + $font->setWrapWidth(null); + $this->assertNull($font->wrapWidth()); + } +} diff --git a/tests/Unit/Typography/LineTest.php b/tests/Unit/Typography/LineTest.php new file mode 100644 index 000000000..b876e567e --- /dev/null +++ b/tests/Unit/Typography/LineTest.php @@ -0,0 +1,92 @@ +assertInstanceOf(Line::class, $line); + } + + #[DataProvider('toStringDataProvider')] + public function testToString(string $text, int $words): void + { + $line = new Line($text); + $this->assertEquals($words, $line->count()); + $this->assertEquals($text, (string) $line); + } + + public function testSetGetPosition(): void + { + $line = new Line('foo'); + $this->assertEquals(0, $line->position()->x()); + $this->assertEquals(0, $line->position()->y()); + + $line->setPosition(new Point(10, 11)); + $this->assertEquals(10, $line->position()->x()); + $this->assertEquals(11, $line->position()->y()); + } + + public function testCount(): void + { + $line = new Line(); + $this->assertEquals(0, $line->count()); + + $line = new Line("foo"); + $this->assertEquals(1, $line->count()); + + $line = new Line("foo bar"); + $this->assertEquals(2, $line->count()); + } + + public function testLength(): void + { + $line = new Line(); + $this->assertEquals(0, $line->length()); + + $line = new Line("foo"); + $this->assertEquals(3, $line->length()); + + $line = new Line("foo bar."); + $this->assertEquals(8, $line->length()); + + $line = new Line("🫷🙂🫸"); + $this->assertEquals(3, $line->length()); + } + + public function testAdd(): void + { + $line = new Line(); + $this->assertEquals(0, $line->count()); + + $result = $line->add('foo'); + $this->assertEquals(1, $line->count()); + $this->assertEquals(1, $result->count()); + + $result = $line->add('bar'); + $this->assertEquals(2, $line->count()); + $this->assertEquals(2, $result->count()); + } + + public static function toStringDataProvider(): Generator + { + yield ['foo', 1]; + yield ['foo bar', 2]; + yield ['测试', 2]; // CJK Unified Ideographs + yield ['テスト', 3]; // japanese + yield ['ทดสอบ', 5]; // thai + yield ['这只是我写的一个测试。', 11]; // CJK Unified Ideographs + } +} diff --git a/tests/Unit/Typography/TextBlockTest.php b/tests/Unit/Typography/TextBlockTest.php new file mode 100644 index 000000000..8555bc710 --- /dev/null +++ b/tests/Unit/Typography/TextBlockTest.php @@ -0,0 +1,48 @@ +block = new TextBlock(<<assertEquals(3, $this->block->count()); + } + + public function testLines(): void + { + $this->assertCount(3, $this->block->lines()); + } + + public function testGetLine(): void + { + $this->assertEquals('foo', $this->block->line(0)); + $this->assertEquals('FooBar', $this->block->line(1)); + $this->assertEquals('bar', $this->block->line(2)); + $this->assertNull($this->block->line(20)); + } + + public function testLongestLine(): void + { + $result = $this->block->longestLine(); + $this->assertEquals('FooBar', (string) $result); + } +} diff --git a/tests/WidenCommandTest.php b/tests/WidenCommandTest.php deleted file mode 100644 index a5444dfb3..000000000 --- a/tests/WidenCommandTest.php +++ /dev/null @@ -1,48 +0,0 @@ -aspectRatio(); }; - $resource = imagecreatefromjpeg(__DIR__.'/images/test.jpg'); - $image = Mockery::mock('Intervention\Image\Image'); - $size = Mockery::mock('Intervention\Image\Size', array(800, 600)); - $size->shouldReceive('resize')->once()->andReturn($size); - $size->shouldReceive('getWidth')->once()->andReturn(800); - $size->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('getWidth')->once()->andReturn(800); - $image->shouldReceive('getHeight')->once()->andReturn(600); - $image->shouldReceive('getSize')->once()->andReturn($size); - $image->shouldReceive('getCore')->once()->andReturn($resource); - $image->shouldReceive('setCore')->once(); - $command = new WidenGd(array(200)); - $result = $command->execute($image); - $this->assertTrue($result); - } - - public function testImagick() - { - $callback = function ($constraint) { $constraint->upsize(); }; - $imagick = Mockery::mock('Imagick'); - $imagick->shouldReceive('scaleimage')->with(300, 200)->once()->andReturn(true); - $size = Mockery::mock('Intervention\Image\Size', array(800, 600)); - $size->shouldReceive('resize')->once()->andReturn($size); - $size->shouldReceive('getWidth')->once()->andReturn(300); - $size->shouldReceive('getHeight')->once()->andReturn(200); - $image = Mockery::mock('Intervention\Image\Image'); - $image->shouldReceive('getCore')->once()->andReturn($imagick); - $image->shouldReceive('getSize')->once()->andReturn($size); - $command = new WidenImagick(array(200)); - $result = $command->execute($image); - $this->assertTrue($result); - } -} diff --git a/tests/images/broken.png b/tests/images/broken.png deleted file mode 100644 index eaecd5c64..000000000 Binary files a/tests/images/broken.png and /dev/null differ diff --git a/tests/images/gradient.png b/tests/images/gradient.png deleted file mode 100644 index 47f1be21b..000000000 Binary files a/tests/images/gradient.png and /dev/null differ diff --git a/tests/images/iptc.jpg b/tests/images/iptc.jpg deleted file mode 100644 index 849dd0a3f..000000000 Binary files a/tests/images/iptc.jpg and /dev/null differ diff --git a/tests/images/star.png b/tests/images/star.png deleted file mode 100644 index def6448d8..000000000 Binary files a/tests/images/star.png and /dev/null differ diff --git a/tests/images/test.jpg b/tests/images/test.jpg deleted file mode 100644 index 4bb654914..000000000 Binary files a/tests/images/test.jpg and /dev/null differ diff --git a/tests/resources/150.dpi.jpg b/tests/resources/150.dpi.jpg new file mode 100644 index 000000000..77d438b44 Binary files /dev/null and b/tests/resources/150.dpi.jpg differ diff --git a/tests/resources/150.dpi.png b/tests/resources/150.dpi.png new file mode 100644 index 000000000..e8eeaaf6c Binary files /dev/null and b/tests/resources/150.dpi.png differ diff --git a/tests/resources/150.dpi.tif b/tests/resources/150.dpi.tif new file mode 100644 index 000000000..b85368095 Binary files /dev/null and b/tests/resources/150.dpi.tif differ diff --git a/tests/resources/300dpi.png b/tests/resources/300dpi.png new file mode 100644 index 000000000..58f54bf9e Binary files /dev/null and b/tests/resources/300dpi.png differ diff --git a/tests/resources/animation.gif b/tests/resources/animation.gif new file mode 100644 index 000000000..c45bb7771 Binary files /dev/null and b/tests/resources/animation.gif differ diff --git a/tests/resources/blocks.png b/tests/resources/blocks.png new file mode 100644 index 000000000..d57fabe3e Binary files /dev/null and b/tests/resources/blocks.png differ diff --git a/tests/resources/blue.gif b/tests/resources/blue.gif new file mode 100644 index 000000000..4be6587b5 Binary files /dev/null and b/tests/resources/blue.gif differ diff --git a/tests/resources/cats.gif b/tests/resources/cats.gif new file mode 100644 index 000000000..307d4f0e5 Binary files /dev/null and b/tests/resources/cats.gif differ diff --git a/tests/images/circle.png b/tests/resources/circle.png similarity index 100% rename from tests/images/circle.png rename to tests/resources/circle.png diff --git a/tests/resources/cmyk.jpg b/tests/resources/cmyk.jpg new file mode 100644 index 000000000..1459b2c02 Binary files /dev/null and b/tests/resources/cmyk.jpg differ diff --git a/tests/images/exif.jpg b/tests/resources/exif.jpg similarity index 100% rename from tests/images/exif.jpg rename to tests/resources/exif.jpg diff --git a/tests/resources/gradient.bmp b/tests/resources/gradient.bmp new file mode 100644 index 000000000..08661077b Binary files /dev/null and b/tests/resources/gradient.bmp differ diff --git a/tests/resources/gradient.gif b/tests/resources/gradient.gif new file mode 100644 index 000000000..0371d6a22 Binary files /dev/null and b/tests/resources/gradient.gif differ diff --git a/tests/resources/green.gif b/tests/resources/green.gif new file mode 100644 index 000000000..99c38625a Binary files /dev/null and b/tests/resources/green.gif differ diff --git a/tests/resources/orientation.jpg b/tests/resources/orientation.jpg new file mode 100644 index 000000000..49c16de26 Binary files /dev/null and b/tests/resources/orientation.jpg differ diff --git a/tests/resources/orientation/landscape_0.jpg b/tests/resources/orientation/landscape_0.jpg new file mode 100644 index 000000000..7e2e4f9ab Binary files /dev/null and b/tests/resources/orientation/landscape_0.jpg differ diff --git a/tests/resources/orientation/landscape_1.jpg b/tests/resources/orientation/landscape_1.jpg new file mode 100644 index 000000000..05bad55e0 Binary files /dev/null and b/tests/resources/orientation/landscape_1.jpg differ diff --git a/tests/resources/orientation/landscape_2.jpg b/tests/resources/orientation/landscape_2.jpg new file mode 100644 index 000000000..02f90b561 Binary files /dev/null and b/tests/resources/orientation/landscape_2.jpg differ diff --git a/tests/resources/orientation/landscape_3.jpg b/tests/resources/orientation/landscape_3.jpg new file mode 100644 index 000000000..460b32c44 Binary files /dev/null and b/tests/resources/orientation/landscape_3.jpg differ diff --git a/tests/resources/orientation/landscape_4.jpg b/tests/resources/orientation/landscape_4.jpg new file mode 100644 index 000000000..df8e328f5 Binary files /dev/null and b/tests/resources/orientation/landscape_4.jpg differ diff --git a/tests/resources/orientation/landscape_5.jpg b/tests/resources/orientation/landscape_5.jpg new file mode 100644 index 000000000..0f1efcd32 Binary files /dev/null and b/tests/resources/orientation/landscape_5.jpg differ diff --git a/tests/resources/orientation/landscape_6.jpg b/tests/resources/orientation/landscape_6.jpg new file mode 100644 index 000000000..5c92932c7 Binary files /dev/null and b/tests/resources/orientation/landscape_6.jpg differ diff --git a/tests/resources/orientation/landscape_7.jpg b/tests/resources/orientation/landscape_7.jpg new file mode 100644 index 000000000..63f000f22 Binary files /dev/null and b/tests/resources/orientation/landscape_7.jpg differ diff --git a/tests/resources/orientation/landscape_8.jpg b/tests/resources/orientation/landscape_8.jpg new file mode 100644 index 000000000..6299f0a34 Binary files /dev/null and b/tests/resources/orientation/landscape_8.jpg differ diff --git a/tests/resources/radial.png b/tests/resources/radial.png new file mode 100644 index 000000000..788cbdfbe Binary files /dev/null and b/tests/resources/radial.png differ diff --git a/tests/resources/red.gif b/tests/resources/red.gif new file mode 100644 index 000000000..408dff183 Binary files /dev/null and b/tests/resources/red.gif differ diff --git a/tests/resources/sphere.webp b/tests/resources/sphere.webp new file mode 100644 index 000000000..c3b2239d3 Binary files /dev/null and b/tests/resources/sphere.webp differ diff --git a/tests/resources/test.jpg b/tests/resources/test.jpg new file mode 100644 index 000000000..0b02a3053 Binary files /dev/null and b/tests/resources/test.jpg differ diff --git a/tests/resources/test.ttf b/tests/resources/test.ttf new file mode 100644 index 000000000..330e91982 Binary files /dev/null and b/tests/resources/test.ttf differ diff --git a/tests/images/tile.png b/tests/resources/tile.png similarity index 100% rename from tests/images/tile.png rename to tests/resources/tile.png diff --git a/tests/images/trim.png b/tests/resources/trim.png similarity index 100% rename from tests/images/trim.png rename to tests/resources/trim.png diff --git a/tests/tmp/.gitkeep b/tests/tmp/.gitkeep deleted file mode 100644 index e69de29bb..000000000