├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── docker ├── README.md ├── php_5.6 │ ├── Dockerfile │ └── php-ini-overrides.ini └── php_8.x │ ├── Dockerfile │ └── php-ini-overrides.ini ├── examples ├── example_img.php ├── example_pdf.php ├── img │ ├── _result_simple.jpg │ ├── php.png │ └── poster.jpg └── pdf │ └── The_Man_In_The_Red_Underpants.pdf ├── phpunit.xml.dist ├── src └── Ajaxray │ └── PHPWatermark │ ├── CommandBuilders │ ├── AbstractCommandBuilder.php │ ├── CommandBuilderFactory.php │ ├── ImageCommandBuilder.php │ ├── PDFCommandBuilder.php │ └── WatermarkCommandBuilderInterface.php │ ├── Requirements │ └── RequirementsChecker.php │ └── Watermark.php └── tests ├── Ajaxray ├── PHPWatermark │ └── Tests │ │ ├── CommandBuilders │ │ ├── ImageCommandBuilderTest.php │ │ └── PDFCommandBuilderTest.php │ │ └── WatermarkTest.php └── TestUtils │ ├── NonPublicAccess.php │ └── OverrideFunctions.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | composer.lock 3 | examples/*/result_* 4 | vendor/ 5 | build/ 6 | 7 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file 8 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 9 | # composer.lock 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.6 4 | - 7.1 5 | - 7.2 6 | - nightly 7 | 8 | matrix: 9 | allow_failures: 10 | - php: nightly 11 | script: 12 | - composer install --prefer-dist --no-interaction 13 | - mkdir -p build/logs 14 | - vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover build/logs/clover.xml 15 | 16 | after_script: 17 | - php vendor/bin/coveralls 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Anis uddin Ahmad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPWatermark 2 | 3 | [![SensioLabsInsight](https://insight.symfony.com/projects/df0125e5-463e-4135-bfc2-252dec120fc3/big.svg)](https://insight.symfony.com/projects/cf8fe138-7232-4390-a3c6-f9e509221353) 4 | [![Latest Stable Version](https://poser.pugx.org/ajaxray/php-watermark/v/stable)](https://packagist.org/packages/ajaxray/php-watermark) 5 | [![Build Status](https://travis-ci.org/ajaxray/php-watermark.svg?branch=master)](https://travis-ci.org/ajaxray/php-watermark) 6 | [![Coverage Status](https://coveralls.io/repos/github/ajaxray/php-watermark/badge.svg?branch=master)](https://coveralls.io/github/ajaxray/php-watermark?branch=master) 7 | [![Total Downloads](https://poser.pugx.org/ajaxray/php-watermark/downloads)](https://packagist.org/packages/ajaxray/php-watermark) 8 | [![License](https://poser.pugx.org/ajaxray/php-watermark/license)](https://packagist.org/packages/ajaxray/php-watermark) 9 | 10 | 11 | Add text or image Watermark on image and PDF using PHP and [ImageMagick][1]. 12 | 13 | ### Pre-requisite 14 | 15 | - PHP (version 5.6 or higher) 16 | - [ImageMagick][1] 17 | - [ghostscript][2] (only for PDF watermarking) 18 | 19 | _PHP [ImageMagick][3] extension is **not required**._ 20 | 21 | ### Installation 22 | 23 | Add as a dependency with composer 24 | 25 | ```bash 26 | $ composer require ajaxray/php-watermark 27 | ``` 28 | Or download latest version as a [Zip file](https://github.com/ajaxray/php-watermark/archive/master.zip). 29 | 30 | For PHP versions `>= 5.6` to `<8` use `v0.1.2` 31 | ```bash 32 | $ composer require ajaxray/php-watermark:v0.1.2 33 | ``` 34 | Also you should check [older readme file](https://github.com/ajaxray/php-watermark/tree/v0.1.2#readme) for PHP version `<8` 35 | 36 | 37 | ### How to use? 38 | 39 | ```php 40 | withText('ajaxray.com') 46 | ->setFontSize(48) 47 | ->setRotate(30) 48 | ->setOpacity(.4) 49 | ->write('path/to/output.jpg'); 50 | 51 | 52 | // Watermark with Image 53 | $watermark->withImage('path/to/logo.png') 54 | ->setPosition(Watermark::POSITION_BOTTOM_RIGHT) 55 | ->setStyle(Watermark::STYLE_IMG_DISSOLVE) 56 | ->write('path/to/output.jpg'); 57 | ``` 58 | If output file name is skipped for `Watermark::write()` function, the source file will be overridden. 59 | 60 | 61 | ### Customization options 62 | 63 | The table below shows customization options and their support matrix. 64 | Listed functions should be called on an object of `Ajaxray\PHPWatermark\Watermark`. 65 | Checkmark column titles means the following - 66 | 67 | - Txt-Img: Watermarking with text on Image ([sample][4], [sample-tiled][5]) 68 | - Img-Img: Watermarking with Image on Image ([sample][6]) 69 | - Txt-PDF: Watermarking with text on PDF ([sample][7]) 70 | - Img-PDF: Watermarking with Image on PDF ([sample][8]) 71 | 72 | ⌛ = coming soon! 73 | 74 | | Function | Value | Txt-Img | Img-Img | Txt-PDF | Img-PDF | 75 | |---|---|:---:|:---:|:---:|:---:| 76 | |`setFont('Arial')` | string; Font Name | ✅ | | ✅ | | 77 | |`setFontSize(36)` | int; Font size | ✅ | | ✅ | | 78 | |`setOpacity(.4)` | float; between 0 (opaque) to 1 (transparent) | ✅ | ✅ | ✅ | ✅ | 79 | |`setRotate(245)` | int; between 0 to 360 | ✅ | | ✅ | | 80 | |`setPosition($position)` | int; One of `Watermark::POSITION_*` constants | ✅ | ✅ | ✅ | ✅ | 81 | |`setOffset(50, 100)` | int, int; X and Y offset relative to position | ✅ | ✅ | ✅ | ✅ | 82 | |`setStyle($style)` | int; One of `Watermark::STYLE_*` constants | ⌛ | ✅ | ⌛ | ⌛ | 83 | |`setTiled()` | boolean; (default `true`) | ✅ | ✅ | ⌛ | ⌛ | 84 | |`setTileSize(200, 150)` | int, int; Width and Height of each tile | ✅ | | ⌛ | | 85 | 86 | 87 | BTW, all the samples linked above are the results of [these examples][9]. 88 | You may generate them yourself just by running example scripts from command line - 89 | 90 | ```bash 91 | $ php vendor/ajaxray/php-watermark/examples/example_img.php 92 | $ php vendor/ajaxray/php-watermark/examples/example_pdf.php 93 | ``` 94 | Then you should get the result files in `vendor/ajaxray/php-watermark/examples/img` 95 | and `vendor/ajaxray/php-watermark/examples/pdf` directories. 96 | 97 | ### Something unexpected? Debug! 🐞🔫 98 | 99 | If anything unexpected happened, try to debug the issue. 100 | 101 | 1. First step is to check if PHP is configured to display errors. Alternatively you may add these lines at the top of your script. 102 | ```php 103 | ini_set('display_errors', 1); 104 | error_reporting(E_ALL); 105 | ``` 106 | 2. A common reason of not getting expected result is mistakes in filepaths. You may try logging / printing source and destination file paths. 107 | 3. Check the permission of the parent directory of destination path. Destination directory indicates - 108 | - The file path mentioned in the second argument of `Watermark::withText()` and `Watermark::withImage()` methods. 109 | - Parent of the source file itself if no separate destination mentioned in above methods. 110 | 4. There is `Watermark::setDebug()` method which will make `Watermark` object to return **imagemagick** command instead of executing it. 111 | Then, you may run the output manually to check if there is any error in underlying `imagemagick` commands. 112 | 113 | 114 | #### Notes: 115 | 116 | * To see the list of supported font names in your system, run `convert -list font` on command prompt 117 | * Remember to set appropriate output file extension (e,g, .pdf for pdf files) 118 | * If possible, use absolute path for files to avoid various mistakes. 119 | * `STYLE_IMG_*` constants are for Image watermarks and `Watermark::STYLE_TEXT_*` are for text. 120 | * Default text style (`Watermark::STYLE_TEXT_BEVEL`) is expected to be visible on any background. 121 | Use other text styles only on selective backgrounds. 122 | * UnitTest are executed and all green against **PHP 5.6** and **PHP 7.1** using **PHPUnit 5.7.5** 123 | * I'v tested all intended functionality with **ImageMagick 7.0.4-6 Q16 x86_64** and **GPL Ghostscript 9.20** installed. 124 | 125 | 126 | #### Important Update for PDF watermarking: 127 | 128 | When using the imagemagick + ghostscript extraction and joining pdf pages, it has a few drawbacks including file-size issue. Many developers were asking for a solution about the file size and PDF quality since releasing of this library. 129 | So, I've created a command line tool for PDF watermarking that will work without converting pages into images. As a result, you'll get better PDF quality and dramatically smaller file size. 130 | 131 | https://github.com/ajaxray/markpdf 132 | 133 | Please note that, it's not a PHP library. So you've to use it using [exec][10], [shell_exec][11] or [Symfony Process Component][12]. 134 | 135 | --- 136 | 137 | > "This is the Book about which there is no doubt, a guidance for those conscious of Allah" - [Al-Quran](http://quran.com) 138 | 139 | [1]: http://www.imagemagick.org "ImageMagick Command line tool" 140 | [2]: https://www.ghostscript.com/ "GhostScript" 141 | [3]: http://php.net/manual/en/book.imagick.php "PHP ImageMagick Extension" 142 | [4]: https://www.dropbox.com/s/itff1ot0h4lj1o3/watermark_text_on_img.jpg?dl=0 "Text Watermarking on Image" 143 | [5]: https://www.dropbox.com/s/8xvr1xwlm76jiom/watermark_text_tiles_on_img.jpg?dl=0 "Tiled Text Watermarking on Image" 144 | [6]: https://www.dropbox.com/s/k2ghbaaif1vxnws/watermark_img_on_img.jpg?dl=0 "Image Watermarking on Image" 145 | [7]: https://www.dropbox.com/s/aorp9aoggynn3pt/watermark_text_on_pdf.pdf?dl=0 "Text Watermarking on PDF" 146 | [8]: https://www.dropbox.com/s/myn2is2nx3xtm3v/watermark_img_on_pdf.pdf?dl=0 "Image Watermarking on PDF" 147 | [9]: https://github.com/ajaxray/php-watermark/tree/master/examples "Example scripts" 148 | [10]: http://php.net/manual/en/function.exec.php 149 | [11]: http://php.net/manual/en/function.shell-exec.php 150 | [12]: https://symfony.com/doc/current/components/process.html 151 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ajaxray/php-watermark", 3 | "description": "Add text or image watermark on images.", 4 | "minimum-stability": "dev", 5 | "license": "MIT", 6 | "keywords": [ 7 | "watermark", 8 | "image", 9 | "image-manipulation", 10 | "imagemagick-watermark", 11 | "imagemagick-wrapper" 12 | ], 13 | "homepage": "https://github.com/ajaxray/php-watermark", 14 | "type": "library", 15 | "authors": [ 16 | { 17 | "name": "Anis Uddin Ahmad", 18 | "email": "anis.programmer@gmail.com" 19 | } 20 | ], 21 | "require": { 22 | "php": "^7.4 | ^8.0", 23 | "ext-fileinfo": "*" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Ajaxray\\PHPWatermark\\": "src/Ajaxray/PHPWatermark/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Ajaxray\\PHPWatermark\\Tests\\": "tests/Ajaxray/PHPWatermark/", 33 | "Ajaxray\\TestUtils\\": "tests/Ajaxray/TestUtils/" 34 | } 35 | }, 36 | "require-dev": { 37 | "satooshi/php-coveralls": "*", 38 | "phpunit/phpunit": "9.*" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Test/Develop with Docker 2 | 3 | ## Testing with PHP8.x 4 | The HEAD of master branch is PHP8 ready. 5 | 6 | Move to Dockerfile for php 8.x and build an image `php56_imagick` 7 | ```shell 8 | cd docker/php_8.x 9 | docker build -t php8_imagick . 10 | ``` 11 | Now go back to source root and run a container with memorable name (used `watermark` here). 12 | ```shell 13 | cd ../.. 14 | docker run -dit --name watermark -v $(pwd):/usr/app php8_imagick 15 | ``` 16 | 17 | Done! You are ready to run any command from that container and of course the tests :) 18 | ```shell 19 | docker exec watermark vendor/bin/phpunit -c phpunit.xml.dist 20 | ``` 21 | 22 | ## Testing with older version (PHP 5.6 - 7.x) 23 | 24 | First, switch to right version of source code 25 | ```shell 26 | git checkout tags/0.1.2 27 | ``` 28 | 29 | Move to Dockerfile for php 5.6 and build an image `php56_imagick` 30 | ```shell 31 | cd docker/php_5.6 32 | docker build -t php56_imagick . 33 | ``` 34 | Now go back to source root and run a container with memorable name (used `watermark56` here). 35 | ```shell 36 | cd ../.. 37 | docker run -dit --name watermark56 -v $(pwd):/usr/app php56_imagick 38 | ``` 39 | 40 | Done! You are ready to run any command from that container and of course the tests :) 41 | ```shell 42 | docker exec watermark56 vendor/bin/phpunit -c phpunit.xml.dist 43 | ``` 44 | 45 | -------------------------------------------------------------------------------- /docker/php_5.6/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:5.6-cli-alpine 2 | 3 | COPY . /usr/app 4 | WORKDIR /usr/app 5 | 6 | ADD php-ini-overrides.ini /usr/local/etc/php/conf.d/overrides.ini 7 | 8 | # Install required tools 9 | RUN apk add --update --no-cache sqlite-dev rsync imagemagick bash pngcrush optipng=0.7.7-r0 10 | 11 | # Install PHP extensions 12 | # Available extensions: 13 | # bcmath bz2 calendar ctype curl dba dom enchant exif fileinfo filter ftp gd gettext gmp hash iconv 14 | # imap interbase intl json ldap mbstring mcrypt mssql mysql mysqli oci8 odbc opcache pcntl pdo 15 | # pdo_dblib pdo_firebird pdo_mysql pdo_oci pdo_odbc pdo_pgsql pdo_sqlite pgsql phar posix pspell 16 | # readline recode reflection session shmop simplexml snmp soap sockets spl standard sybase_ct sysvmsg 17 | # sysvsem sysvshm tidy tokenizer wddx xml xmlreader xmlrpc xmlwriter xsl zip 18 | RUN docker-php-ext-install -j$(nproc) pdo_mysql pdo_sqlite 19 | 20 | # Install composer globally 21 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 22 | 23 | # I need fonts for watermarking with Text 24 | RUN apk --no-cache add msttcorefonts-installer fontconfig && \ 25 | update-ms-fonts && \ 26 | fc-cache -f 27 | 28 | # ENTRYPOINT ["php"] -------------------------------------------------------------------------------- /docker/php_5.6/php-ini-overrides.ini: -------------------------------------------------------------------------------- 1 | max_execution_time = 120 2 | -------------------------------------------------------------------------------- /docker/php_8.x/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8-cli-alpine 2 | 3 | COPY . /usr/app 4 | WORKDIR /usr/app 5 | 6 | ADD php-ini-overrides.ini /usr/local/etc/php/conf.d/overrides.ini 7 | 8 | # Install required tools 9 | RUN apk add --update --no-cache sqlite-dev rsync imagemagick bash pngcrush optipng=0.7.7-r0 10 | 11 | # Install PHP extensions 12 | # Available extensions: 13 | # bcmath bz2 calendar ctype curl dba dom enchant exif fileinfo filter ftp gd gettext gmp hash iconv 14 | # imap interbase intl json ldap mbstring mcrypt mssql mysql mysqli oci8 odbc opcache pcntl pdo 15 | # pdo_dblib pdo_firebird pdo_mysql pdo_oci pdo_odbc pdo_pgsql pdo_sqlite pgsql phar posix pspell 16 | # readline recode reflection session shmop simplexml snmp soap sockets spl standard sybase_ct sysvmsg 17 | # sysvsem sysvshm tidy tokenizer wddx xml xmlreader xmlrpc xmlwriter xsl zip 18 | RUN docker-php-ext-install -j$(nproc) pdo_mysql pdo_sqlite 19 | 20 | # Install composer globally 21 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 22 | 23 | # I need fonts for watermarking with Text 24 | RUN apk --no-cache add msttcorefonts-installer fontconfig && \ 25 | update-ms-fonts && \ 26 | fc-cache -f 27 | 28 | 29 | # ENTRYPOINT ["php"] -------------------------------------------------------------------------------- /docker/php_8.x/php-ini-overrides.ini: -------------------------------------------------------------------------------- 1 | max_execution_time = 120 2 | -------------------------------------------------------------------------------- /examples/example_img.php: -------------------------------------------------------------------------------- 1 | withText("ajaxray.com") 13 | ->setFont('Arial') 14 | ->setFontSize(36) 15 | ->setOpacity(.4) 16 | ->setRotate(330) 17 | ->setOffset(-80, 200) 18 | ->setPosition(Watermark::POSITION_RIGHT) 19 | ->write(__DIR__.'/img/result_simple.jpg'); 20 | 21 | // Watermarking Tiled/ text 22 | $watermark->setTiled() 23 | ->setTileSize(200, 200) 24 | ->setFontSize(24) 25 | ->setRotate(330) 26 | ->setOffset(60, 0) 27 | ->write(__DIR__ . '/img/result_tiled.jpg'); 28 | 29 | // Watermarking with image 30 | $imgMark = new Watermark(__DIR__ . '/img/poster.jpg'); 31 | $imgMark->withImage(__DIR__ . '/img/php.png') 32 | ->setPosition(Watermark::POSITION_BOTTOM_RIGHT) 33 | ->setOffset(50, 50) 34 | ->setOpacity(.3) 35 | ->setStyle(Watermark::STYLE_IMG_DISSOLVE) 36 | ->write( __DIR__ . '/img/result_logo.jpg'); 37 | -------------------------------------------------------------------------------- /examples/example_pdf.php: -------------------------------------------------------------------------------- 1 | withText($text) 12 | ->setFont('Arial') 13 | ->setFontSize(18) 14 | ->setRotate(345) 15 | ->setOffset(20, 60) 16 | ->setPosition(Watermark::POSITION_BOTTOM_RIGHT) 17 | ->write(__DIR__ . '/pdf/result_text.pdf'); 18 | 19 | // Watermarking with image 20 | $watermark->withImage(__DIR__ . '/img/php.png') 21 | ->setPosition(Watermark::POSITION_TOP_RIGHT) 22 | ->setOffset(50, 50) 23 | ->setOpacity(.5) 24 | ->write( __DIR__ . '/pdf/result_img.pdf'); 25 | -------------------------------------------------------------------------------- /examples/img/_result_simple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajaxray/php-watermark/c713ef4999062aebdf6eb66a54e6736afa094c30/examples/img/_result_simple.jpg -------------------------------------------------------------------------------- /examples/img/php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajaxray/php-watermark/c713ef4999062aebdf6eb66a54e6736afa094c30/examples/img/php.png -------------------------------------------------------------------------------- /examples/img/poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajaxray/php-watermark/c713ef4999062aebdf6eb66a54e6736afa094c30/examples/img/poster.jpg -------------------------------------------------------------------------------- /examples/pdf/The_Man_In_The_Red_Underpants.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajaxray/php-watermark/c713ef4999062aebdf6eb66a54e6736afa094c30/examples/pdf/The_Man_In_The_Red_Underpants.pdf -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tests/Ajaxray/PHPWatermark/ 7 | 8 | 9 | 10 | 11 | 12 | vendor/ 13 | 14 | 15 | src/Ajaxray/PHPWatermark/ 16 | 17 | 18 | 19 | 20 | 21 | tests/Ajaxray/TestUtils/ 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Ajaxray/PHPWatermark/CommandBuilders/AbstractCommandBuilder.php: -------------------------------------------------------------------------------- 1 | source = $source; 15 | 16 | (new RequirementsChecker())->ensureImagemagickInstallation(); 17 | } 18 | 19 | protected function getSource(): string 20 | { 21 | return escapeshellarg($this->source); 22 | } 23 | 24 | protected function prepareContext($output, array $options): array 25 | { 26 | $this->options = $options; 27 | 28 | return array($this->getSource(), escapeshellarg($output)); 29 | } 30 | 31 | protected function getAnchor(): string 32 | { 33 | return 'gravity ' . $this->options['position']; 34 | } 35 | 36 | protected function getOffset(): array 37 | { 38 | return [$this->options['offsetX'], $this->options['offsetY']]; 39 | } 40 | 41 | protected function getStyle(): int 42 | { 43 | return $this->options['style']; 44 | } 45 | 46 | protected function isTiled(): bool 47 | { 48 | return $this->options['tiled']; 49 | } 50 | 51 | protected function getTextTileSize(): string 52 | { 53 | return "-size " . implode('x', $this->options['tileSize']); 54 | } 55 | 56 | protected function getFont(): string 57 | { 58 | return '-pointsize ' . intval($this->options['fontSize']) . 59 | ' -font ' . escapeshellarg($this->options['font']); 60 | } 61 | 62 | protected function getDuelTextOffset(): array 63 | { 64 | $offset = $this->getOffset(); 65 | 66 | return [ 67 | "{$offset[0]},{$offset[1]}", 68 | ($offset[0] + 1) . ',' . ($offset[1] + 1), 69 | ]; 70 | } 71 | 72 | protected function getImageOffset(): string 73 | { 74 | $offsetArr = $this->getOffset(); 75 | 76 | return "geometry +{$offsetArr[0]}+{$offsetArr[1]}"; 77 | } 78 | 79 | protected function getOpacity(): float 80 | { 81 | return $this->options['opacity']; 82 | } 83 | 84 | protected function getTile(): string 85 | { 86 | return empty($this->isTiled()) ? '' : '-tile'; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Ajaxray/PHPWatermark/CommandBuilders/CommandBuilderFactory.php: -------------------------------------------------------------------------------- 1 | prepareContext($output, $options); 13 | $marker = escapeshellarg($markerImage); 14 | 15 | $anchor = $this->getAnchor(); 16 | $offset = $this->getImageOffset(); 17 | 18 | $tile = $this->getTile(); 19 | $opacity = $this->getImageOpacity(); 20 | 21 | return "composite -$anchor -$offset -$opacity $tile $marker $source $destination"; 22 | } 23 | 24 | /** @inheritDoc */ 25 | public function getTextMarkCommand(string $text, string $output, array $options): string 26 | { 27 | list($source, $destination) = $this->prepareContext($output, $options); 28 | $text = escapeshellarg($text); 29 | 30 | $anchor = $this->getAnchor(); 31 | $rotate = $this->getRotate(); 32 | $font = $this->getFont(); 33 | 34 | list($light, $dark) = $this->getDuelTextColor(); 35 | list($offsetLight, $offsetDark) = $this->getDuelTextOffset(); 36 | 37 | $draw = " -draw \"$rotate $anchor $light text $offsetLight $text $dark text $offsetDark $text\" "; 38 | 39 | if ($this->isTiled()) { 40 | $size = $this->getTextTileSize(); 41 | $command = "convert $size xc:none $font -$anchor $draw miff:- "; 42 | $command .= " | composite -tile - $source $destination"; 43 | } else { 44 | $command = "convert $source $font $draw $destination"; 45 | } 46 | 47 | return $command; 48 | } 49 | 50 | protected function getDuelTextColor(): array 51 | { 52 | return [ 53 | "fill \"rgba\\(255,255,255,{$this->getOpacity()}\\)\"", 54 | "fill \"rgba\\(0,0,0,{$this->getOpacity()}\\)\"", 55 | ]; 56 | } 57 | 58 | protected function getRotate(): string 59 | { 60 | return empty($this->options['rotate']) ? '' : "rotate {$this->options['rotate']}"; 61 | } 62 | 63 | protected function getImageOpacity(): string 64 | { 65 | $strategy = (Watermark::STYLE_IMG_COLORLESS == $this->options['style']) ? 'watermark' : 'dissolve'; 66 | 67 | return "$strategy ". ($this->options['opacity'] * 100) .'%'; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Ajaxray/PHPWatermark/CommandBuilders/PDFCommandBuilder.php: -------------------------------------------------------------------------------- 1 | prepareContext($output, $options); 11 | $marker = escapeshellarg($markerImage); 12 | 13 | $opacity = $this->getMarkerOpacity(); 14 | $anchor = $this->getAnchor(); 15 | $offset = $this->getImageOffset(); 16 | 17 | return sprintf( 18 | "convert %s %s miff:- | convert -density 100 %s null: - -%s -%s -quality 100 -compose multiply -layers composite %s", 19 | $marker, 20 | $opacity, 21 | $source, 22 | $anchor, 23 | $offset, 24 | $destination 25 | ); 26 | } 27 | 28 | /** @inheritDoc */ 29 | public function getTextMarkCommand(string $text, string $output, array $options): string 30 | { 31 | list($source, $destination) = $this->prepareContext($output, $options); 32 | $text = escapeshellarg($text); 33 | 34 | $anchor = $this->getAnchor(); 35 | $rotate = $this->getRotate(); 36 | $font = $this->getFont(); 37 | 38 | list($light, $dark) = $this->getDuelTextColor(); 39 | list($offsetLight, $offsetDark) = $this->getDuelTextOffset(); 40 | 41 | return sprintf( 42 | "convert %s -%s -quality 100 -density 100 %s -%s -annotate %s%s %s -%s -annotate %s%s %s %s", 43 | $source, 44 | $anchor, 45 | $font, 46 | $light, 47 | $rotate, 48 | $offsetLight, 49 | $text, 50 | $dark, 51 | $rotate, 52 | $offsetDark, 53 | $text, 54 | $destination 55 | ); 56 | } 57 | 58 | private function getMarkerOpacity(): string 59 | { 60 | $opacity = $this->getOpacity() * 100; 61 | 62 | return "-alpha set -channel A -evaluate set {$opacity}%"; 63 | } 64 | 65 | protected function getDuelTextOffset(): array 66 | { 67 | $offset = $this->getOffset(); 68 | 69 | return [ 70 | "+{$offset[0]}+{$offset[1]}", 71 | '+'.($offset[0] + 1) .'+'. ($offset[1] + 1), 72 | ]; 73 | } 74 | 75 | protected function getRotate(): string 76 | { 77 | return empty($this->options['rotate']) ? '' : "{$this->options['rotate']}x{$this->options['rotate']}"; 78 | } 79 | 80 | protected function getDuelTextColor(): array 81 | { 82 | return [ 83 | "fill \"rgba(255,255,255,{$this->getOpacity()})\"", 84 | "fill \"rgba(0,0,0,{$this->getOpacity()})\"", 85 | ]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Ajaxray/PHPWatermark/CommandBuilders/WatermarkCommandBuilderInterface.php: -------------------------------------------------------------------------------- 1 | 'Center', 38 | 'offsetX' => 0, 39 | 'offsetY' => 0, 40 | 'tiled' => false, 41 | 'tileSize' => [100, 100], 42 | 'font' => 'Arial', 43 | 'fontSize' => 24, 44 | 'opacity' => 0.3, 45 | 'rotate' => 0, 46 | 'style' => 1, // STYLE_IMG_DISSOLVE or STYLE_TEXT_BEVEL 47 | ]; 48 | 49 | public function __construct(string $source) 50 | { 51 | $this->ensureExists($source); 52 | 53 | $this->source = $source; 54 | $this->commandBuilder = CommandBuilderFactory::getCommandBuilder($source); 55 | } 56 | 57 | public function withText(string $text): self 58 | { 59 | $this->marker = $text; 60 | $this->markerType = self::MARKER_TEXT; 61 | 62 | return $this; 63 | } 64 | 65 | public function withImage(string $imagePath): self 66 | { 67 | $this->ensureExists($imagePath); 68 | 69 | $this->marker = $imagePath; 70 | $this->markerType = self::MARKER_IMG; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Make the executable ImageMagick command 77 | */ 78 | public function getCommand(?string $outputPath = null): string 79 | { 80 | $destination = $outputPath ?? $this->source; 81 | 82 | $this->ensureWritable($outputPath ? dirname($destination) : $destination); 83 | 84 | if ($this->markerType === self::MARKER_IMG) { 85 | return $this->commandBuilder->getImageMarkCommand($this->marker, $destination, $this->options); 86 | } 87 | 88 | if ($this->markerType === self::MARKER_TEXT) { 89 | return $this->commandBuilder->getTextMarkCommand($this->marker, $destination, $this->options); 90 | } 91 | 92 | throw new \LogicException("Unknown marker type set: {$this->markerType}."); 93 | } 94 | 95 | /** 96 | * Write the output image 97 | */ 98 | public function write(?string $outputPath = null): bool 99 | { 100 | $output = $returnCode = null; 101 | exec($this->getCommand($outputPath), $output, $returnCode); 102 | 103 | return empty($output) && $returnCode === 0; 104 | } 105 | 106 | public function setPosition(string $position): self 107 | { 108 | if (! in_array($position, $this->supportedPositionList())) { 109 | throw new \InvalidArgumentException("Position $position is not supported! Use Watermark::POSITION_* constants."); 110 | } 111 | 112 | $this->options['position'] = $position; 113 | 114 | return $this; 115 | } 116 | 117 | public function setOffset(int $offsetX, int $offsetY): self 118 | { 119 | $this->options['offsetX'] = $offsetX; 120 | $this->options['offsetY'] = $offsetY; 121 | 122 | return $this; 123 | } 124 | 125 | public function setStyle(int $style): self 126 | { 127 | $this->options['style'] = $style; 128 | 129 | return $this; 130 | } 131 | 132 | public function setTiled(bool $tiled = true): self 133 | { 134 | $this->options['tiled'] = $tiled; 135 | 136 | return $this; 137 | } 138 | 139 | public function setTileSize(int $width, int $height): self 140 | { 141 | $this->options['tileSize'] = [$width, $height]; 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * Font name should be one of the list displayed by `convert -list font` command 148 | */ 149 | public function setFont(string $font): self 150 | { 151 | $this->options['font'] = $font; 152 | 153 | return $this; 154 | } 155 | 156 | public function setFontSize(int $fontSize): self 157 | { 158 | $this->options['fontSize'] = $fontSize; 159 | 160 | return $this; 161 | } 162 | 163 | /** 164 | * @param float $opacity Between .1 (very transparent) to .9 (almost opaque). 165 | */ 166 | public function setOpacity(float $opacity): self 167 | { 168 | if ($opacity < 0 || $opacity > 1) { 169 | throw new \InvalidArgumentException('Opacity should be float between 0 to 1!'); 170 | } 171 | 172 | $this->options['opacity'] = $opacity; 173 | 174 | return $this; 175 | } 176 | 177 | /** 178 | * @param int $rotate Degree of rotation 179 | */ 180 | public function setRotate(int $rotate): self 181 | { 182 | $this->options['rotate'] = abs($rotate); 183 | 184 | return $this; 185 | } 186 | 187 | final public function supportedPositionList(): array 188 | { 189 | return [ 190 | self::POSITION_TOP_LEFT, 191 | self::POSITION_TOP, 192 | self::POSITION_TOP_RIGHT, 193 | self::POSITION_RIGHT, 194 | self::POSITION_CENTER, 195 | self::POSITION_LEFT, 196 | self::POSITION_BOTTOM_LEFT, 197 | self::POSITION_BOTTOM, 198 | self::POSITION_BOTTOM_RIGHT, 199 | ]; 200 | } 201 | 202 | private function ensureExists(string $filePath): void 203 | { 204 | if (! file_exists($filePath)) { 205 | throw new \InvalidArgumentException("The specified file $filePath was not found!"); 206 | } 207 | } 208 | 209 | private function ensureWritable(string $dirPath): void 210 | { 211 | if (! is_writable($dirPath)) { 212 | throw new \InvalidArgumentException("The specified destination $dirPath is not writable!"); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /tests/Ajaxray/PHPWatermark/Tests/CommandBuilders/ImageCommandBuilderTest.php: -------------------------------------------------------------------------------- 1 | 'Center', 15 | 'offsetX' => 0, 16 | 'offsetY' => 0, 17 | 'tiled' => false, 18 | 'tileSize' => [100, 100], 19 | 'font' => 'Arial', 20 | 'fontSize' => 24, 21 | 'opacity' => 0.3, 22 | 'rotate' => 0, 23 | 'style' => 1, // IMG_STYLE_DISSOLVE or TEXT_STYLE_BEVEL 24 | ]; 25 | 26 | private string $cmdText = 'convert \'path/source.jpg\' -pointsize 24 -font \'Arial\' -draw " gravity Center fill "rgba\(255,255,255,0.3\)" text 0,0 \'ajaxray.com\' fill "rgba\(0,0,0,0.3\)" text 1,1 \'ajaxray.com\'" \'path/output.jpg\''; 27 | private string $cmdTiledText = 'convert -size 100x100 xc:none -pointsize 24 -font \'Arial\' -gravity Center -draw " gravity Center fill "rgba\(255,255,255,0.3\)" text 0,0 \'ajaxray.com\' fill "rgba\(0,0,0,0.3\)" text 1,1 \'ajaxray.com\'" miff:- | composite -tile - \'path/source.jpg\' \'path/output.jpg\''; 28 | private string $cmdImg = "composite -gravity Center -geometry +0+0 -dissolve 30% 'path/logo.png' 'path/source.jpg' 'path/output.jpg'"; 29 | 30 | private ImageCommandBuilder $builder; 31 | 32 | /** 33 | * ImageCommandBuilderTest constructor. 34 | */ 35 | public function __construct() 36 | { 37 | parent::__construct(); 38 | 39 | $this->builder = new ImageCommandBuilder('path/source.jpg'); 40 | } 41 | 42 | public function testBasicWatermarkingWithText(): void 43 | { 44 | $execCommand = $this->getTxtCommandWithOption([]); 45 | $this->assertEquals($this->cmdText, $execCommand); 46 | } 47 | 48 | public function testWatermarkingWithChangingTextLocation(): void 49 | { 50 | $execCommand = $this->getTxtCommandWithOption([ 51 | 'position' => Watermark::POSITION_BOTTOM_LEFT, 52 | 'offsetX' => 10, 53 | 'offsetY' => 15, 54 | ]); 55 | $expected = str_replace(['gravity Center', 'text 0,0', 'text 1,1'], ['gravity SouthWest', 'text 10,15', 'text 11,16'], $this->cmdText); 56 | $this->assertEquals($expected, $execCommand); 57 | } 58 | 59 | public function testWatermarkingWithChangingTextOpacity(): void 60 | { 61 | $execCommand = $this->getTxtCommandWithOption(['opacity' => .7]); 62 | $expected = str_replace(['255,255,255,0.3', '0,0,0,0.3'], ['255,255,255,0.7', '0,0,0,0.7'], $this->cmdText); 63 | $this->assertEquals($expected, $execCommand); 64 | } 65 | 66 | public function testWatermarkingWithChangingTextRotation(): void 67 | { 68 | $execCommand = $this->getTxtCommandWithOption(['rotate' => 15]); 69 | $expected = str_replace('-draw "', '-draw "rotate 15', $this->cmdText); 70 | $this->assertEquals($expected, $execCommand); 71 | } 72 | 73 | public function testWatermarkingWithChangingTextFont(): void 74 | { 75 | $execCommand = $this->getTxtCommandWithOption(['font' => 'sans-serif', 'fontSize' => 36]); 76 | $expected = str_replace('-pointsize 24 -font \'Arial\'', '-pointsize 36 -font \'sans-serif\'', $this->cmdText); 77 | $this->assertEquals($expected, $execCommand); 78 | } 79 | 80 | public function testWatermarkingWithTiledText(): void 81 | { 82 | $execCommand = $this->getTxtCommandWithOption(['tiled' => true]); 83 | $this->assertEquals($this->cmdTiledText, $execCommand); 84 | } 85 | 86 | public function testBasicWatermarkingWithImage(): void 87 | { 88 | $execCommand = $this->getImgCommandWithOption([]); 89 | $this->assertEquals($this->cmdImg, $execCommand); 90 | } 91 | 92 | public function testWatermarkingWithChangingImageLocation(): void 93 | { 94 | $execCommand = $this->getImgCommandWithOption([ 95 | 'position' => Watermark::POSITION_TOP_RIGHT, 96 | 'offsetX' => 100, 97 | 'offsetY' => 150, 98 | ]); 99 | $expected = str_replace(['gravity Center', '+0+0'], ['gravity NorthEast', '+100+150'], $this->cmdImg); 100 | $this->assertEquals($expected, $execCommand); 101 | } 102 | 103 | public function testWatermarkingWithChangingImageOpacity(): void 104 | { 105 | $execCommand = $this->getImgCommandWithOption(['opacity' => .5]); 106 | $expected = str_replace('30%', '50%', $this->cmdImg); 107 | $this->assertEquals($expected, $execCommand); 108 | } 109 | 110 | public function testWatermarkingWithChangingImageStyle(): void 111 | { 112 | $execCommand = $this->getImgCommandWithOption(['style' => Watermark::STYLE_IMG_COLORLESS]); 113 | $expected = str_replace('-dissolve', '-watermark', $this->cmdImg); 114 | $this->assertEquals($expected, $execCommand); 115 | } 116 | 117 | private function getTxtCommandWithOption(array $options): string 118 | { 119 | $options = array_merge($this->options, $options); 120 | return $this->builder->getTextMarkCommand('ajaxray.com', 'path/output.jpg', $options); 121 | } 122 | 123 | private function getImgCommandWithOption(array $options): string 124 | { 125 | $options = array_merge($this->options, $options); 126 | return $this->builder->getImageMarkCommand('path/logo.png', 'path/output.jpg', $options); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/Ajaxray/PHPWatermark/Tests/CommandBuilders/PDFCommandBuilderTest.php: -------------------------------------------------------------------------------- 1 | 'Center', 15 | 'offsetX' => 0, 16 | 'offsetY' => 0, 17 | 'tiled' => false, 18 | 'tileSize' => [100, 100], 19 | 'font' => 'Arial', 20 | 'fontSize' => 24, 21 | 'opacity' => 0.3, 22 | 'rotate' => 0, 23 | 'style' => 1, // IMG_STYLE_DISSOLVE or TEXT_STYLE_BEVEL 24 | ]; 25 | 26 | private string $cmdText = 'convert \'path/name.pdf\' -gravity Center -quality 100 -density 100 -pointsize 24 -font \'Arial\' -fill "rgba(255,255,255,0.3)" -annotate +0+0 \'LIFE CHANGING BOOK\' -fill "rgba(0,0,0,0.3)" -annotate +1+1 \'LIFE CHANGING BOOK\' \'path/output.pdf\''; 27 | private string $cmdImg = "convert 'path/logo.png' -alpha set -channel A -evaluate set 30% miff:- | convert -density 100 'path/name.pdf' null: - -gravity Center -geometry +0+0 -quality 100 -compose multiply -layers composite 'path/output.pdf'"; 28 | 29 | private PDFCommandBuilder $builder; 30 | 31 | /** 32 | * PDFCommandBuilderTest constructor. 33 | */ 34 | public function __construct() 35 | { 36 | parent::__construct(); 37 | 38 | $this->builder = new PDFCommandBuilder('path/name.pdf'); 39 | } 40 | 41 | public function testTextWatermarking(): void 42 | { 43 | $execCommand = $this->getTxtCommandWithOption([]); 44 | $this->assertEquals($this->cmdText, $execCommand); 45 | } 46 | 47 | public function testTextWatermarkingWithRotate(): void 48 | { 49 | $execCommand = $this->getTxtCommandWithOption(['rotate' => 30]); 50 | $this->assertEquals(str_replace('-annotate ', '-annotate 30x30', $this->cmdText), $execCommand); 51 | } 52 | 53 | public function testTextWatermarkingConfigureOpacity(): void 54 | { 55 | $execCommand = $this->getTxtCommandWithOption(['opacity' => .6]); 56 | $this->assertEquals(str_replace(',0.3)', ',0.6)', $this->cmdText), $execCommand); 57 | } 58 | 59 | public function testTextWatermarkingConfigureFont(): void 60 | { 61 | $execCommand = $this->getTxtCommandWithOption(['fontSize' => 48, 'font' => 'monospace']); 62 | $this->assertEquals(str_replace(['24', 'Arial'], ['48', 'monospace'], $this->cmdText), $execCommand); 63 | } 64 | 65 | public function testTextWatermarkingConfigurePosition(): void 66 | { 67 | $execCommand = $this->getTxtCommandWithOption([ 68 | 'position' => Watermark::POSITION_BOTTOM_RIGHT, 69 | 'offsetX' => '220', 70 | 'offsetY' => '50', 71 | ]); 72 | $this->assertEquals(str_replace(['Center', '+0+0', '+1+1'], ['SouthEast', '+220+50', '+221+51'], $this->cmdText), $execCommand); 73 | } 74 | 75 | public function testImageWatermarkingBasic(): void 76 | { 77 | $execCommand = $this->builder->getImageMarkCommand('path/logo.png', 'path/output.pdf', $this->options); 78 | $this->assertEquals($this->cmdImg, $execCommand); 79 | } 80 | 81 | public function testImageWatermarkingWithLocationChange(): void 82 | { 83 | $execCommand = $this->getImgCommandWithOption([ 84 | 'position' => Watermark::POSITION_BOTTOM_RIGHT, 85 | 'offsetX' => 50, 86 | 'offsetY' => 100, 87 | ]); 88 | $expected = str_replace('-gravity Center -geometry +0+0', '-gravity SouthEast -geometry +50+100', $this->cmdImg); 89 | $this->assertEquals($expected, $execCommand); 90 | } 91 | 92 | public function testImageWatermarkingWithOpacityChange(): void 93 | { 94 | $execCommand = $this->getImgCommandWithOption(['opacity' => .7]); 95 | $expected = str_replace('-evaluate set 30%', '-evaluate set 70%', $this->cmdImg); 96 | 97 | $this->assertEquals($expected, $execCommand); 98 | } 99 | 100 | private function getTxtCommandWithOption(array $options): string 101 | { 102 | $options = array_merge($this->options, $options); 103 | return $this->builder->getTextMarkCommand('LIFE CHANGING BOOK', 'path/output.pdf', $options); 104 | } 105 | 106 | private function getImgCommandWithOption(array $options): string 107 | { 108 | $options = array_merge($this->options, $options); 109 | return $this->builder->getImageMarkCommand('path/logo.png', 'path/output.pdf', $options); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/Ajaxray/PHPWatermark/Tests/WatermarkTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ImageCommandBuilder::class, $this->invokeProperty($watermark, 'commandBuilder')); 37 | 38 | $watermark = new Watermark('path/to/image/file.jpg'); 39 | $this->assertInstanceOf(ImageCommandBuilder::class, $this->invokeProperty($watermark, 'commandBuilder')); 40 | 41 | $watermark = new Watermark('path/to/file.png'); 42 | $this->assertInstanceOf(ImageCommandBuilder::class, $this->invokeProperty($watermark, 'commandBuilder')); 43 | } 44 | 45 | public function testLoadingPDFCommandBuilderForPdfs(): void 46 | { 47 | $watermark = new Watermark('path/to/pdf/file.pdf'); 48 | $this->assertInstanceOf(PDFCommandBuilder::class, $this->invokeProperty($watermark, 'commandBuilder')); 49 | 50 | $watermark = new Watermark('path/to/x-pdf/file.pdf'); 51 | $this->assertInstanceOf(PDFCommandBuilder::class, $this->invokeProperty($watermark, 'commandBuilder')); 52 | } 53 | 54 | public function testThrowsExceptionForUnsupportedSourceTypes(): void 55 | { 56 | $this->expectException('\InvalidArgumentException'); 57 | $this->expectExceptionMessage('The source file type no-pdf/no-image is not supported'); 58 | 59 | new Watermark('path/to/test.html'); 60 | } 61 | 62 | public function testWatermarkWithTextExecutesShellCommand(): void 63 | { 64 | global $lastExecCommand; 65 | $watermark = new Watermark('path/to/file.png'); 66 | $watermark->withText('CONFIDENTIAL') 67 | ->write('output.jpg'); 68 | 69 | $this->assertStringContainsString('convert', $lastExecCommand); 70 | $this->assertStringContainsString('CONFIDENTIAL', $lastExecCommand); 71 | $this->assertStringContainsString('path/to/file.png', $lastExecCommand); 72 | $this->assertStringContainsString('output.jpg', $lastExecCommand); 73 | } 74 | 75 | public function testWatermarkWithImageExecutesShellCommand(): void 76 | { 77 | global $lastExecCommand; 78 | $watermark = new Watermark('path/to/file.jpg'); 79 | $watermark->withImage('path/company-logo.png') 80 | ->write('output.jpg'); 81 | 82 | $this->assertStringContainsString('composite', $lastExecCommand); 83 | $this->assertStringContainsString('path/company-logo.png', $lastExecCommand); 84 | $this->assertStringContainsString('path/to/file.jpg', $lastExecCommand); 85 | $this->assertStringContainsString('output.jpg', $lastExecCommand); 86 | } 87 | 88 | public function testThrowsExceptionOnInvalidPosition(): void 89 | { 90 | $this->expectException('\InvalidArgumentException'); 91 | $this->expectExceptionMessage('Position SOMEWHERE_ELSE is not supported! Use Watermark::POSITION_* constants.'); 92 | 93 | $watermark = new Watermark('path/to/source.jpg'); 94 | $watermark->setPosition('SOMEWHERE_ELSE'); 95 | } 96 | 97 | public function testThrowsExceptionIfOpacityIsNotBetween0To1(): void 98 | { 99 | $this->expectException('\InvalidArgumentException'); 100 | $this->expectExceptionMessage('Opacity should be float between 0 to 1!'); 101 | 102 | $watermark = new Watermark('path/to/file.jpg'); 103 | $watermark->setOpacity(2); 104 | } 105 | 106 | public function testRotationCastingToAbsoluteInt(): void 107 | { 108 | $watermark = new Watermark('path/to/file.jpg'); 109 | $watermark->setRotate(5); 110 | 111 | $options = $this->invokeProperty($watermark, 'options'); 112 | $this->assertTrue(is_int($options['rotate'])); 113 | $this->assertEquals(5, $options['rotate']); 114 | } 115 | 116 | public function testThrowsExceptionIfSourceNotFound(): void 117 | { 118 | global $mockGlobalFunctions; 119 | $mockGlobalFunctions = false; 120 | 121 | $this->expectException('\InvalidArgumentException'); 122 | $this->expectExceptionMessage('The specified file path/to/file.jpg was not found!'); 123 | 124 | new Watermark('path/to/file.jpg'); 125 | } 126 | 127 | public function testThrowsExceptionIfMarkerImageNotFound(): void 128 | { 129 | global $mockGlobalFunctions; 130 | 131 | $this->expectException('\InvalidArgumentException'); 132 | $this->expectExceptionMessage('The specified file non/existing/marker.png was not found!'); 133 | 134 | $watermark = new Watermark('path/to/file.jpg'); 135 | 136 | $mockGlobalFunctions = false; 137 | $watermark->withImage('non/existing/marker.png'); 138 | } 139 | 140 | public function testThrowsExceptionIfDestinationNotWritable(): void 141 | { 142 | global $mockGlobalFunctions; 143 | 144 | $this->expectException('\InvalidArgumentException'); 145 | $this->expectExceptionMessage('The specified destination non/existing is not writable!'); 146 | 147 | $watermark = new Watermark('path/to/file.jpg'); 148 | 149 | $mockGlobalFunctions = false; 150 | $watermark->withText('text') 151 | ->write('non/existing/output.jpg'); 152 | } 153 | 154 | public function testThrowsExceptionIfSourceNotImageOrPDF(): void 155 | { 156 | global $mockGlobalFunctions; 157 | $mockGlobalFunctions = false; 158 | 159 | $this->expectException('\InvalidArgumentException'); 160 | $this->expectExceptionMessage('The source file type text/x-php is not supported.'); 161 | 162 | new Watermark(__FILE__); 163 | } 164 | 165 | public function testGetCommandReturnsStringCommand(): void 166 | { 167 | global $lastExecCommand; 168 | 169 | $watermark = new Watermark('path/to/file.jpg'); 170 | $command = $watermark->withImage('path/to/logo.png') 171 | ->getCommand(); 172 | 173 | $this->assertStringContainsString('composit', $command); 174 | $this->assertStringContainsString('path/to/logo.png', $command); 175 | } 176 | 177 | public function testSetPositionOnPositionList(): void 178 | { 179 | $watermark = new Watermark('path/to/file.jpg'); 180 | $setPosition = $watermark->setPosition(Watermark::POSITION_CENTER); 181 | $options = $this->invokeProperty($watermark, 'options'); 182 | 183 | $this->assertInstanceOf(Watermark::class, $setPosition); 184 | $this->assertContains(Watermark::POSITION_CENTER, $options); 185 | } 186 | 187 | public function testSetStyle(): void 188 | { 189 | $watermark = new Watermark('path/to/file.jpg'); 190 | $setStyle = $watermark->setStyle(Watermark::STYLE_IMG_COLORLESS); 191 | $options = $this->invokeProperty($watermark, 'options'); 192 | 193 | $this->assertInstanceOf(Watermark::class, $setStyle); 194 | $this->assertContains(Watermark::STYLE_IMG_COLORLESS, $options); 195 | } 196 | 197 | public function testSetTileSize(): void 198 | { 199 | $watermark = new Watermark('path/to/file.jpg'); 200 | $setStyle = $watermark->setTileSize(200, 150); 201 | $options = $this->invokeProperty($watermark, 'options'); 202 | 203 | $this->assertInstanceOf(Watermark::class, $setStyle); 204 | $this->assertContains([200, 150], $options); 205 | } 206 | 207 | public function testSetFont(): void 208 | { 209 | $watermark = new Watermark('path/to/file.jpg'); 210 | $setFont = $watermark->setFont('Arial'); 211 | $options = $this->invokeProperty($watermark, 'options'); 212 | 213 | $this->assertInstanceOf(Watermark::class, $setFont); 214 | $this->assertContains('Arial', $options); 215 | } 216 | 217 | public function testSetFontSize(): void 218 | { 219 | $watermark = new Watermark('path/to/file.jpg'); 220 | $setFontSize = $watermark->setFontSize(20); 221 | $options = $this->invokeProperty($watermark, 'options'); 222 | 223 | $this->assertInstanceOf(Watermark::class, $setFontSize); 224 | $this->assertContains(20, $options); 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /tests/Ajaxray/TestUtils/NonPublicAccess.php: -------------------------------------------------------------------------------- 1 | getMethod($methodName); 22 | $method->setAccessible(true); 23 | 24 | return $method->invokeArgs($object, $parameters); 25 | } 26 | 27 | /** 28 | * Access protected/private property of a class. 29 | * 30 | * @param object &$object Instantiated object that we will run method on. 31 | * @param $propName 32 | * @return mixed Object property. 33 | * 34 | */ 35 | public function invokeProperty(object &$object, string $propName): mixed 36 | { 37 | $reflection = new \ReflectionClass(get_class($object)); 38 | $prop = $reflection->getProperty($propName); 39 | $prop->setAccessible(true); 40 | 41 | return $prop->getValue($object); 42 | } 43 | } -------------------------------------------------------------------------------- /tests/Ajaxray/TestUtils/OverrideFunctions.php: -------------------------------------------------------------------------------- 1 |