diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2748a19 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +* text=auto +* eol=lf + +.git export-ignore +.gitattributes export-ignore +.gitignore export-ignore +CREDITS export-ignore +phpcs-ruleset.xml export-ignore +phpdoc.xml export-ignore +phpunit.xml export-ignore +.github export-ignore +examples export-ignore +tests export-ignore +CONTRIBUTING.md export-ignore +CHANGELOG.md export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cdc293e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: [ push, pull_request ] + +jobs: + CI: + name: ${{ matrix.php }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - php: '7.4' + - php: '8.0' + - php: '8.1' + - php: '8.2' + - php: '8.3' + - php: '8.4' + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: ffi.enable=true, zend.max_allowed_stack_size=-1 + extensions: ffi + tools: composer:v2 + coverage: none + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install libvips + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends libvips + + - name: Install composer dependencies + run: | + composer update --prefer-dist --no-interaction --no-progress --no-ansi + + - name: PHPUnit + run: composer test diff --git a/.gitignore b/.gitignore index a66d4dd..1cd2994 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ vendor composer.lock /docs *.swp +.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 582e779..0000000 --- a/.travis.yml +++ /dev/null @@ -1,59 +0,0 @@ -sudo: false - -language: php - -dist: trusty - -php: - - 7.0.13 - - 7.1 - -env: - global: - - VIPS_VERSION_MAJOR=8 - - VIPS_VERSION_MINOR=6 - - VIPS_VERSION_MICRO=0 - - PATH=$HOME/vips/bin:$PATH - - LD_LIBRARY_PATH=$HOME/vips/lib:$LD_LIBRARY_PATH - - PKG_CONFIG_PATH=$HOME/vips/lib/pkgconfig:$PKG_CONFIG_PATH - - PYTHONPATH=$HOME/vips/lib/python2.7/site-packages:$PYTHONPATH - - GI_TYPELIB_PATH=$HOME/vips/lib/girepository-1.0:$GI_TYPELIB_PATH - -cache: - apt: true - directories: - - $HOME/.composer/cache - - $HOME/vips - -addons: - apt: - packages: - - gobject-introspection - - libcfitsio3-dev - - libfftw3-dev - - libgif-dev - - libgs-dev - - libgsf-1-dev - - libmatio-dev - - libopenslide-dev - - liborc-0.4-dev - - libpango1.0-dev - - libpoppler-glib-dev - - libwebp-dev - -before_install: - - bash install-vips.sh - --disable-debug - --disable-dependency-tracking - --disable-introspection - --disable-static - --enable-gtk-doc-html=no - --enable-gtk-doc=no - --enable-pyvips8=no - --without-orc - --without-python - - yes '' | pecl install vips - -install: composer install --prefer-dist - -script: composer test diff --git a/API-1.0.0 b/API-1.0.0 deleted file mode 100644 index e69de29..0000000 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f1ab2f..b543c23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,198 @@ # Changelog -All notable changes to `:vips` will be documented in this file. + +All notable changes to `php-vips` will be documented in this file. + +## master + +## 2.5.0 - 2025-04-04 + +- add addLibraryPath() to let users set their libvips location [kleisauke] +- only test `max_allowed_stack_size` when we have to [kleisauke] + +## 2.4.1 - 2025-01-13 + +- fix php 8.4 compatibility [deluxetom] + +## 2.4.0 - 2024-04-09 + +- added `animate-image.php` example [jcupitt] +- added flags support [jcupitt] +- added `keep.php` example [jcupitt] +- added streaming examples [jcupitt] +- fix php 8.3 compatibility [uuf6429] +- better library finding [uuf6429] +- fix compat with libvips before 8.9 [allanvb] + +## 2.3.0 - 2023-09-26 + +- add getFields() to fetch an array of field names [jcupitt] + +## 2.2.0 - 2023-08-17 + +- improve FFI startup [West14, jcupitt] +- revise example use of composer [jcupitt] +- fix bandrank [axkirillov] +- fix FFI startup log error [ganicus] +- add Source, Target, SourceResource, TargetResource, SourceCustom, + TargetCustom [L3tum] +- add setProgress and progress example [jcupitt] +- add streaming-custom example [jcupitt] + +## 2.1.1 - 2022-11-13 + +- remove unused vips_error_buffer_copy() declaration to fix compatibility with + libvips before 8.9 [Waschnick] +- refactor callBase() for maintainability +- work around a php-ffi memory leak in getPspec() [levmv] +- work around a php-ffi memory leak in arrayType() +- better test for disabled ffi + +## 2.1.0 - 2022-10-11 + +- allow "-" as a name component separator [andrews05] +- split FFI into a separate class [kleisauke] +- improve finding of library binary [jcupitt] + +## 2.0.3 - 2022-07-04 + +- Fix on Windows [kleisauke] +- Fix 32-bit support [kleisauke] +- Code cleanups [kleisauke] + +## 2.0.2 - 2022-4-14 + +- Fix extra optional string args on file open + +## 2.0.1 - 2022-1-20 + +- Fix library name on macOS [andrefelipe] + +## 2.0.0 - 2022-1-20 + +Rewritten to use PHP FFI to call into the libvips library rather than a binary +extension. This means php-vips now requires php 7.4 or later. + +### Added +- `Interpolate` class + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Remove +- Nothing + +### Security +- Nothing + +### 1.0.9 - 2021-11-20 + +### Added +- update docs for libvips 8.12 + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Remove +- Nothing + +### Security +- Nothing + +## 1.0.8 - 2020-08-29 + +### Added +- allow type names as type params to Image::setType() -- fixes issue with GType + on 32-bit platforms + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Remove +- Nothing + +### Security +- Nothing + +## 1.0.7 - 2020-08-28 + +### Added +- use nullable types and void return type where possible + +### Deprecated +- requires php >= 7.1 + +### Fixed +- fix autodocs for non-static methods + +### Remove +- Nothing + +### Security +- Nothing + +## 1.0.6 - 2020-08-28 + +### Added +- Image::setType() +- Utils::typeFromName() +- Updated autodocs for libvips 8.10 + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Remove +- Nothing + +### Security +- Nothing + +## 1.0.5 - 2019-09-26 + +### Added +- writeToArray() [John Cupitt] + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Remove +- Nothing + +### Security +- Nothing + +## 1.0.4 - 2018-12-22 + +### Added +- polar() and rect() now work on non-complex images [John Cupitt] +- add crossPhase() [John Cupitt] +- update autodocs [John Cupitt] + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Remove +- Nothing + +### Security +- Nothing ## 1.0.3 - 2017-06-06 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 724f6ac..b092559 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Contributions are **welcome** and will be fully **credited**. -We accept contributions via Pull Requests on [Github](https://github.com/jcupitt/php-vips). +We accept contributions via Pull Requests on [Github](https://github.com/libvips/php-vips). ## Pull Requests diff --git a/README.md b/README.md index 14cdeae..e435a8e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # PHP binding for libvips -[![Build Status](https://travis-ci.org/jcupitt/php-vips.svg?branch=master)](https://travis-ci.org/jcupitt/php-vips) +[![CI](https://github.com/libvips/php-vips/workflows/CI/badge.svg)](https://github.com/libvips/php-vips/actions) -`php-vips` is a binding for [libvips](https://github.com/jcupitt/libvips) for -PHP 7. +`php-vips` is a binding for [libvips](https://github.com/libvips/libvips) 8.7 +and later that runs on PHP 7.4 and later. libvips is fast and needs little memory. The [`vips-php-bench`](https://github.com/jcupitt/php-vips-bench) repository @@ -17,46 +17,53 @@ image. When the pipe is connected to a destination, the whole pipeline executes at once and in parallel, streaming the image from source to destination in a set of small fragments. -This module builds upon the `vips` PHP extension: +### Install -https://github.com/jcupitt/php-vips-ext +You need to [install the libvips +library](https://www.libvips.org/install.html). It's in the linux +package managers, homebrew and MacPorts, and there are Windows binaries on +the vips website. For example, on Debian: -You'll need to install that first. It's tested on Linux and macOS --- -Windows would need some work, but should be possible. +``` +sudo apt-get install --no-install-recommends libvips42 +``` + +(`--no-install-recommends` stops Debian installing a *lot* of extra packages) -See the README there, but briefly: +Or macOS: -1. [Install the libvips library and - headers](https://jcupitt.github.io/libvips/install.html). It's in - the linux package managers, homebrew and MacPorts, and there are Windows - binaries on the vips website. For example, on Debian: +``` +brew install vips +``` - ``` - sudo apt-get install libvips-dev - ``` +You'll need to [enable FFI in your +PHP](https://www.php.net/manual/en/ffi.configuration.php), then add vips +to your `composer.json`: - Or macOS: +``` +"require": { + "jcupitt/vips" : "2.4.0" +} +``` - ``` - brew install vips - ``` +php-vips does not yet support preloading, so you need to enable FFI globally. +This has some security implications, since anyone who can run php on your +server can use it to call any native library they have access to. -2. Install the binary PHP extension: +Of course if attackers are running their own PHP code on your webserver you +are probably already toast, unfortunately. - ``` - pecl install vips - ``` +Finally, on php 8.3 and later you need to disable stack overflow +tests. php-vips executes FFI callbacks off the main thread and this confuses +those checks, at least in php 8.3.0. - You may need to add `extension=vips.so` or equivalent to `php.ini`, see the - output of pecl. +Add: -3. Add vips to your `composer.json`: +``` +zend.max_allowed_stack_size=-1 +``` - ``` - "require": { - "jcupitt/vips" : "1.0.2" - } - ``` +To your `php.ini`. ### Example @@ -66,6 +73,12 @@ See the README there, but briefly: require __DIR__ . '/vendor/autoload.php'; use Jcupitt\Vips; +// handy for Windows +Vips\FFI::addLibraryPath("C:/vips-dev-8.16/bin"); + +// check libvips version +echo 'libvips version: ' . Vips\Config::version() . PHP_EOL; + // fast thumbnail generator $image = Vips\Image::thumbnail('somefile.jpg', 128); $image->writeToFile('tiny.jpg'); @@ -85,7 +98,22 @@ $ ./try1.php ~/pics/k2.jpg x.tif ``` See `examples/`. We have a [complete set of formatted API -docs](https://jcupitt.github.io/php-vips/docs/classes/Jcupitt.Vips.Image.html). +docs](https://libvips.github.io/php-vips/classes/Jcupitt-Vips-Image.html). + + +### How it works + +php-vips uses [php-ffi](https://www.php.net/manual/en/book.ffi.php) to +call directly into the libvips binary. It introspects the library binary +and presents the methods it finds as members of the `Image` class. + +This means that the API you see depends on the version of libvips that +php-vips finds at runtime, and not on php-vips. php-vips documentation assumes +you are using the latest stable version of the libvips library. + +The previous php-vips version that relied on a binary extension +and not on php-ffi is still available and supported in [the 1.x +branch](https://github.com/libvips/php-vips/tree/1.x). ### Introduction to the API @@ -102,7 +130,7 @@ either 255 or the original image. Note that libvips operators always make new images, they don't modify existing images, so after the line above, `$image` is unchanged. -You use long, double, array and image as parameters. For example: +You can use long, double, array and image as parameters. For example: ```php $image = $image->add(2); @@ -126,7 +154,7 @@ to add two images. Or: $image = $image->add([[1, 2, 3], [4, 5, 6]]); ``` -To make a 2 x 3 image from the array, then add that image to the original. +To make a 3 x 2 image from the array, then add that image to the original. Almost all methods can take an extra final argument: an array of options. For example: @@ -135,7 +163,9 @@ For example: $image->writeToFile("fred.jpg", ["Q" => 90]); ``` -`php-vips` comes [with full API docs](https://jcupitt.github.io/php-vips/docs/classes/Jcupitt.Vips.Image.html). To regenerate these from your sources, type: +`php-vips` comes [with API +docs](https://libvips.github.io/php-vips/classes/Jcupitt-Vips-Image.html). +To regenerate these from your sources, type: ``` $ vendor/bin/phpdoc @@ -143,29 +173,17 @@ $ vendor/bin/phpdoc And look in `docs/`. -There are around 300 operations in the library, see the vips docs for an -introduction: +Unfortunatly, due to php-doc limitations, these do not list every option +to every operation. For a full API description you need to see the main +libvips documentation: -https://jcupitt.github.io/libvips/API/current - -### How it works - -The `vips` extension defines a simple but ugly way to call any libvips -operation from PHP. It uses libvips' own introspection facilities -and does not depend on anything else (so no gobject-introspection, -for example). It's a fairly short 1,600 lines of C. - -This module is a PHP layer over the ugly `vips` extension that -tries to make a nice interface for programmers. It uses `__call()` and -`__get()` to make all libvips operations appear as methods, and all -libvips properties as properties of the PHP `Vips\Image` class. +https://libvips.org/API/current ### Test and install ``` -$ phpcs --standard=PSR2 src -$ php ~/packages/php/composer.phar install -$ vendor/bin/phpunit +$ composer install +$ composer test $ vendor/bin/phpdoc ``` @@ -173,6 +191,5 @@ $ vendor/bin/phpdoc ``` $ cd src -$ ../examples/generate_phpdoc.rb +$ ../examples/generate_phpdoc.py ``` - diff --git a/RELEASE-1.0.2 b/RELEASE-1.0.2 deleted file mode 100644 index e69de29..0000000 diff --git a/composer.json b/composer.json index e0fde80..f007871 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "processing", "libvips" ], - "homepage": "https://github.com/jcupitt/php-vips", + "homepage": "https://github.com/libvips/php-vips", "license": "MIT", "authors": [ { @@ -17,15 +17,15 @@ } ], "require": { - "php": ">=7.0.11", - "ext-vips": ">=0.1.2", - "psr/log": "^1.0.1" + "php": ">=7.4", + "ext-ffi": "*", + "psr/log": "^1.1.3|^2.0|^3.0" }, "require-dev": { - "phpunit/phpunit": "^6.3", - "phpdocumentor/phpdocumentor" : "^2.9", - "jakub-onderka/php-parallel-lint": "^0.9.2", - "squizlabs/php_codesniffer": "3.*" + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpdocumentor/shim": "^3.3", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.7" }, "autoload": { "psr-4": { @@ -39,7 +39,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "scripts": { @@ -48,5 +48,12 @@ "phpunit", "phpcs --standard=phpcs-ruleset.xml ." ] + }, + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "allow-plugins": { + "phpdocumentor/shim": true + } } } diff --git a/examples/addconst.php b/examples/addconst.php index 9722a89..e9d330e 100755 --- a/examples/addconst.php +++ b/examples/addconst.php @@ -1,7 +1,7 @@ #!/usr/bin/env php 300, "rgba" => true]); +$animation = null; +$delay = []; + +for ($x = 0; $x < $image->width + $text->width; $x += 10) { + // append the frame to the image vertically ... we make a very tall, thin + // strip of frames to save + $frame = $image->composite2($text, "over", [ + "x" => $x - $text->width, + "y" => $image->height / 2 - $text->height / 2 + ]); + if ($animation == null) { + $animation = $frame; + } else { + $animation = $animation->join($frame, "vertical"); + } + + // frame delay in ms + array_push($delay, 30); +} + +// set animation properties +$animation->set("delay", $delay); +$animation->set("loop", 0); +$animation->set("page-height", $image->height); + +$animation->writeToFile($argv[2]); diff --git a/examples/bench.php b/examples/bench.php index 9743dde..e262ac7 100755 --- a/examples/bench.php +++ b/examples/bench.php @@ -1,10 +1,15 @@ #!/usr/bin/env php Vips\Access::SEQUENTIAL]); $im = $im->crop(100, 100, $im->width - 200, $im->height - 200); diff --git a/examples/class.php b/examples/class.php index cedf97f..47c5b3b 100755 --- a/examples/class.php +++ b/examples/class.php @@ -1,7 +1,7 @@ #!/usr/bin/env php getFields() as &$name) { + $value = $im->get($name); + if (str_ends_with($name, "-data")) { + $len = strlen($value); + $value = "<$len bytes of binary data>"; + } + echo " $name: $value\n"; + } +} + +$im = Vips\Image::newFromFile($argv[1]); +printMetadata($im); + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + * vim600: expandtab sw=4 ts=4 fdm=marker + * vim<600: expandtab sw=4 ts=4 + */ diff --git a/examples/generate_phpdoc.py b/examples/generate_phpdoc.py new file mode 100755 index 0000000..5b89216 --- /dev/null +++ b/examples/generate_phpdoc.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 + +# needs pyvips 2.2.3 or later + +from pyvips import Image, Introspect, GValue, Error, \ + ffi, enum_dict, flags_dict, vips_lib, gobject_lib, \ + type_map, type_name, type_from_name, nickname_find + +# This file generates the phpdoc comments for the magic methods and properties. + +# Regenerate docs with something like: +# +# cd src +# python ../examples/generate_phpdoc.py + +# this needs pyvips +# +# pip install --user pyvips + +# map a Python gtype to PHP argument type names +gtype_to_php_arg = { + GValue.gbool_type: 'bool', + GValue.gint_type: 'integer', + GValue.gdouble_type: 'float', + GValue.gstr_type: 'string', + GValue.refstr_type: 'string', + GValue.genum_type: 'string', + GValue.gflags_type: 'integer', + GValue.gobject_type: 'string', + GValue.image_type: 'Image', + GValue.array_int_type: 'integer[]|integer', + GValue.array_double_type: 'float[]|float', + GValue.array_image_type: 'Image[]|Image', + GValue.blob_type: 'string', + GValue.source_type: 'Source', + GValue.target_type: 'Target' +} + +# php result type names are different, annoyingly, and very restricted +gtype_to_php_result = { + GValue.gbool_type: 'bool', + GValue.gint_type: 'integer', + GValue.gdouble_type: 'float', + GValue.gstr_type: 'string', + GValue.refstr_type: 'string', + GValue.genum_type: 'string', + GValue.gflags_type: 'integer', + GValue.gobject_type: 'string', + GValue.image_type: 'Image', + GValue.array_int_type: 'array', + GValue.array_double_type: 'array', + GValue.array_image_type: 'array', + GValue.blob_type: 'string', + GValue.source_type: 'Source', + GValue.target_type: 'Target' +} + +# values for VipsArgumentFlags +_REQUIRED = 1 +_INPUT = 16 +_OUTPUT = 32 +_DEPRECATED = 64 +_MODIFY = 128 + +# for VipsOperationFlags +_OPERATION_DEPRECATED = 8 + +# some names we might generate are reserved PHP names ... just append a "1". +reserved_php_names = { + 'DEFAULT': 'DEFAULT1', + 'XOR': 'XOR1', + 'AND': 'AND1', + 'OR': 'OR1' +} + + +def gtype_to_php(gtype, result=False): + """Map a gtype to PHP type name we use to represent it. + """ + + fundamental = gobject_lib.g_type_fundamental(gtype) + + gtype_map = gtype_to_php_result if result else gtype_to_php_arg + + if gtype in gtype_map: + return gtype_map[gtype] + if fundamental in gtype_map: + return gtype_map[fundamental] + return '' + + +def remove_prefix(enum_str): + prefix = 'Vips' + + if enum_str.startswith(prefix): + return enum_str[len(prefix):] + + return enum_str + + +def generate_operation(operation_name): + intro = Introspect.get(operation_name) + + result = ' * @method ' + if intro.member_x is None: + result += 'static ' + if len(intro.required_output) == 0: + result += 'void ' + elif len(intro.required_output) == 1: + arg = intro.required_output[0] + details = intro.details[arg] + result += '{0} '.format(gtype_to_php(details['type'], True)) + else: + # we generate a Returns: block for this case, see below + result += 'array ' + + result += '{0}('.format(operation_name) + for name in intro.method_args: + details = intro.details[name] + result += '{0} ${1}, '.format(gtype_to_php(details['type']), name) + + result += 'array $options = []) ' + + description = intro.description + result += description[0].upper() + description[1:] + '.\n' + + # find any Enums we've referenced and output @see lines for them + for name in intro.required_output + intro.method_args: + details = intro.details[name] + fundamental = gobject_lib.g_type_fundamental(details['type']) + + if fundamental != GValue.genum_type: + continue + + result += ' * @see {0} for possible values for ${1}\n'.format(remove_prefix(type_name(details['type'])), name) + + if len(intro.required_output) > 1: + result += ' * Return array with: [\n' + for name in intro.required_output: + details = intro.details[name] + result += ' * \'{0}\' => @type {1} {2}\n'.format(name, gtype_to_php(details['type']), + details['blurb'][0].upper() + details['blurb'][1:]) + result += ' * ];\n' + + result += ' * @throws Exception\n' + + return result + + +preamble = """ + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +""" + +class_header = """ * @category Images + * @package Jcupitt\\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips +""" + + +def generate_auto_doc(filename): + all_nicknames = [] + + def add_nickname(gtype, a, b): + nickname = nickname_find(gtype) + try: + # can fail for abstract types + intro = Introspect.get(nickname) + + # we are only interested in non-deprecated operations + if (intro.flags & _OPERATION_DEPRECATED) == 0: + all_nicknames.append(nickname) + except Error: + pass + + type_map(gtype, add_nickname) + + return ffi.NULL + + type_map(type_from_name('VipsOperation'), add_nickname) + + # add 'missing' synonyms by hand + all_nicknames.append('crop') + + # make list unique and sort + all_nicknames = list(set(all_nicknames)) + all_nicknames.sort() + + # these have hand-written methods, don't autodoc them + no_generate = [ + 'bandjoin', + 'bandrank', + 'ifthenelse', + 'add', + 'subtract', + 'multiply', + 'divide', + 'remainder' + ] + all_nicknames = [x for x in all_nicknames if x not in no_generate] + + print('Generating {0} ...'.format(filename)) + + with open(filename, 'w') as f: + f.write(preamble) + f.write('\n') + f.write('namespace Jcupitt\\Vips;\n') + f.write('\n') + f.write('/**\n') + f.write(' * Autodocs for the Image class.\n') + f.write(class_header) + f.write(' *\n') + + for nickname in all_nicknames: + f.write(generate_operation(nickname)) + + f.write(' *\n') + + # all magic properties + tmp_file = Image.new_temp_file('%s.v') + all_properties = tmp_file.get_fields() + for name in all_properties: + php_name = name.replace('-', '_') + gtype = tmp_file.get_typeof(name) + fundamental = gobject_lib.g_type_fundamental(gtype) + + f.write(' * @property {0} ${1} {2}\n'.format(gtype_to_php(gtype), php_name, tmp_file.get_blurb(name))) + + if fundamental == GValue.genum_type: + f.write(' * @see {0} for possible values\n'.format(remove_prefix(type_name(gtype)))) + + f.write(' */\n') + f.write('abstract class ImageAutodoc extends VipsObject\n') + f.write('{\n') + f.write(' abstract public function __set(string $name, $value);\n') + f.write(' abstract public function __get(string $name);\n') + f.write('}\n') + + +def generate_enums(): + # otherwise we're missing some enums + vips_lib.vips_token_get_type() + vips_lib.vips_saveable_get_type() + vips_lib.vips_image_type_get_type() + + all_enums = [] + + def add_enum(gtype, a, b): + nickname = type_name(gtype) + all_enums.append(nickname) + + type_map(gtype, add_enum) + + return ffi.NULL + + type_map(type_from_name('GEnum'), add_enum) + + # Filter internal enums + blacklist = ['VipsImageType', 'VipsToken'] + all_enums = [name for name in all_enums if name not in blacklist] + + for name in all_enums: + gtype = type_from_name(name) + php_name = remove_prefix(name) + + print('Generating {0}.php ...'.format(php_name)) + + with open('{0}.php'.format(php_name), 'w') as f: + f.write(preamble) + f.write('\n') + f.write('namespace Jcupitt\\Vips;\n') + f.write('\n') + f.write('/**\n') + f.write(' * The {0} enum.\n'.format(php_name)) + f.write(class_header) + f.write(' */\n') + f.write('abstract class {0}\n'.format(php_name)) + f.write('{\n') + + for key, value in enum_dict(gtype).items(): + php_name = key.replace('-', '_').upper() + if php_name in reserved_php_names: + php_name = reserved_php_names[php_name] + f.write(' const {0} = \'{1}\';\n'.format(php_name, key)) + + f.write('}\n') + + +def generate_flags(): + all_flags = [] + + def add_flags(gtype, a, b): + nickname = type_name(gtype) + all_flags.append(nickname) + + type_map(gtype, add_flags) + + return ffi.NULL + + type_map(type_from_name('GFlags'), add_flags) + + # Filter internal flags + blacklist = ['VipsForeignFlags'] + all_flags = [name for name in all_flags if name not in blacklist] + + for name in all_flags: + gtype = type_from_name(name) + php_name = remove_prefix(name) + + print('Generating {0}.php ...'.format(php_name)) + + with open('{0}.php'.format(php_name), 'w') as f: + f.write(preamble) + f.write('\n') + f.write('namespace Jcupitt\\Vips;\n') + f.write('\n') + f.write('/**\n') + f.write(' * The {0} flags.\n'.format(php_name)) + f.write(class_header) + f.write(' */\n') + f.write('abstract class {0}\n'.format(php_name)) + f.write('{\n') + + for key, value in flags_dict(gtype).items(): + php_name = key.replace('-', '_').upper() + if php_name in reserved_php_names: + php_name = reserved_php_names[php_name] + f.write(' const {0} = {1};\n'.format(php_name, value)) + + f.write('}\n') + + +generate_auto_doc('ImageAutodoc.php') +generate_enums() +generate_flags() diff --git a/examples/generate_phpdoc.rb b/examples/generate_phpdoc.rb deleted file mode 100755 index 3d7fc16..0000000 --- a/examples/generate_phpdoc.rb +++ /dev/null @@ -1,347 +0,0 @@ -#!/usr/bin/env ruby - -require 'vips' - -# This file generates the phpdoc comments for the magic methods and properties. -# It's in Ruby, since we use the whole of gobject-introspection, not just the -# small bit exposed by php-vips-ext. - -# Regenerate docs with something like: -# -# cd src -# ../examples/generate_phpdoc.rb - -# this needs version 1.x of ruby-vips -# -# gem install ruby-vips -v 1.0.6 - -# gobject-introspection 3.0.7 crashes a lot if it GCs while doing -# something -GC.disable - -Vips::init("") - -# these have hand-written methods, don't autodoc them -$no_generate = %w( - bandjoin - bandrank - ifthenelse - add - subtract - multiply - divide - remainder - extract_area -) - -# Find all the vips enums -$enums = [] -Vips.constants.each do |name| - const = Vips.const_get name - if const.respond_to? :superclass and - const.superclass == GLib::Enum - $enums << name.to_s - end -end - -def enum?(name) - return true if $enums.include?(name) - - # is there a "Vips" at the front of the name? remove it and try the - # enums again - trim = name.to_s.tap{|s| s.slice!("Vips")} - return true if $enums.include?(trim) - - # is there a "::" at the front of the name? remove it and try the - # enums again - trim.slice! "::" - return true if $enums.include?(trim) - - return false -end - -# map Ruby type names to PHP argument type names -$to_php_map = { - "Vips::Image" => "Image", - "Image" => "Image", - "Array" => "integer[]|integer", - "Array" => "float[]|float", - "Array" => "Image[]|image", - "Integer" => "integer", - "gint" => "integer", - "guint64" => "integer", - "Double" => "float", - "gdouble" => "float", - "Float" => "float", - "String" => "string", - "Boolean" => "bool", - "gboolean" => "bool", - "Vips::Blob" => "string", - "gchararray" => "string", - "gpointer" => "string", -} - -# php result type names are different, annoyingly, and very restricted -$to_php_result_map = { - "Vips::Image" => "Image", - "Image" => "Image", - "Array" => "array", - "Array" => "array", - "Array" => "array", - "Integer" => "integer", - "gint" => "integer", - "guint64" => "integer", - "Double" => "float", - "gdouble" => "float", - "Float" => "float", - "String" => "string", - "Boolean" => "bool", - "gboolean" => "bool", - "Vips::Blob" => "string", - "gchararray" => "string", - "gpointer" => "string", -} - -def type_to_php(map, type) - return map[type] if map.include?(type) - return "string" if enum? type - - # no mapping found - puts "no mapping found for #{type}" - return "" -end - -class Vips::Argument - def to_php - type_to_php $to_php_map, type - end - - def to_php_result - type_to_php $to_php_result_map, type - end -end - -def generate_operation(file, op) - flags = op.flags - return if (flags & :deprecated) != 0 - nickname = Vips::nickname_find op.gtype - - return if $no_generate.include? nickname - - all_args = op.get_args.select {|arg| not arg.isset} - - # separate args into various categories - - required_input = all_args.select do |arg| - (arg.flags & :input) != 0 and - (arg.flags & :required) != 0 - end - - optional_input = all_args.select do |arg| - (arg.flags & :input) != 0 and - (arg.flags & :required) == 0 - end - - required_output = all_args.select do |arg| - (arg.flags & :output) != 0 and - (arg.flags & :required) != 0 - end - - # required input args with :modify are copied and appended to - # output - modified_required_input = required_input.select do |arg| - (arg.flags & :modify) != 0 - end - required_output += modified_required_input - - optional_output = all_args.select do |arg| - (arg.flags & :output) != 0 and - (arg.flags & :required) == 0 - end - - # optional input args with :modify are copied and appended to - # output - modified_optional_input = optional_input.select do |arg| - (arg.flags & :modify) != 0 - end - optional_output += modified_optional_input - - # find the first input image, if any ... we will be a method of this - # instance - member_x = required_input.find do |x| - x.gtype.type_is_a? GLib::Type["VipsImage"] - end - if member_x != nil - required_input.delete member_x - end - - file << " * @method " - file << "static " if not member_x - if required_output.length == 0 - file << "void " - elsif required_output.length == 1 - file << "#{required_output[0].to_php_result} " - else - # we generate a Returns: block for this case, see below - file << "array " - end - - file << "#{nickname}(" - - required_input.each do |arg| - file << "#{arg.to_php} $#{arg.name}, " - end - file << "array $options = []) " - - file << "#{op.description.capitalize}.\n" - - # find any Enums we've referenced and output @see lines for them - used_enums = [] - (required_output + required_input).each do |arg| - next if not enum? arg.type - file << " * @see #{arg.type} for possible values for $#{arg.name}\n" - end - - if required_output.length > 1 - file << " * Return array with: [\n" - required_output.each do |arg| - file << " * '#{arg.name}' => " - file << "@type #{arg.to_php} #{arg.blurb.capitalize}.\n" - end - file << " * ];\n" - end - - file << " * @throws Exception\n" -end - -def generate_class(file, gtype) - begin - # can fail for abstract types - # can't find a way to get to #abstract? from a gtype - op = Vips::Operation.new gtype.name - rescue - op = nil - end - - generate_operation(file, op) if op - - gtype.children.each {|x| generate_class file, x} -end - -preamble = < - * @copyright 2016 John Cupitt - * @license https://opensource.org/licenses/MIT MIT - * @link https://github.com/jcupitt/php-vips - */ -EOF - -class_header = < - * @copyright 2016 John Cupitt - * @license https://opensource.org/licenses/MIT MIT - * @link https://github.com/jcupitt/php-vips -EOF - -# The image class is the most complex -puts "Generating ImageAutodoc.php ..." -File.open("ImageAutodoc.php", "w") do |file| - file << preamble - file << "\n" - file << "namespace Jcupitt\\Vips;\n" - - file << "\n" - file << "/**\n" - file << " * Autodocs for the Image class.\n" - file << class_header - file << " *\n" - - generate_class file, GLib::Type["VipsOperation"] - -# extract_area is in there twice, once as "crop" ... do them by hand - file << <getFields() as &$name) { + $value = $im->get($name); + if (str_ends_with($name, "-data")) { + $len = strlen($value); + $value = "<$len bytes of binary data>"; + } + echo " $name: $value\n"; + } +} + +$im = Vips\Image::newFromFile($argv[1]); +echo "$argv[1]\n"; +printMetadata($im); + +echo "\nafter keep => icc\n"; +$buf = $im->tiffsave_buffer(['keep' => Vips\ForeignKeep::ICC]); +$im2 = Vips\Image::newFromBuffer($buf, ""); +printMetadata($im2); + +echo "\nafter keep => exif|xmp\n"; +$buf = $im->tiffsave_buffer(['keep' => Vips\ForeignKeep::ICC | Vips\ForeignKeep::XMP]); +$im2 = Vips\Image::newFromBuffer($buf, ""); +printMetadata($im2); + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + * vim600: expandtab sw=4 ts=4 fdm=marker + * vim<600: expandtab sw=4 ts=4 + */ diff --git a/examples/progress.php b/examples/progress.php new file mode 100755 index 0000000..d53faac --- /dev/null +++ b/examples/progress.php @@ -0,0 +1,28 @@ +#!/usr/bin/env php +setProgress(true); + +$image->signalConnect("preeval", function ($image, $progress) { + echo "preeval:\n"; +}); +$image->signalConnect("eval", function ($image, $progress) { + echo "eval: $progress->percent % complete\r"; +}); + +$image->signalConnect("posteval", function ($image, $progress) { + echo "\nposteval:\n"; +}); + +// trigger evaluation +$image->avg(); + +$image = null; + +Vips\FFI::shutDown(); diff --git a/examples/sig.php b/examples/sig.php index 426890d..8b295c7 100755 --- a/examples/sig.php +++ b/examples/sig.php @@ -1,7 +1,7 @@ #!/usr/bin/env php Vips\Access::SEQUENTIAL]); /** diff --git a/examples/streaming-bench.php b/examples/streaming-bench.php new file mode 100755 index 0000000..1fcf04c --- /dev/null +++ b/examples/streaming-bench.php @@ -0,0 +1,131 @@ +#!/usr/bin/env php + 'sequential']; + $sourceOptionString = 'access=sequential'; + $iterations = 100; + $targetWidth = 100.0; + $targetSuffix = '.jpg'; + $targetOptions = ['optimize-coding' => true, 'strip' => true, 'Q' => 100, 'profile' => 'srgb']; + $targetFile = dirname(__DIR__) . "/tests/images/target.jpg"; + $sourceFile = dirname(__DIR__) . '/tests/images/img_0076.jpg'; + +### Callbacks + $start = microtime(true); + + for ($i = 0; $i < $iterations; $i++) { + $source = new SourceResource(fopen($sourceFile, 'rb')); + $target = new TargetResource(fopen($targetFile, 'wb+')); + $image = Image::newFromSource($source, '', $sourceOptions); + $image = $image->resize($targetWidth / $image->width); + $image->writeToTarget( + $target, + $targetSuffix, + $targetOptions + ); + unlink($targetFile); + } + + echo (microtime(true) - $start) . ' Seconds for Streaming with callbacks' . PHP_EOL; + +### Builtin + $start = microtime(true); + + for ($i = 0; $i < $iterations; $i++) { + $source = Source::newFromFile($sourceFile); + $target = Target::newToFile($targetFile); + $image = Image::newFromSource($source, '', $sourceOptions); + $image = $image->resize($targetWidth / $image->width); + $image->writeToTarget( + $target, + $targetSuffix, + $targetOptions + ); + unlink($targetFile); + } + + echo (microtime(true) - $start) . ' Seconds for Streaming with builtin source/target' . PHP_EOL; + +### Callbacks Thumbnail + $start = microtime(true); + + for ($i = 0; $i < $iterations; $i++) { + $source = new SourceResource(fopen($sourceFile, 'rb')); + $target = new TargetResource(fopen($targetFile, 'wb+')); + $image = Image::thumbnail_source($source, $targetWidth); + $image->writeToTarget( + $target, + $targetSuffix, + $targetOptions + ); + unlink($targetFile); + } + + echo (microtime(true) - $start) . ' Seconds for Streaming Thumbnail with callbacks' . PHP_EOL; + +### Builtin Thumbnail + $start = microtime(true); + + for ($i = 0; $i < $iterations; $i++) { + $source = Source::newFromFile($sourceFile); + $target = Target::newToFile($targetFile); + $image = Image::thumbnail_source($source, $targetWidth); + $image->writeToTarget( + $target, + $targetSuffix, + $targetOptions + ); + unlink($targetFile); + } + + echo (microtime(true) - $start) . ' Seconds for Streaming Thumbnail with builtin source/target' . PHP_EOL; + +### Thumbnail + $start = microtime(true); + + for ($i = 0; $i < $iterations; $i++) { + $image = Image::thumbnail($sourceFile . "[$sourceOptionString]", $targetWidth); + $image->writeToFile( + $targetFile, + $targetOptions + ); + unlink($targetFile); + } + + echo (microtime(true) - $start) . ' Seconds for Thumbnail API' . PHP_EOL; + +### Classic + $start = microtime(true); + + for ($i = 0; $i < $iterations; $i++) { + $image = Image::newFromFile($sourceFile, $sourceOptions); + $image = $image->resize($targetWidth / $image->width); + $image->writeToFile( + $targetFile, + $targetOptions + ); + unlink($targetFile); + } + + echo (microtime(true) - $start) . ' Seconds for Classic API' . PHP_EOL; +}; + +$doBenchmark(); + +//echo "=== NOW NO CACHE ===" . PHP_EOL; +// +//Config::cacheSetMax(0); +//Config::cacheSetMaxFiles(0); +//Config::cacheSetMaxMem(0); +// +//$doBenchmark(); diff --git a/examples/streaming-custom.php b/examples/streaming-custom.php new file mode 100755 index 0000000..d3b78f8 --- /dev/null +++ b/examples/streaming-custom.php @@ -0,0 +1,55 @@ +#!/usr/bin/env php +onRead(function ($bufferLength) use (&$in_file) { + // return 0 for EOF, -ve for read error + return fread($in_file, $bufferLength); +}); +// seek is optional +$source->onSeek(function ($offset, $whence) use (&$in_file) { + if (fseek($in_file, $offset, $whence)) { + return -1; + } + + return ftell($in_file); +}); + +// open for write and read ... formats like tiff need to be able to seek back +// in the output and update bytes later +$out_file = fopen($argv[2], 'w+'); +$target = new Vips\TargetCustom(); +$target->onWrite(function ($buffer) use (&$out_file) { + $result = fwrite($out_file, $buffer); + if ($result === false) { + // IO error + return -1; + } else { + return $result; + } +}); +// read and seek are optional +$target->onSeek(function ($offset, $whence) use (&$out_file) { + if (fseek($out_file, $offset, $whence)) { + return -1; + } + + return ftell($out_file); +}); +$target->onRead(function ($bufferLength) use (&$out_file) { + return fread($out_file, $bufferLength); +}); + +$image = Vips\Image::newFromSource($source); +$image->writeToTarget($target, $argv[3]); diff --git a/examples/streaming.php b/examples/streaming.php new file mode 100755 index 0000000..f5628e2 --- /dev/null +++ b/examples/streaming.php @@ -0,0 +1,12 @@ +#!/usr/bin/env php +writeToTarget($target, '.jpg[Q=95]'); diff --git a/examples/vips-magick.php b/examples/vips-magick.php index 690ee37..42e6691 100755 --- a/examples/vips-magick.php +++ b/examples/vips-magick.php @@ -1,10 +1,15 @@ #!/usr/bin/env php 'sequential']); + +// we'll read the watermark image many times, so we need random access for this +$watermark = Vips\Image::newFromFile($argv[3]); + +// the watermark image needs to have an alpha channel +if (!$watermark->hasAlpha() || $watermark->bands != 4) { + echo("watermark image is not RGBA\n"); + exit(1); +} + +// make the watermark semi-transparent +$watermark = $watermark->multiply([1, 1, 1, 0.3])->cast("uchar"); + +// repeat the watermark to the size of the image +$watermark = $watermark->replicate( + 1 + $image->width / $watermark->width, + 1 + $image->height / $watermark->height +); +$watermark = $watermark->crop(0, 0, $image->width, $image->height); + +// composite the watermark over the main image +$image = $image->composite2($watermark, 'over'); + +$image->writeToFile($argv[2]); + +$image = null; +$watermark = null; + +Vips\FFI::shutDown(); diff --git a/examples/watermark-text.php b/examples/watermark-text.php new file mode 100755 index 0000000..8a60d6c --- /dev/null +++ b/examples/watermark-text.php @@ -0,0 +1,80 @@ +#!/usr/bin/env php + 'sequential', +]); +$page_height = $image->height; + +// is this an animated image? open all pages +if ($image->getType("n-pages") != 0) { + $image = Vips\Image::newFromFile($argv[1], [ + 'access' => 'sequential', + 'n' => -1 + ]); + + // the size of each frame + $page_height = $image->get('page-height'); +} + +$output_filename = $argv[2]; +$text = $argv[3]; + +$text_mask = Vips\Image::text($text, [ + 'width' => $image->width, + 'dpi' => 150 +]); + +// semi-transparent white text on a blue background +$foreground = [255, 255, 255, 50]; +$background = [0, 0, 255, 50]; + +// and a 10-pixel margin +$margin = 10; + +$overlay = $text_mask->ifthenelse($foreground, $background, [ + 'blend' => true +]); + +// add a margin, with the same background +$overlay = $overlay->embed( + $margin, + $margin, + $overlay->width + 2 * $margin, + $overlay->height + 2 * $margin, + [ + 'extend' => 'background', + 'background' => $background + ] +); + +// tag as srgb +$overlay = $overlay->copy(['interpretation' => 'srgb']); + +// expand to the size of a frame, transparent background, place at the bottom +// left +$overlay = $overlay->embed( + $margin, + $page_height - $overlay->height - $margin, + $image->width, + $page_height +); + +// expand to the full size of the gif roll +$overlay = $overlay->replicate(1, $image->height / $page_height); + +// composite on top of the gif +$image = $image->composite2($overlay, 'over'); + +// and write back +$image->writeToFile($output_filename); diff --git a/install-vips.sh b/install-vips.sh deleted file mode 100755 index edf02ea..0000000 --- a/install-vips.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -vips_site=https://github.com/jcupitt/libvips/releases/download -version=$VIPS_VERSION_MAJOR.$VIPS_VERSION_MINOR.$VIPS_VERSION_MICRO - -set -e - -# do we already have the correct vips built? early exit if yes -# we could check the configure params as well I guess -if [ -d "$HOME/vips/bin" ]; then - installed_version=$($HOME/vips/bin/vips --version) - escaped_version="$VIPS_VERSION_MAJOR\.$VIPS_VERSION_MINOR\.$VIPS_VERSION_MICRO" - echo "Need vips-$version" - echo "Found $installed_version" - if [[ "$installed_version" =~ ^vips-$escaped_version ]]; then - echo "Using cached directory" - exit 0 - fi -fi - -rm -rf $HOME/vips -wget $vips_site/v$version/vips-$version.tar.gz -tar xf vips-$version.tar.gz -cd vips-$version -CXXFLAGS=-D_GLIBCXX_USE_CXX11_ABI=0 ./configure --prefix=$HOME/vips $* -make && make install diff --git a/phpcs-ruleset.xml b/phpcs-ruleset.xml index fd567e0..f2bc93b 100644 --- a/phpcs-ruleset.xml +++ b/phpcs-ruleset.xml @@ -9,6 +9,9 @@ vendor/ + + docs/ + diff --git a/phpdoc.xml b/phpdoc.xml index a37402d..131e964 100644 --- a/phpdoc.xml +++ b/phpdoc.xml @@ -1,16 +1,14 @@ - + php-vips + php-vips docs - docs - src - diff --git a/phpunit.xml b/phpunit.xml index 0bc9183..cd2dbad 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,8 +8,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" - syntaxCheck="false"> + stopOnFailure="false"> ./tests/ diff --git a/scrutinizer.yml b/scrutinizer.yml deleted file mode 100644 index af8cd38..0000000 --- a/scrutinizer.yml +++ /dev/null @@ -1,34 +0,0 @@ -filter: - excluded_paths: [tests/*] -checks: - php: - code_rating: true - remove_extra_empty_lines: true - remove_php_closing_tag: true - remove_trailing_whitespace: true - fix_use_statements: - remove_unused: true - preserve_multiple: false - preserve_blanklines: true - order_alphabetically: true - fix_php_opening_tag: true - fix_linefeed: true - fix_line_ending: true - fix_identation_4spaces: true - fix_doc_comments: true -tools: - external_code_coverage: - timeout: 600 - runs: 3 - php_code_coverage: false - php_code_sniffer: - config: - standard: PSR2 - filter: - paths: ['src'] - php_loc: - enabled: true - excluded_dirs: [vendor, tests] - php_cpd: - enabled: true - excluded_dirs: [vendor, tests] \ No newline at end of file diff --git a/src/ArgumentFlags.php b/src/ArgumentFlags.php new file mode 100644 index 0000000..cb54e69 --- /dev/null +++ b/src/ArgumentFlags.php @@ -0,0 +1,69 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * The ArgumentFlags enum. + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +abstract class ArgumentFlags +{ + const REQUIRED = 1; + const CONSTRUCT = 2; + const SET_ONCE = 4; + const SET_ALWAYS = 8; + const INPUT = 16; + const OUTPUT = 32; + const DEPRECATED = 64; + const MODIFY = 128; + + const NAMES = [ + "REQUIRED" => self::REQUIRED, + "CONSTRUCT" => self::CONSTRUCT, + "SET_ONCE" => self::SET_ONCE, + "SET_ALWAYS" => self::SET_ALWAYS, + "INPUT" => self::INPUT, + "OUTPUT" => self::OUTPUT, + "DEPRECATED" => self::DEPRECATED, + "MODIFY" => self::MODIFY, + ]; +} diff --git a/src/BlendMode.php b/src/BlendMode.php index 833ee1b..8f0df6d 100644 --- a/src/BlendMode.php +++ b/src/BlendMode.php @@ -60,7 +60,7 @@ abstract class BlendMode const DEST_IN = 'dest-in'; const DEST_OUT = 'dest-out'; const DEST_ATOP = 'dest-atop'; - const XOR = 'xor'; + const XOR1 = 'xor'; const ADD = 'add'; const SATURATE = 'saturate'; const MULTIPLY = 'multiply'; diff --git a/src/Config.php b/src/Config.php index 1c78b0c..f7824d7 100644 --- a/src/Config.php +++ b/src/Config.php @@ -55,10 +55,8 @@ class Config /** * The logger instance. - * - * @var LoggerInterface */ - private static $logger; + private static ?LoggerInterface $logger = null; /** * Sets a logger. This can be handy for debugging. For example: @@ -71,7 +69,7 @@ class Config * * @return void */ - public static function setLogger(LoggerInterface $logger) + public static function setLogger(LoggerInterface $logger): void { self::$logger = $logger; } @@ -79,9 +77,9 @@ public static function setLogger(LoggerInterface $logger) /** * Gets a logger. * - * @return LoggerInterface $logger|null + * @return LoggerInterface|null The logger or null. */ - public static function getLogger() + public static function getLogger(): ?LoggerInterface { return self::$logger; } @@ -94,23 +92,23 @@ public static function getLogger() * * @return void */ - public static function cacheSetMax($value) + public static function cacheSetMax(int $value): void { - vips_cache_set_max($value); + FFI::vips()->vips_cache_set_max($value); } /** * Set the maximum amount of memory to allow cached operations to use, in * bytes. * - * @param integer $value The maximum amount of memory cached opertations can + * @param integer $value The maximum amount of memory cached operations can * hold, in bytes. * * @return void */ - public static function cacheSetMaxMem($value) + public static function cacheSetMaxMem(int $value): void { - vips_cache_set_max_mem($value); + FFI::vips()->vips_cache_set_max_mem($value); } /** @@ -121,9 +119,9 @@ public static function cacheSetMaxMem($value) * * @return void */ - public static function cacheSetMaxFiles($value) + public static function cacheSetMaxFiles(int $value): void { - vips_cache_set_max_files($value); + FFI::vips()->vips_cache_set_max_files($value); } /** @@ -135,20 +133,32 @@ public static function cacheSetMaxFiles($value) * * @return void */ - public static function concurrencySet($value) + public static function concurrencySet(int $value): void { - vips_concurrency_set($value); + FFI::vips()->vips_concurrency_set($value); } /** - * Gets the libvips version number as a atring of the form + * Gets the libvips version number as a string of the form * MAJOR.MINOR.MICRO, for example "8.6.1". * * @return string */ public static function version(): string { - return vips_version(); + return FFI::version(); + } + + /** + * Is this at least libvips major.minor[.patch]? + * @param int $x Major component. + * @param int $y Minor component. + * @param int $z Patch component. + * @return bool `true` if at least libvips major.minor[.patch]; otherwise, `false`. + */ + public static function atLeast(int $x, int $y, int $z = 0): bool + { + return FFI::atLeast($x, $y, $z); } } diff --git a/src/Connection.php b/src/Connection.php new file mode 100644 index 0000000..096a422 --- /dev/null +++ b/src/Connection.php @@ -0,0 +1,36 @@ +pointer = FFI::vips()->cast(FFI::ctypes('VipsConnection'), $pointer); + parent::__construct($pointer); + } + + /** + * Get the filename associated with a connection. Return null if there is no associated file. + */ + public function filename(): ?string + { + return FFI::vips()->vips_connection_filename($this->pointer); + } + + /** + * Make a human-readable name for a connection suitable for error messages. + */ + public function nick(): ?string + { + return FFI::vips()->vips_connection_nick($this->pointer); + } +} diff --git a/src/DebugLogger.php b/src/DebugLogger.php index 4cfa6ee..ddf8373 100644 --- a/src/DebugLogger.php +++ b/src/DebugLogger.php @@ -69,7 +69,7 @@ class DebugLogger implements LoggerInterface * * @return void */ - public function log($level, $message, array $context = []) + public function log($level, $message, array $context = []): void { // `Vips\Image` to string convert array_walk_recursive($context, function (&$value) { diff --git a/src/DemandStyle.php b/src/DemandStyle.php index 395a404..36970a2 100644 --- a/src/DemandStyle.php +++ b/src/DemandStyle.php @@ -53,5 +53,4 @@ abstract class DemandStyle const SMALLTILE = 'smalltile'; const FATSTRIP = 'fatstrip'; const THINSTRIP = 'thinstrip'; - const ANY = 'any'; } diff --git a/src/Exception.php b/src/Exception.php index d4c4672..a3704e7 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -51,6 +51,17 @@ */ class Exception extends \Exception { + public function __construct($message = "", $code = 0, ?\Throwable $previous = null) + { + if ($message == "") { + $message = "libvips error: " . FFI::vips()->vips_error_buffer(); + FFI::vips()->vips_error_clear(); + } + + Utils::errorLog($message); + + parent::__construct($message, $code, $previous); + } } /* diff --git a/src/FFI.php b/src/FFI.php new file mode 100644 index 0000000..f259f7b --- /dev/null +++ b/src/FFI.php @@ -0,0 +1,902 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * This class contains the libvips FFI methods. + * + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +class FFI +{ + + /** + * The FFI handle we use for the glib binary. + * + * @internal + */ + private static \FFI $glib; + + /** + * The FFI handle we use for the gobject binary. + * + * @internal + */ + private static \FFI $gobject; + + /** + * The FFI handle we use for the libvips binary. + * + * @internal + */ + private static \FFI $vips; + + /** + * Are the above FFI handles initialized? + * + * @internal + */ + private static bool $ffi_inited = false; + + /** + * A list of paths where libvips might reside. + * + * @internal + */ + private static array $libraryPaths = [ + "" // system library + ]; + + /** + * Look up these once. + * + * @internal + */ + private static array $ctypes; + private static array $gtypes; + private static array $ftypes; + + /** + * The library version number we detect. + * + * @internal + */ + private static int $library_major; + private static int $library_minor; + private static int $library_micro; + + public static function glib(): \FFI + { + self::init(); + + return self::$glib; + } + + public static function gobject(): \FFI + { + self::init(); + + return self::$gobject; + } + + public static function vips(): \FFI + { + self::init(); + + return self::$vips; + } + + public static function ctypes(string $name): \FFI\CType + { + self::init(); + + return self::$ctypes[$name]; + } + + public static function gtypes(string $name): int + { + self::init(); + + return self::$gtypes[$name]; + } + + public static function ftypes(string $name): string + { + self::init(); + + return self::$ftypes[$name]; + } + + /** + * Gets the libvips version number as a string of the form + * MAJOR.MINOR.MICRO, for example "8.6.1". + * + * @return string + */ + public static function version(): string + { + self::init(); + + return self::$library_major . "." . + self::$library_minor . "." . + self::$library_micro; + } + + /** + * Is this at least libvips major.minor[.patch]? + * @param int $x Major component. + * @param int $y Minor component. + * @param int $z Patch component. + * @return bool `true` if at least libvips major.minor[.patch]; otherwise, `false`. + */ + public static function atLeast(int $x, int $y, int $z = 0): bool + { + return self::$library_major > $x || + (self::$library_major === $x && self::$library_minor > $y) || + (self::$library_major === $x && self::$library_minor === $y && + self::$library_micro >= $z); + } + + /** + * Adds a directory to the search path for shared libraries. + * + * This method has no effect if FFI handles are already initialized, + * if the specified path is non-existent, or if the path is already + * included. + * + * @param string $path The path of the library. + * @return bool `true` if the path was added; otherwise, `false`. + */ + public static function addLibraryPath(string $path): bool + { + // Already initialized. + if (self::$ffi_inited) { + return false; + } + + $path = realpath($path); + if ($path === false) { + return false; + } + + $path .= DIRECTORY_SEPARATOR; + + if (in_array($path, self::$libraryPaths)) { + return false; + } + + self::$libraryPaths[] = $path; + + return true; + } + + /** + * Shut down libvips. Call this just before process exit. + * + * @return void + */ + public static function shutDown(): void + { + self::vips()->vips_shutdown(); + } + + public static function newGClosure(): \FFI\CData + { + // GClosure measures 32-bit with the first few fields until marshal. + // - Marshal is a function pointer, thus platform-dependant. + // - Data is a pointer, thus platform-dependant. + // - Notifiers is an array-pointer, thus platform-dependant. + // All in all it's basically 4 (bytes) + 3 * POINTER_SIZE + // However, gobject wants 8 (bytes) + 3 * POINTER_SIZE. + // I'm not sure where that extra byte comes from. Padding on 64-bit machines? + return self::gobject()->g_closure_new_simple(8 + 3 * PHP_INT_SIZE, null); + } + + private static function libraryName(string $name, int $abi): string + { + switch (PHP_OS_FAMILY) { + case "Windows": + return "$name-$abi.dll"; + + case "OSX": + case "Darwin": + return "$name.$abi.dylib"; + + default: + // most *nix + return "$name.so.$abi"; + } + } + + private static function libraryLoad( + string $libraryName, + string $interface + ): ?\FFI { + Utils::debugLog("trying to open", ["libraryName" => $libraryName]); + foreach (self::$libraryPaths as $path) { + Utils::debugLog("trying path", ["path" => $path]); + try { + $library = \FFI::cdef($interface, $path . $libraryName); + Utils::debugLog("success", []); + return $library; + } catch (\FFI\Exception $e) { + Utils::debugLog("init", [ + "msg" => "library load failed", + "exception" => $e->getMessage() + ]); + } + } + return null; + } + + private static function init(): void + { + // Already initialized. + if (self::$ffi_inited) { + return; + } + + // detect the most common installation problems + if (!extension_loaded('ffi')) { + throw new Exception('FFI extension not loaded'); + } + if (!ini_get('ffi.enable')) { + throw new Exception("ffi.enable not set to 'true'"); + } + + $vips_libname = self::libraryName("libvips", 42); + $glib_libname = self::libraryName("libglib-2.0", 0); + $gobject_libname = self::libraryName("libgobject-2.0", 0); + + Utils::debugLog("init", ["library" => $vips_libname]); + + $is_64bits = PHP_INT_SIZE === 8; + + if (PHP_OS_FAMILY === "OSX" || PHP_OS_FAMILY === "Darwin") { + // Homebrew on Apple Silicon + self::addLibraryPath("/opt/homebrew/lib"); + // See https://github.com/Homebrew/brew/issues/13481#issuecomment-1207203483 + self::addLibraryPath("/usr/local/lib"); + } + + $vips = self::libraryLoad($vips_libname, <<<'CPP' + int vips_init (const char *argv0); + const char *vips_error_buffer (void); + int vips_version(int flag); + CPP); + + if ($vips === null) { + // drop the "" (system path) member + array_shift(self::$libraryPaths); + $msg = "Unable to open library '$vips_libname'"; + if (!empty(self::$libraryPaths)) { + $msg .= " in any of ['" . implode("', '", self::$libraryPaths) . "']"; + } + $msg .= ". Make sure that you've installed libvips and that '$vips_libname'"; + $msg .= " is on your system's library search path."; + throw new Exception($msg); + } + + $result = $vips->vips_init(""); + if ($result != 0) { + throw new Exception("libvips error: " . $vips->vips_error_buffer()); + } + Utils::debugLog("init", ["vips_init" => $result]); + + # get the library version number, then we can build the API + self::$library_major = $vips->vips_version(0); + self::$library_minor = $vips->vips_version(1); + self::$library_micro = $vips->vips_version(2); + Utils::debugLog("init", [ + "libvips version" => [ + self::$library_major, + self::$library_minor, + self::$library_micro + ] + ]); + + if (!self::atLeast(8, 7)) { + throw new Exception("your libvips is too old -- " . + "8.7 or later required"); + } + + // Typedefs shared across the libvips, GLib and GObject declarations + $typedefs = <<<'CPP' +// we need the glib names for these types +typedef uint32_t guint32; +typedef int32_t gint32; +typedef uint64_t guint64; +typedef int64_t gint64; +typedef void* gpointer; + +CPP; + + // GType is the size of a pointer + $typedefs .= 'typedef ' . ($is_64bits ? 'guint64' : 'guint32') . ' GType;'; + + $typedefs .= <<<'CPP' + +typedef struct _GData GData; + +typedef struct _GTypeClass GTypeClass; + +typedef struct _GTypeInstance { + GTypeClass *g_class; +} GTypeInstance; + +typedef struct _GObject { + GTypeInstance g_type_instance; + unsigned int ref_count; + GData *qdata; +} GObject; +CPP; + + // GLib declarations + $glib_decls = $typedefs . <<<'CPP' +void* g_malloc (size_t size); +void g_free (void* data); +void g_strfreev (char** str_array); +CPP; + + // GObject declarations + $gobject_decls = $typedefs . <<<'CPP' +typedef struct _GValue { + GType g_type; + guint64 data[2]; +} GValue; + +typedef struct _GParamSpec { + GTypeInstance g_type_instance; + + const char* name; + unsigned int flags; + GType value_type; + GType owner_type; + + // private, but cffi in API mode needs these to be able to get the + // offset of any member + char* _nick; + char* _blurb; + GData* qdata; + unsigned int ref_count; + unsigned int param_id; +} GParamSpec; + +const char* g_type_name (GType gtype); +GType g_type_from_name (const char* name); + +void g_value_init (GValue* value, GType gtype); +void g_value_unset (GValue* value); +GType g_type_fundamental (GType gtype); + +void g_value_set_boolean (GValue* value, bool v_boolean); +void g_value_set_int (GValue* value, int i); +void g_value_set_uint64 (GValue* value, guint64 ull); +void g_value_set_int64 (GValue* value, guint64 ull); +void g_value_set_double (GValue* value, double d); +void g_value_set_enum (GValue* value, int e); +void g_value_set_flags (GValue* value, unsigned int f); +void g_value_set_string (GValue* value, const char* str); +void g_value_set_object (GValue* value, void* object); +void g_value_set_pointer (GValue* value, gpointer pointer); + +bool g_value_get_boolean (const GValue* value); +int g_value_get_int (GValue* value); +guint64 g_value_get_uint64 (GValue* value); +gint64 g_value_get_int64 (GValue* value); +double g_value_get_double (GValue* value); +int g_value_get_enum (GValue* value); +unsigned int g_value_get_flags (GValue* value); +const char* g_value_get_string (GValue* value); +void* g_value_get_object (GValue* value); +gpointer g_value_get_pointer (GValue* value); + +typedef struct _GEnumValue { + int value; + + const char *value_name; + const char *value_nick; +} GEnumValue; + +typedef struct _GEnumClass { + GTypeClass *g_type_class; + + int minimum; + int maximum; + unsigned int n_values; + GEnumValue *values; +} GEnumClass; + +typedef struct _GFlagsValue { + unsigned int value; + + const char *value_name; + const char *value_nick; +} GFlagsValue; + +typedef struct _GFlagsClass { + GTypeClass *g_type_class; + + unsigned int mask; + unsigned int n_values; + GFlagsValue *values; +} GFlagsClass; + +void* g_type_class_ref (GType type); + +void* g_object_new (GType type, void*); +void g_object_ref (void* object); +void g_object_unref (void* object); + +void g_object_set_property (GObject* object, + const char *name, GValue* value); +void g_object_get_property (GObject* object, + const char* name, GValue* value); + +typedef void (*GCallback)(void); +typedef void (*GClosureNotify)(void* data, struct _GClosure *); +long g_signal_connect_data (GObject* object, + const char* detailed_signal, + GCallback c_handler, + void* data, + GClosureNotify destroy_data, + int connect_flags); + +const char* g_param_spec_get_blurb (GParamSpec* psp); + +typedef void *GClosure; +typedef void (*marshaler)( + struct GClosure* closure, + GValue* return_value, + int n_param_values, + const GValue* param_values, + void* invocation_hint, + void* marshal_data +); +void g_closure_set_marshal(GClosure* closure, marshaler marshal); +long g_signal_connect_closure(GObject* object, const char* detailed_signal, GClosure *closure, bool after); +GClosure* g_closure_new_simple (int sizeof_closure, void* data); +CPP; + + # the whole libvips API, mostly adapted from pyvips + $vips_decls = $typedefs . <<<'CPP' +typedef struct _VipsImage VipsImage; +typedef struct _VipsProgress VipsProgress; + +// Defined in GObject, just typedef to void +typedef void GParamSpec; +typedef void GValue; + +int vips_init (const char *argv0); +int vips_shutdown (void); + +const char *vips_error_buffer (void); +void vips_error_clear (void); +void vips_error_freeze (void); +void vips_error_thaw (void); + +int vips_version(int flag); + +void vips_leak_set (int leak); + +GType vips_type_find (const char* basename, const char* nickname); +const char* vips_nickname_find (GType type); + +typedef void* (*VipsTypeMap2Fn) (GType type, void* a, void* b); +void* vips_type_map (GType base, VipsTypeMap2Fn fn, void* a, void* b); + +int vips_enum_from_nick (const char* domain, + GType gtype, const char* str); +const char *vips_enum_nick (GType gtype, int value); + +void vips_value_set_ref_string (GValue* value, const char* str); +void vips_value_set_array_double (GValue* value, +const double* array, int n ); +void vips_value_set_array_int (GValue* value, +const int* array, int n ); +void vips_value_set_array_image (GValue *value, int n); +typedef void (*FreeFn)(void* a); +void vips_value_set_blob (GValue* value, + FreeFn free_fn, void* data, size_t length); + +const char* vips_value_get_ref_string (const GValue* value, + size_t* length); +double* vips_value_get_array_double (const GValue* value, int* n); +int* vips_value_get_array_int (const GValue* value, int* n); +VipsImage** vips_value_get_array_image (const GValue* value, int* n); +void* vips_value_get_blob (const GValue* value, size_t* length); + +// need to make some of these by hand +GType vips_interpretation_get_type (void); +GType vips_operation_flags_get_type (void); +GType vips_band_format_get_type (void); +GType vips_token_get_type (void); +GType vips_saveable_get_type (void); +GType vips_image_type_get_type (void); + +void vips_image_set_progress (VipsImage* image, bool progress); +void vips_image_set_kill (VipsImage* image, bool kill); + +typedef struct _VipsProgress { + VipsImage* im; + + int run; + int eta; + gint64 tpels; + gint64 npels; + int percent; + void* start; +} VipsProgress; + +typedef struct _VipsObject { + GObject parent_instance; + + bool constructed; + bool static_object; + void *argument_table; + char *nickname; + char *description; + bool preclose; + bool close; + bool postclose; + size_t local_memory; +} VipsObject; + +typedef struct _VipsObjectClass VipsObjectClass; + +typedef struct _VipsArgument { + GParamSpec *pspec; +} VipsArgument; + +typedef struct _VipsArgumentInstance { + VipsArgument parent; + + // more +} VipsArgumentInstance; + +typedef enum _VipsArgumentFlags { + VIPS_ARGUMENT_NONE = 0, + VIPS_ARGUMENT_REQUIRED = 1, + VIPS_ARGUMENT_CONSTRUCT = 2, + VIPS_ARGUMENT_SET_ONCE = 4, + VIPS_ARGUMENT_SET_ALWAYS = 8, + VIPS_ARGUMENT_INPUT = 16, + VIPS_ARGUMENT_OUTPUT = 32, + VIPS_ARGUMENT_DEPRECATED = 64, + VIPS_ARGUMENT_MODIFY = 128 +} VipsArgumentFlags; + +typedef struct _VipsArgumentClass { + VipsArgument parent; + + VipsObjectClass *object_class; + VipsArgumentFlags flags; + int priority; + unsigned int offset; +} VipsArgumentClass; + +int vips_object_get_argument (VipsObject* object, const char *name, + GParamSpec** pspec, + VipsArgumentClass** argument_class, + VipsArgumentInstance** argument_instance); + +void vips_object_print_all (void); + +int vips_object_set_from_string (VipsObject* object, + const char* options); + +const char* vips_object_get_description (VipsObject* object); + +typedef struct _VipsImage { + VipsObject parent_instance; + // more +} VipsImage; + +const char* vips_foreign_find_load (const char* name); +const char* vips_foreign_find_load_buffer (const void* data, + size_t size); +const char* vips_foreign_find_save (const char* name); +const char* vips_foreign_find_save_buffer (const char* suffix); + +VipsImage* vips_image_new_matrix_from_array (int width, int height, + const double* array, int size); +VipsImage* vips_image_new_from_memory (const void* data, size_t size, + int width, int height, int bands, int format); +VipsImage* vips_image_new_from_memory_copy (const void *data, size_t size, + int width, int height, int bands, int format); + +VipsImage* vips_image_copy_memory (VipsImage* image); + +GType vips_image_get_typeof (const VipsImage* image, + const char* name); +int vips_image_get (const VipsImage* image, + const char* name, GValue* value_copy); +void vips_image_set (VipsImage* image, + const char* name, GValue* value); +int vips_image_remove (VipsImage* image, const char* name); + +char* vips_filename_get_filename (const char* vips_filename); +char* vips_filename_get_options (const char* vips_filename); + +VipsImage* vips_image_new_temp_file (const char* format); + +int vips_image_write (VipsImage* image, VipsImage* out); +void* vips_image_write_to_memory (VipsImage* in, size_t* size_out); + +typedef struct _VipsInterpolate { + VipsObject parent_object; + + // more +} VipsInterpolate; + +VipsInterpolate* vips_interpolate_new (const char* name); + +typedef struct _VipsOperation { + VipsObject parent_instance; + + // more +} VipsOperation; + +VipsOperation* vips_operation_new (const char* name); + +typedef void* (*VipsArgumentMapFn) (VipsObject* object, + GParamSpec* pspec, + VipsArgumentClass* argument_class, + VipsArgumentInstance* argument_instance, + void* a, void* b); + +void* vips_argument_map (VipsObject* object, + VipsArgumentMapFn fn, void* a, void* b); + +typedef struct _VipsRegion { + VipsObject parent_object; + + // more +} VipsRegion; +VipsRegion* vips_region_new (VipsImage*); + +VipsOperation* vips_cache_operation_build (VipsOperation* operation); +void vips_object_unref_outputs (VipsObject* object); + +int vips_operation_get_flags (VipsOperation* operation); + +void vips_concurrency_set( int concurrency ); + +void vips_cache_set_max (int max); +void vips_cache_set_max_mem (size_t max_mem); +void vips_cache_set_max_files (int max_files); +void vips_cache_set_trace (int trace); + +int vips_cache_get_max(); +int vips_cache_get_size(); +size_t vips_cache_get_max_mem(); +int vips_cache_get_max_files(); + +size_t vips_tracked_get_mem_highwater(); +size_t vips_tracked_get_mem(); +int vips_tracked_get_allocs(); +int vips_tracked_get_files(); + +char** vips_image_get_fields (VipsImage* image); +int vips_image_hasalpha (VipsImage* image); + +GType vips_blend_mode_get_type (void); +void vips_value_set_blob_free (GValue* value, void* data, size_t length); + +int vips_object_get_args (VipsObject* object, + const char*** names, int** flags, int* n_args); +CPP; + + if (self::atLeast(8, 8)) { + $vips_decls = $vips_decls . <<<'CPP' +char** vips_foreign_get_suffixes (void); + +void* vips_region_fetch (VipsRegion*, int, int, int, int, + size_t* length); +int vips_region_width (VipsRegion*); +int vips_region_height (VipsRegion*); +int vips_image_get_page_height (VipsImage*); +int vips_image_get_n_pages (VipsImage*); +CPP; + } + + if (self::atLeast(8, 8)) { + $vips_decls = $vips_decls . <<<'CPP' +typedef struct _VipsConnection { + VipsObject parent_object; + + // more +} VipsConnection; + +const char* vips_connection_filename (VipsConnection* stream); +const char* vips_connection_nick (VipsConnection* stream); + +typedef struct _VipsSource { + VipsConnection parent_object; + + // more +} VipsSource; + +VipsSource* vips_source_new_from_descriptor (int descriptor); +VipsSource* vips_source_new_from_file (const char* filename); +VipsSource* vips_source_new_from_memory (const void* data, + size_t size); + +typedef struct _VipsSourceCustom { + VipsSource parent_object; + + // more +} VipsSourceCustom; + +VipsSourceCustom* vips_source_custom_new (void); + +typedef struct _VipsTarget { + VipsConnection parent_object; + + // more +} VipsTarget; + +VipsTarget* vips_target_new_to_descriptor (int descriptor); +VipsTarget* vips_target_new_to_file (const char* filename); +VipsTarget* vips_target_new_to_memory (void); + +typedef struct _VipsTargetCustom { + VipsTarget parent_object; + + // more +} VipsTargetCustom; + +VipsTargetCustom* vips_target_custom_new (void); + +const char* vips_foreign_find_load_source (VipsSource *source); +const char* vips_foreign_find_save_target (const char* suffix); +CPP; + } + + Utils::debugLog("init", ["binding ..."]); + + /** + * We can sometimes get dependent libraries from libvips -- either the platform + * will open dependencies for us automatically, or the libvips binary has been + * built to includes all main dependencies (common on Windows, can happen + * elsewhere). + * + * We must get GLib functions from libvips if we can, since it will be the + * one that libvips itself is using, and they will share runtime types. + */ + self::$glib = + self::libraryLoad($vips_libname, $glib_decls) ?? + self::libraryLoad($glib_libname, $glib_decls); + self::$gobject = + self::libraryLoad($vips_libname, $gobject_decls) ?? + self::libraryLoad($gobject_libname, $gobject_decls); + + self::$vips = self::libraryLoad($vips_libname, $vips_decls); + + # Useful for debugging + # self::$vips->vips_leak_set(1); + + # force the creation of some types we need + self::$vips->vips_blend_mode_get_type(); + self::$vips->vips_interpretation_get_type(); + self::$vips->vips_operation_flags_get_type(); + self::$vips->vips_band_format_get_type(); + self::$vips->vips_token_get_type(); + self::$vips->vips_saveable_get_type(); + self::$vips->vips_image_type_get_type(); + + // look these up in advance + self::$ctypes = [ + "GObject" => self::$gobject->type("GObject*"), + "GClosure" => self::$gobject->type("GClosure"), + "GParamSpec" => self::$gobject->type("GParamSpec*"), + "VipsObject" => self::$vips->type("VipsObject*"), + "VipsOperation" => self::$vips->type("VipsOperation*"), + "VipsImage" => self::$vips->type("VipsImage*"), + "VipsInterpolate" => self::$vips->type("VipsInterpolate*"), + "VipsProgress" => self::$vips->type("VipsProgress*"), + ]; + + if (self::atLeast(8, 9)) { + self::$ctypes = array_merge(self::$ctypes, [ + "VipsConnection" => self::$vips->type("VipsConnection*"), + "VipsSource" => self::$vips->type("VipsSource*"), + "VipsSourceCustom" => self::$vips->type("VipsSourceCustom*"), + "VipsTarget" => self::$vips->type("VipsTarget*"), + "VipsTargetCustom" => self::$vips->type("VipsTargetCustom*"), + ]); + } + + self::$gtypes = [ + "gboolean" => self::$gobject->g_type_from_name("gboolean"), + "gint" => self::$gobject->g_type_from_name("gint"), + "gint64" => self::$gobject->g_type_from_name("gint64"), + "guint64" => self::$gobject->g_type_from_name("guint64"), + "gdouble" => self::$gobject->g_type_from_name("gdouble"), + "gchararray" => self::$gobject->g_type_from_name("gchararray"), + "VipsRefString" => self::$gobject->g_type_from_name("VipsRefString"), + + "GEnum" => self::$gobject->g_type_from_name("GEnum"), + "GFlags" => self::$gobject->g_type_from_name("GFlags"), + "VipsBandFormat" => self::$gobject->g_type_from_name("VipsBandFormat"), + "VipsBlendMode" => self::$gobject->g_type_from_name("VipsBlendMode"), + "VipsArrayInt" => self::$gobject->g_type_from_name("VipsArrayInt"), + "VipsArrayDouble" => + self::$gobject->g_type_from_name("VipsArrayDouble"), + "VipsArrayImage" => self::$gobject->g_type_from_name("VipsArrayImage"), + "VipsBlob" => self::$gobject->g_type_from_name("VipsBlob"), + + "GObject" => self::$gobject->g_type_from_name("GObject"), + "VipsImage" => self::$gobject->g_type_from_name("VipsImage"), + + "GClosure" => self::$gobject->g_type_from_name("GClosure"), + ]; + + // map vips format names to c type names + self::$ftypes = [ + "char" => "char", + "uchar" => "unsigned char", + "short" => "short", + "ushort" => "unsigned short", + "int" => "int", + "uint" => "unsigned int", + "float" => "float", + "double" => "double", + "complex" => "float", + "dpcomplex" => "double", + ]; + + Utils::debugLog("init", ["done"]); + self::$ffi_inited = true; + } +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + * vim600: expandtab sw=4 ts=4 fdm=marker + * vim<600: expandtab sw=4 ts=4 + */ diff --git a/src/Token.php b/src/FailOn.php similarity index 90% rename from src/Token.php rename to src/FailOn.php index b48aa2c..eaa4d9c 100644 --- a/src/Token.php +++ b/src/FailOn.php @@ -39,7 +39,7 @@ namespace Jcupitt\Vips; /** - * The Token enum. + * The FailOn enum. * @category Images * @package Jcupitt\Vips * @author John Cupitt @@ -47,11 +47,10 @@ * @license https://opensource.org/licenses/MIT MIT * @link https://github.com/jcupitt/php-vips */ -abstract class Token +abstract class FailOn { - const LEFT = 'left'; - const RIGHT = 'right'; - const STRING = 'string'; - const EQUALS = 'equals'; - const COMMA = 'comma'; + const NONE = 'none'; + const TRUNCATED = 'truncated'; + const ERROR = 'error'; + const WARNING = 'warning'; } diff --git a/src/ForeignDzContainer.php b/src/ForeignDzContainer.php index 5ba8d34..054b853 100644 --- a/src/ForeignDzContainer.php +++ b/src/ForeignDzContainer.php @@ -51,4 +51,5 @@ abstract class ForeignDzContainer { const FS = 'fs'; const ZIP = 'zip'; + const SZI = 'szi'; } diff --git a/src/ForeignDzLayout.php b/src/ForeignDzLayout.php index c06e3c7..ae36259 100644 --- a/src/ForeignDzLayout.php +++ b/src/ForeignDzLayout.php @@ -52,4 +52,6 @@ abstract class ForeignDzLayout const DZ = 'dz'; const ZOOMIFY = 'zoomify'; const GOOGLE = 'google'; + const IIIF = 'iiif'; + const IIIF3 = 'iiif3'; } diff --git a/src/ForeignHeifCompression.php b/src/ForeignHeifCompression.php new file mode 100644 index 0000000..926b1ab --- /dev/null +++ b/src/ForeignHeifCompression.php @@ -0,0 +1,56 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * The ForeignHeifCompression enum. + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +abstract class ForeignHeifCompression +{ + const HEVC = 'hevc'; + const AVC = 'avc'; + const JPEG = 'jpeg'; + const AV1 = 'av1'; +} diff --git a/src/ForeignHeifEncoder.php b/src/ForeignHeifEncoder.php new file mode 100644 index 0000000..718644b --- /dev/null +++ b/src/ForeignHeifEncoder.php @@ -0,0 +1,57 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * The ForeignHeifEncoder enum. + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +abstract class ForeignHeifEncoder +{ + const AUTO = 'auto'; + const AOM = 'aom'; + const RAV1E = 'rav1e'; + const SVT = 'svt'; + const X265 = 'x265'; +} diff --git a/src/ForeignJpegSubsample.php b/src/ForeignJpegSubsample.php new file mode 100644 index 0000000..f87bfa7 --- /dev/null +++ b/src/ForeignJpegSubsample.php @@ -0,0 +1,55 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * The ForeignJpegSubsample enum. + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +abstract class ForeignJpegSubsample +{ + const AUTO = 'auto'; + const ON = 'on'; + const OFF = 'off'; +} diff --git a/src/ForeignKeep.php b/src/ForeignKeep.php new file mode 100644 index 0000000..b7bec4b --- /dev/null +++ b/src/ForeignKeep.php @@ -0,0 +1,59 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * The ForeignKeep flags. + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +abstract class ForeignKeep +{ + const NONE = 0; + const EXIF = 1; + const XMP = 2; + const IPTC = 4; + const ICC = 8; + const OTHER = 16; + const ALL = 31; +} diff --git a/src/ForeignPngFilter.php b/src/ForeignPngFilter.php new file mode 100644 index 0000000..485c05d --- /dev/null +++ b/src/ForeignPngFilter.php @@ -0,0 +1,58 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * The ForeignPngFilter flags. + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +abstract class ForeignPngFilter +{ + const NONE = 8; + const SUB = 16; + const UP = 32; + const AVG = 64; + const PAETH = 128; + const ALL = 248; +} diff --git a/src/ForeignPpmFormat.php b/src/ForeignPpmFormat.php new file mode 100644 index 0000000..e476cc6 --- /dev/null +++ b/src/ForeignPpmFormat.php @@ -0,0 +1,57 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * The ForeignPpmFormat enum. + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +abstract class ForeignPpmFormat +{ + const PBM = 'pbm'; + const PGM = 'pgm'; + const PPM = 'ppm'; + const PFM = 'pfm'; + const PNM = 'pnm'; +} diff --git a/src/ForeignSubsample.php b/src/ForeignSubsample.php new file mode 100644 index 0000000..c46f73f --- /dev/null +++ b/src/ForeignSubsample.php @@ -0,0 +1,55 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * The ForeignSubsample enum. + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +abstract class ForeignSubsample +{ + const AUTO = 'auto'; + const ON = 'on'; + const OFF = 'off'; +} diff --git a/src/ForeignTiffCompression.php b/src/ForeignTiffCompression.php index 6354e6e..3c459f1 100644 --- a/src/ForeignTiffCompression.php +++ b/src/ForeignTiffCompression.php @@ -55,4 +55,7 @@ abstract class ForeignTiffCompression const PACKBITS = 'packbits'; const CCITTFAX4 = 'ccittfax4'; const LZW = 'lzw'; + const WEBP = 'webp'; + const ZSTD = 'zstd'; + const JP2K = 'jp2k'; } diff --git a/src/ForeignWebpPreset.php b/src/ForeignWebpPreset.php index 44f84ef..cfbf0b0 100644 --- a/src/ForeignWebpPreset.php +++ b/src/ForeignWebpPreset.php @@ -49,7 +49,7 @@ */ abstract class ForeignWebpPreset { - const DEFAULT = 'default'; + const DEFAULT1 = 'default'; const PICTURE = 'picture'; const PHOTO = 'photo'; const DRAWING = 'drawing'; diff --git a/src/GObject.php b/src/GObject.php new file mode 100644 index 0000000..50ae8d1 --- /dev/null +++ b/src/GObject.php @@ -0,0 +1,285 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/libvips/php-vips + */ + +namespace Jcupitt\Vips; + +use Closure; +use FFI\CData; + +/** + * This class holds a pointer to a GObject and manages object lifetime. + * + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/libvips/php-vips + */ +abstract class GObject +{ + /** + * A pointer to the underlying GObject. + * + * @internal + */ + private CData $pointer; + + /** + * libvips executes FFI callbacks off the main thread and this confuses + * the stack limit checks available since PHP 8.3.0. We need to check + * if `zend.max_allowed_stack_size` is set to `-1`. + * See: https://github.com/libvips/php-vips/pull/237. + */ + private static bool $check_max_stack_size = true; + + /** + * Wrap a GObject around an underlying vips resource. The GObject takes + * ownership of the pointer and will unref it on finalize. + * + * Don't call this yourself, users should stick to (for example) + * Image::newFromFile(). + * + * @param CData $pointer The underlying pointer that this + * object should wrap. + * + * @internal + */ + public function __construct(CData $pointer) + { + $this->pointer = FFI::vips()->cast(FFI::ctypes("GObject"), $pointer); + } + + public function __destruct() + { + $this->unref(); + } + + public function __clone() + { + $this->ref(); + } + + public function ref(): void + { + FFI::gobject()->g_object_ref($this->pointer); + } + + public function unref(): void + { + FFI::gobject()->g_object_unref($this->pointer); + } + + /** + * Connect to a signal on this object. + * The callback will be triggered every time this signal is issued on this instance. + * @throws Exception + */ + public function signalConnect(string $name, callable $callback): void + { + if (self::$check_max_stack_size) { + $max_allowed_stack_size = ini_get('zend.max_allowed_stack_size'); + if ($max_allowed_stack_size !== false && + $max_allowed_stack_size !== '-1') { + throw new Exception("signalConnect() requires zend.max_allowed_stack_size set to '-1'"); + } + + self::$check_max_stack_size = false; + } + + $marshaler = self::getMarshaler($name, $callback); + if ($marshaler === null) { + throw new Exception("unsupported signal $name"); + } + + $gc = FFI::newGClosure(); + FFI::gobject()->g_closure_set_marshal($gc, $marshaler); + FFI::gobject()->g_signal_connect_closure($this->pointer, $name, $gc, 0); + } + + private static function getMarshaler(string $name, callable $callback): ?Closure + { + switch ($name) { + case 'preeval': + case 'eval': + case 'posteval': + return static function ( + CData $gClosure, + ?CData $returnValue, + int $numberOfParams, + CData $params, + CData $hint, + ?CData $data + ) use (&$callback) { + assert($numberOfParams === 2); + /** + * Signature: void(VipsImage* image, void* progress, void* handle) + */ + $vi = FFI::gobject()->g_value_get_object(\FFI::addr($params[0])); + FFI::gobject()->g_object_ref($vi); + $image = new Image($vi); + $pr = FFI::vips()->cast( + FFI::ctypes('VipsProgress'), + FFI::gobject()->g_value_get_pointer(\FFI::addr($params[1])) + ); + $callback($image, $pr); + }; + case 'read': + if (FFI::atLeast(8, 9)) { + return static function ( + CData $gClosure, + CData $returnValue, + int $numberOfParams, + CData $params, + CData $hint, + ?CData $data + ) use (&$callback): void { + assert($numberOfParams === 3); + /** + * Signature: gint64(VipsSourceCustom* source, void* buffer, gint64 length, void* handle) + */ + $bufferLength = (int)FFI::gobject()->g_value_get_int64(\FFI::addr($params[2])); + $returnBuffer = $callback($bufferLength); + $returnBufferLength = 0; + + if ($returnBuffer !== null) { + $returnBufferLength = strlen($returnBuffer); + $bufferPointer = FFI::gobject()->g_value_get_pointer(\FFI::addr($params[1])); + \FFI::memcpy($bufferPointer, $returnBuffer, $returnBufferLength); + } + FFI::gobject()->g_value_set_int64($returnValue, $returnBufferLength); + }; + } + + return null; + case 'seek': + if (FFI::atLeast(8, 9)) { + return static function ( + CData $gClosure, + CData $returnValue, + int $numberOfParams, + CData $params, + CData $hint, + ?CData $data + ) use (&$callback): void { + assert($numberOfParams === 3); + /** + * Signature: gint64(VipsSourceCustom* source, gint64 offset, int whence, void* handle) + */ + $offset = (int)FFI::gobject()->g_value_get_int64(\FFI::addr($params[1])); + $whence = (int)FFI::gobject()->g_value_get_int(\FFI::addr($params[2])); + FFI::gobject()->g_value_set_int64($returnValue, $callback($offset, $whence)); + }; + } + + return null; + case 'write': + if (FFI::atLeast(8, 9)) { + return static function ( + CData $gClosure, + CData $returnValue, + int $numberOfParams, + CData $params, + CData $hint, + ?CData $data + ) use (&$callback): void { + assert($numberOfParams === 3); + /** + * Signature: gint64(VipsTargetCustom* target, void* buffer, gint64 length, void* handle) + */ + $bufferPointer = FFI::gobject()->g_value_get_pointer(\FFI::addr($params[1])); + $bufferLength = (int)FFI::gobject()->g_value_get_int64(\FFI::addr($params[2])); + $buffer = \FFI::string($bufferPointer, $bufferLength); + $returnBufferLength = $callback($buffer); + FFI::gobject()->g_value_set_int64($returnValue, $returnBufferLength); + }; + } + + return null; + case 'finish': + if (FFI::atLeast(8, 9)) { + return static function ( + CData $gClosure, + ?CData $returnValue, + int $numberOfParams, + CData $params, + CData $hint, + ?CData $data + ) use (&$callback): void { + assert($numberOfParams === 1); + /** + * Signature: void(VipsTargetCustom* target, void* handle) + */ + $callback(); + }; + } + + return null; + case 'end': + if (FFI::atLeast(8, 13)) { + return static function ( + CData $gClosure, + CData $returnValue, + int $numberOfParams, + CData $params, + CData $hint, + ?CData $data + ) use (&$callback): void { + assert($numberOfParams === 1); + /** + * Signature: int(VipsTargetCustom* target, void* handle) + */ + FFI::gobject()->g_value_set_int($returnValue, $callback()); + }; + } + + return null; + default: + return null; + } + } +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + * vim600: expandtab sw=4 ts=4 fdm=marker + * vim<600: expandtab sw=4 ts=4 + */ diff --git a/src/GValue.php b/src/GValue.php new file mode 100644 index 0000000..2bd657e --- /dev/null +++ b/src/GValue.php @@ -0,0 +1,353 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/libvips/php-vips + */ + +namespace Jcupitt\Vips; + +class GValue +{ + private \FFI\CData $struct; + public \FFI\CData $pointer; + + public function __construct() + { + # allocate a gvalue on the heap, and make it persistent between requests + $this->struct = FFI::gobject()->new("GValue", true, true); + $this->pointer = \FFI::addr($this->struct); + + # GValue needs to be inited to all zero + \FFI::memset($this->pointer, 0, \FFI::sizeof($this->struct)); + } + + /** + * Turn a string into an enum value, if possible + * @throws Exception + */ + public static function toEnum(int $gtype, $value): int + { + if (is_string($value)) { + $enum_value = FFI::vips()-> + vips_enum_from_nick("php-vips", $gtype, $value); + if ($enum_value < 0) { + throw new Exception(); + } + } else { + $enum_value = $value; + } + + return $enum_value; + } + + /** + * Turn an enum into a string, if possible + * @throws Exception + */ + public static function fromEnum(int $gtype, int $value): string + { + $result = FFI::vips()->vips_enum_nick($gtype, $value); + if ($result === null) { + throw new Exception("value not in enum"); + } + + return $result; + } + + public function __destruct() + { + FFI::gobject()->g_value_unset($this->pointer); + } + + public function setType(int $gtype): void + { + FFI::gobject()->g_value_init($this->pointer, $gtype); + } + + public function getType(): int + { + return $this->pointer->g_type; + } + + /** + * Set a GValue. + * + * @param mixed $value Value to be set. + * + * @throws Exception + */ + public function set($value): void + { + $gtype = $this->getType(); + + switch ($gtype) { + case FFI::gtypes("gboolean"): + FFI::gobject()->g_value_set_boolean($this->pointer, $value); + break; + + case FFI::gtypes("gint"): + FFI::gobject()->g_value_set_int($this->pointer, $value); + break; + + case FFI::gtypes("gint64"): + FFI::gobject()->g_value_set_int64($this->pointer, $value); + break; + + case FFI::gtypes("guint64"): + FFI::gobject()->g_value_set_uint64($this->pointer, $value); + break; + + case FFI::gtypes("gdouble"): + FFI::gobject()->g_value_set_double($this->pointer, $value); + break; + + case FFI::gtypes("gchararray"): + FFI::gobject()->g_value_set_string($this->pointer, $value); + break; + + case FFI::gtypes("VipsRefString"): + FFI::vips()-> + vips_value_set_ref_string($this->pointer, $value); + break; + + case FFI::gtypes("VipsArrayInt"): + if (!is_array($value)) { + $value = [$value]; + } + $n = count($value); + $array = FFI::vips()->new("int[$n]"); + for ($i = 0; $i < $n; $i++) { + $array[$i] = $value[$i]; + } + FFI::vips()-> + vips_value_set_array_int($this->pointer, $array, $n); + break; + + case FFI::gtypes("VipsArrayDouble"): + if (!is_array($value)) { + $value = [$value]; + } + $n = count($value); + $array = FFI::vips()->new("double[$n]"); + for ($i = 0; $i < $n; $i++) { + $array[$i] = $value[$i]; + } + FFI::vips()-> + vips_value_set_array_double($this->pointer, $array, $n); + break; + + case FFI::gtypes("VipsArrayImage"): + if (!is_array($value)) { + $value = [$value]; + } + $n = count($value); + FFI::vips()->vips_value_set_array_image($this->pointer, $n); + $array = FFI::vips()-> + vips_value_get_array_image($this->pointer, null); + for ($i = 0; $i < $n; $i++) { + $image = $value[$i]; + $array[$i] = $image->pointer; + $image->ref(); + } + break; + + case FFI::gtypes("VipsBlob"): + # we need to set the blob to a copy of the data that vips_lib + # can own and free + $n = strlen($value); + $memory = FFI::vips()->new("char[$n]", false, true); + \FFI::memcpy($memory, $value, $n); + FFI::vips()-> + vips_value_set_blob_free($this->pointer, $memory, $n); + break; + + default: + $fundamental = FFI::gobject()->g_type_fundamental($gtype); + switch ($fundamental) { + case FFI::gtypes("GObject"): + FFI::gobject()-> + g_value_set_object($this->pointer, $value->pointer); + break; + + case FFI::gtypes("GEnum"): + FFI::gobject()->g_value_set_enum( + $this->pointer, + self::toEnum($gtype, $value) + ); + break; + + case FFI::gtypes("GFlags"): + /* Just set as int. + */ + FFI::gobject()-> + g_value_set_flags($this->pointer, $value); + break; + + default: + $typeName = FFI::gobject()->g_type_name($gtype); + throw new \BadMethodCallException( + "gtype $typeName ($gtype) not implemented" + ); + } + } + } + + /** + * Get the contents of a GValue. + * + * @return mixed The contents of this GValue. + * + * @throws Exception + */ + public function get() + { + $gtype = $this->getType(); + $result = null; + + switch ($gtype) { + case FFI::gtypes("gboolean"): + $result = FFI::gobject()->g_value_get_boolean($this->pointer); + break; + + case FFI::gtypes("gint"): + $result = FFI::gobject()->g_value_get_int($this->pointer); + break; + + case FFI::gtypes("gint64"): + $result = FFI::gobject()->g_value_get_int64($this->pointer); + break; + + case FFI::gtypes("guint64"): + $result = FFI::gobject()->g_value_get_uint64($this->pointer); + break; + + case FFI::gtypes("gdouble"): + $result = FFI::gobject()->g_value_get_double($this->pointer); + break; + + case FFI::gtypes("gchararray"): + $result = FFI::gobject()->g_value_get_string($this->pointer); + break; + + case FFI::gtypes("VipsRefString"): + $p_size = FFI::vips()->new("size_t[1]"); + $result = FFI::vips()-> + vips_value_get_ref_string($this->pointer, $p_size); + # $p_size[0] will be the string length, but assume it's null + # terminated + break; + + case FFI::gtypes("VipsImage"): + $pointer = FFI::gobject()->g_value_get_object($this->pointer); + $result = new Image($pointer); + // get_object does not increment the ref count + $result->ref(); + break; + + case FFI::gtypes("VipsArrayInt"): + $p_len = FFI::vips()->new("int[1]"); + $pointer = FFI::vips()-> + vips_value_get_array_int($this->pointer, $p_len); + $result = []; + for ($i = 0; $i < $p_len[0]; $i++) { + $result[] = $pointer[$i]; + } + break; + + case FFI::gtypes("VipsArrayDouble"): + $p_len = FFI::vips()->new("int[1]"); + $pointer = FFI::vips()-> + vips_value_get_array_double($this->pointer, $p_len); + $result = []; + for ($i = 0; $i < $p_len[0]; $i++) { + $result[] = $pointer[$i]; + } + break; + + case FFI::gtypes("VipsArrayImage"): + $p_len = FFI::vips()->new("int[1]"); + $pointer = FFI::vips()-> + vips_value_get_array_image($this->pointer, $p_len); + $result = []; + for ($i = 0; $i < $p_len[0]; $i++) { + $image = new Image($pointer[$i]); + $image->ref(); + $result[] = $image; + } + break; + + case FFI::gtypes("VipsBlob"): + $p_len = FFI::vips()->new("size_t[1]"); + $pointer = FFI::vips()-> + vips_value_get_blob($this->pointer, $p_len); + $result = \FFI::string($pointer, $p_len[0]); + break; + + default: + $fundamental = FFI::gobject()->g_type_fundamental($gtype); + switch ($fundamental) { + case FFI::gtypes("GEnum"): + $result = FFI::gobject()-> + g_value_get_enum($this->pointer); + $result = self::fromEnum($gtype, $result); + break; + + case FFI::gtypes("GFlags"): + /* Just get as int. + */ + $result = FFI::gobject()-> + g_value_get_flags($this->pointer); + break; + + default: + $typeName = FFI::gobject()->g_type_name($gtype); + throw new \BadMethodCallException( + "gtype $typeName ($gtype) not implemented" + ); + } + } + + return $result; + } +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/src/GsfOutputCsvQuotingMode.php b/src/GsfOutputCsvQuotingMode.php new file mode 100644 index 0000000..a8a08f0 --- /dev/null +++ b/src/GsfOutputCsvQuotingMode.php @@ -0,0 +1,54 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * The GsfOutputCsvQuotingMode enum. + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +abstract class GsfOutputCsvQuotingMode +{ + const NEVER = 'never'; + const AUTO = 'auto'; +} diff --git a/src/Image.php b/src/Image.php index 2889852..afc9b45 100644 --- a/src/Image.php +++ b/src/Image.php @@ -42,11 +42,8 @@ * This class represents a Vips image object. * * This module provides a binding for the [vips image processing - * library](https://jcupitt.github.io/libvips/). - * - * It needs libvips 8.0 or later to be installed, and it needs the binary - * [`vips` extension](https://github.com/jcupitt/php-vips-ext) to be added to - * your PHP. + * library](https://libvips.org) version 8.7 and later, and requires PHP 7.4 + * and later. * * # Example * @@ -69,7 +66,7 @@ * * ``` * "require": { - * "jcupitt/vips" : "@dev" + * "jcupitt/vips" : "2.0.0" * } * ``` * @@ -99,8 +96,12 @@ * default mode is `random`, this allows for full random access to image pixels, * but is slower and needs more memory. * - * You can also load formatted images from - * strings or create images from PHP arrays. + * You can also load formatted images from strings or create images from + * PHP arrays. + * + * See the [main libvips + * documentation](https://www.libvips.org/API/current/VipsImage.html#vips-image-new-from-file) + * for a more detailed explaination. * * The next line: * @@ -113,6 +114,8 @@ * conform to PHP naming conventions, you can use something like * `$image->get('ipct-data')`. * + * Use `$image->getFields()` to get an array of all the possible field names. + * * Next we have: * * ```php @@ -137,7 +140,8 @@ * * `Image::writeToFile` writes an image back to the filesystem. It can * write any format supported by vips: the file type is set from the filename - * suffix. You can also write formatted images to strings. + * suffix. You can write formatted images to strings, and pixel values to + * arrays. * * # Getting more help * @@ -212,26 +216,6 @@ * Use `$image->get('ipct-data')` for property names which are not valid under * PHP syntax. * - * # How it works - * - * The binary - * [`vips` extension](https://github.com/jcupitt/php-vips-ext) adds a few extra - * functions to PHP to let you call anything in the libvips library. The API - * it provides is simple, but horrible. - * - * This module is pure PHP and builds on the binary extension to provide a - * convenient interface for programmers. It uses the PHP magic methods - * `__call()`, `__callStatic()`, `__get()` and `__set()` to make vips operators - * appear as methods on the `Image` class, and vips properties as PHP - * properties. - * - * The API you end up with is a object-oriented version of the [VIPS C - * API](https://jcupitt.github.io/libvips/API/current). - * Full documentation - * on the operations and what they do is there, you can use it directly. This - * document explains the extra features of the PHP API and lists the available - * operations very briefly. - * * # Automatic wrapping * * This binding has a `__call()` method and uses @@ -248,8 +232,8 @@ * produces several results. * * For example, `Image::min`, the vips operation that searches an image for - * the minimum value, has a large number of optional arguments. You can use it to - * find the minimum value like this: + * the minimum value, has a large number of optional arguments. You can use it + * to find the minimum value like this: * * ```php * $min_value = $image->min(); @@ -499,102 +483,25 @@ class Image extends ImageAutodoc implements \ArrayAccess { /** - * Map load nicknames to canonical names. Regenerate this table with - * something like: - * - * $ vips -l foreign | grep -i load | awk '{ print $2, $1; }' - * - * Plus a bit of editing. - * - * @internal - */ - private static $nicknameToCanonical = [ - 'csvload' => 'VipsForeignLoadCsv', - 'matrixload' => 'VipsForeignLoadMatrix', - 'rawload' => 'VipsForeignLoadRaw', - 'vipsload' => 'VipsForeignLoadVips', - 'analyzeload' => 'VipsForeignLoadAnalyze', - 'ppmload' => 'VipsForeignLoadPpm', - 'radload' => 'VipsForeignLoadRad', - 'pdfload' => 'VipsForeignLoadPdfFile', - 'pdfload_buffer' => 'VipsForeignLoadPdfBuffer', - 'svgload' => 'VipsForeignLoadSvgFile', - 'svgload_buffer' => 'VipsForeignLoadSvgBuffer', - 'gifload' => 'VipsForeignLoadGifFile', - 'gifload_buffer' => 'VipsForeignLoadGifBuffer', - 'pngload' => 'VipsForeignLoadPng', - 'pngload_buffer' => 'VipsForeignLoadPngBuffer', - 'matload' => 'VipsForeignLoadMat', - 'jpegload' => 'VipsForeignLoadJpegFile', - 'jpegload_buffer' => 'VipsForeignLoadJpegBuffer', - 'webpload' => 'VipsForeignLoadWebpFile', - 'webpload_buffer' => 'VipsForeignLoadWebpBuffer', - 'tiffload' => 'VipsForeignLoadTiffFile', - 'tiffload_buffer' => 'VipsForeignLoadTiffBuffer', - 'magickload' => 'VipsForeignLoadMagickFile', - 'magickload_buffer' => 'VipsForeignLoadMagickBuffer', - 'fitsload' => 'VipsForeignLoadFits', - 'openexrload' => 'VipsForeignLoadOpenexr' - ]; - - /** - * Combine takes an array of blend modes, passed to libvips as an array of - * int. Because libvips does now know they should be enums, we have to do - * the string->int conversion ourselves. We ought to introspect to find the - * mapping, but until we have the machinery for that, we just hardwire the - * mapping here. + * A pointer to the underlying VipsImage. This is the same as the + * GObject, just cast to VipsImage to help FFI. * * @internal */ - private static $blendModeToInt = [ - BlendMode::CLEAR => 0, - BlendMode::SOURCE => 1, - BlendMode::OVER => 2, - BlendMode::IN => 3, - BlendMode::OUT => 4, - BlendMode::ATOP => 5, - BlendMode::DEST => 6, - BlendMode::DEST_OVER => 7, - BlendMode::DEST_IN => 8, - BlendMode::DEST_OUT => 9, - BlendMode::DEST_ATOP => 10, - BlendMode::XOR => 11, - BlendMode::ADD => 12, - BlendMode::SATURATE => 13, - BlendMode::MULTIPLY => 14, - BlendMode::SCREEN => 15, - BlendMode::OVERLAY => 16, - BlendMode::DARKEN => 17, - BlendMode::LIGHTEN => 18, - BlendMode::COLOUR_DODGE => 19, - BlendMode::COLOUR_BURN => 20, - BlendMode::HARD_LIGHT => 21, - BlendMode::SOFT_LIGHT => 22, - BlendMode::DIFFERENCE => 23, - BlendMode::EXCLUSION => 24 - ]; - - /** - * The resource for the underlying VipsImage. - * - * @internal - */ - private $image; + public \FFI\CData $pointer; /** - * Wrap a Image around an underlying vips resource. + * Wrap an Image around an underlying CData pointer. * * Don't call this yourself, users should stick to (for example) * Image::newFromFile(). * - * @param resource $image The underlying vips image resource that this - * class should wrap. - * * @internal */ - public function __construct($image) + public function __construct(\FFI\CData $pointer) { - $this->image = $image; + $this->pointer = FFI::vips()->cast(FFI::ctypes("VipsImage"), $pointer); + parent::__construct($pointer); } /** @@ -612,12 +519,10 @@ private static function mapNumeric($value, \Closure $func) { if (is_numeric($value)) { $value = $func($value); - } else { - if (is_array($value)) { - array_walk_recursive($value, function (&$value) use ($func) { - $value = self::mapNumeric($value, $func); - }); - } + } elseif (is_array($value)) { + array_walk_recursive($value, function (&$value) use ($func) { + $value = self::mapNumeric($value, $func); + }); } return $value; @@ -664,16 +569,15 @@ private static function is2D($value): bool * * @internal */ - private static function isImageish($value): bool + public static function isImageish($value): bool { return self::is2D($value) || $value instanceof Image; } /** * Turn a constant (eg. 1, '12', [1, 2, 3], [[1]]) into an image using - * match_image as a guide. + * this as a guide. * - * @param Image $match_image Use this image as a guide. * @param mixed $value Turn this into an image. * * @throws Exception @@ -682,131 +586,126 @@ private static function isImageish($value): bool * * @internal */ - private static function imageize(Image $match_image, $value): Image + public function imageize($value): Image { - if (self::is2D($value)) { - $result = self::newFromArray($value); + if ($value instanceof Image) { + return $value; + } elseif (self::is2D($value)) { + return self::newFromArray($value); } else { - $result = $match_image->newFromImage($value); + return $this->newFromImage($value); } - - return $result; } /** - * Unwrap an array of stuff ready to pass down to the vips_ layer. We - * swap instances of the Image for the plain resource. + * Run a function expecting a complex image. If the image is not in complex + * format, try to make it complex by joining adjacant bands as real and + * imaginary. * - * @param array $result Unwrap this. + * @param \Closure $func The function to run. + * @param Image $image The image to run the function on. * - * @return array $result unwrapped, ready for vips. + * @throws Exception + * + * @return Image A new Image. * * @internal */ - private static function unwrap(array $result): array + private static function runCmplx(\Closure $func, Image $image): Image { - array_walk_recursive($result, function (&$value) { - if ($value instanceof Image) { - $value = $value->image; + $original_format = $image->format; + + if ($image->format != 'complex' && $image->format != 'dpcomplex') { + if ($image->bands % 2 != 0) { + throw new Exception('not an even number of bands'); } - }); - return $result; - } + if ($image->format != 'float' && $image->format != 'double') { + $image = $image->cast('float'); + } - /** - * Is $value a VipsImage. - * - * @param mixed $value The thing to test. - * - * @return bool true if this is a vips image resource. - * - * @internal - */ - private static function isImage($value): bool - { - return is_resource($value) && - get_resource_type($value) === 'GObject'; - } + if ($image->format == 'double') { + $new_format = 'dpcomplex'; + } else { + $new_format = 'complex'; + } - /** - * Wrap up the result of a vips_ call ready to return it to PHP. We do - * two things: - * - * - If the array is a singleton, we strip it off. For example, many - * operations return a single result and there's no sense handling - * this as an array of values, so we transform ['out' => x] -> x. - * - * - Any VipsImage resources are rewrapped as instances of Image. - * - * @param mixed $result Wrap this up. - * - * @return mixed $result, but wrapped up as a php class. - * - * @internal - */ - private static function wrapResult($result) - { - if (!is_array($result)) { - $result = ['x' => $result]; + $image = $image->copy(['format' => $new_format, + 'bands' => $image->bands / 2]); } - array_walk_recursive($result, function (&$item) { - if (self::isImage($item)) { - $item = new self($item); + $image = $func($image); + + if ($original_format != 'complex' && $original_format != 'dpcomplex') { + if ($image->format == 'dpcomplex') { + $new_format = 'double'; + } else { + $new_format = 'float'; } - }); - if (count($result) === 1) { - $result = array_shift($result); + $image = $image->copy(['format' => $new_format, + 'bands' => $image->bands * 2]); } - return $result; + return $image; } /** - * Throw a vips error as an exception. + * Handy for things like self::more. Call a 2-ary vips operator like + * 'more', but if the arg is not an image (i.e. it's a constant), call + * 'more_const' instead. + * + * @param mixed $other The right-hand argument. + * @param string $base The base part of the operation name. + * @param string $op The action to invoke. + * @param array $options An array of options to pass to the operation. * * @throws Exception * - * @return void + * @return mixed The operation result. * * @internal */ - private static function errorVips() - { - $message = vips_error_buffer(); - $exception = new Exception($message); - Utils::errorLog($message, $exception); - throw $exception; + private function callEnum( + $other, + string $base, + string $op, + array $options = [] + ) { + if (self::isImageish($other)) { + return VipsOperation::call($base, $this, [$other, $op], $options); + } else { + return VipsOperation::call( + $base . '_const', + $this, + [$op, $other], + $options + ); + } } /** - * Check the result of a vips_ call for an error, and throw an exception - * if we see one. - * - * This won't work for things like __get where a non-array return can be - * a valid return. - * - * @param mixed $result Test this. - * - * @throws Exception + * Find the name of the load operation vips will use to load a file, for + * example "VipsForeignLoadJpegFile". You can use this to work out what + * options to pass to newFromFile(). * - * @return void + * @param string $filename The file to test. * - * @internal + * @return string|null The name of the load operation, or null. */ - private static function errorIsArray($result) + public static function findLoad(string $filename): ?string { - if (!is_array($result)) { - self::errorVips(); - } + return FFI::vips()->vips_foreign_find_load($filename); } /** * Create a new Image from a file on disc. * - * @param string $filename The file to open. + * See the [main libvips + * documentation](https://www.libvips.org/API/current/VipsImage.html#vips-image-new-from-file) + * for a more detailed explaination. + * + * @param string $name The file to open. * @param array $options Any options to pass on to the load operation. * * @throws Exception @@ -814,69 +713,48 @@ private static function errorIsArray($result) * @return Image A new Image. */ public static function newFromFile( - string $filename, + string $name, array $options = [] ): Image { - Utils::debugLog('newFromFile', [ - 'instance' => null, - 'arguments' => [$filename, $options] - ]); + $filename = Utils::filenameGetFilename($name); + $string_options = Utils::filenameGetOptions($name); - $options = self::unwrap($options); - $result = vips_image_new_from_file($filename, $options); - self::errorIsArray($result); - $result = self::wrapResult($result); + $loader = self::findLoad($filename); + if ($loader == null) { + throw new Exception(); + } - Utils::debugLog('newFromFile', ['result' => $result]); + if (strlen($string_options) != 0) { + $options = array_merge([ + "string_options" => $string_options, + ], $options); + } - return $result; + return VipsOperation::call($loader, null, [$filename], $options); } /** - * Find the name of the load operation vips will use to load a file, for - * example "VipsForeignLoadJpegFile". You can use this to work out what - * options to pass to newFromFile(). + * Find the name of the load operation vips will use to load a buffer, for + * example 'VipsForeignLoadJpegBuffer'. You can use this to work out what + * options to pass to newFromBuffer(). * - * @param string $filename The file to test. + * @param string $buffer The formatted image to test. * * @return string|null The name of the load operation, or null. */ - public static function findLoad(string $filename) + public static function findLoadBuffer(string $buffer): ?string { - Utils::debugLog('findLoad', [ - 'instance' => null, - 'arguments' => [$filename] - ]); - - // added in 1.0.5 of the binary module - if (function_exists('vips_foreign_find_load')) { - $result = vips_foreign_find_load($filename); - } else { - $result = null; - - // fallback: use the vips-loader property ... this can be much slower - try { - $image = self::newFromFile($filename); - // Unfortunately, vips-loader is the operation nickname, rather - // than the canonical name returned by vips_foreign_find_load(). - $loader = $image->get('vips-loader'); - $result = self::$nicknameToCanonical[$loader]; - } catch (Exception $ignored) { - } - } - - Utils::debugLog('findLoad', ['result' => [$result]]); - - return $result; + return FFI::vips()-> + vips_foreign_find_load_buffer($buffer, strlen($buffer)); } /** * Create a new Image from a compressed image held as a string. * * @param string $buffer The formatted image to open. - * @param string $option_string Any text-style options to pass to the + * @param string $string_options Any text-style options to pass to the * selected loader. - * @param array $options Any options to pass on to the load operation. + * @param array $options Options to pass on to the load operation. * * @throws Exception * @@ -884,61 +762,21 @@ public static function findLoad(string $filename) */ public static function newFromBuffer( string $buffer, - string $option_string = '', + string $string_options = '', array $options = [] ): Image { - Utils::debugLog('newFromBuffer', [ - 'instance' => null, - 'arguments' => [$buffer, $option_string, $options] - ]); - - $options = self::unwrap($options); - $result = vips_image_new_from_buffer($buffer, $option_string, $options); - self::errorIsArray($result); - $result = self::wrapResult($result); - - Utils::debugLog('newFromBuffer', ['result' => $result]); - - return $result; - } - - /** - * Find the name of the load operation vips will use to load a buffer, for - * example 'VipsForeignLoadJpegBuffer'. You can use this to work out what - * options to pass to newFromBuffer(). - * - * @param string $buffer The formatted image to test. - * - * @return string|null The name of the load operation, or null. - */ - public static function findLoadBuffer(string $buffer) - { - Utils::debugLog('findLoadBuffer', [ - 'instance' => null, - 'arguments' => [$buffer] - ]); - - // added in 1.0.5 of the binary module - if (function_exists('vips_foreign_find_load_buffer')) { - $result = vips_foreign_find_load_buffer($buffer); - } else { - $result = null; - - // fallback: use the vips-loader property ... this can be much slower - try { - $image = self::newFromBuffer($buffer); - // Unfortunately, vips-loader is the operation nickname, rather - // than the canonical name returned by - // vips_foreign_find_load_buffer(). - $loader = $image->get('vips-loader'); - $result = self::$nicknameToCanonical[$loader]; - } catch (Exception $ignored) { - } + $loader = self::findLoadBuffer($buffer); + if ($loader == null) { + throw new Exception(); } - Utils::debugLog('findLoadBuffer', ['result' => [$result]]); + if (strlen($string_options) != 0) { + $options = array_merge([ + "string_options" => $string_options, + ], $options); + } - return $result; + return VipsOperation::call($loader, null, [$buffer], $options); } /** @@ -961,18 +799,30 @@ public static function newFromArray( float $scale = 1.0, float $offset = 0.0 ): Image { - Utils::debugLog('newFromArray', [ - 'instance' => null, - 'arguments' => [$array, $scale, $offset] - ]); + if (!self::is2D($array)) { + $array = [$array]; + } - $result = vips_image_new_from_array($array, $scale, $offset); - if ($result === -1) { - self::errorVips(); + $height = count($array); + $width = count($array[0]); + + $n = $width * $height; + $a = FFI::vips()->new("double[$n]", true, true); + for ($y = 0; $y < $height; $y++) { + for ($x = 0; $x < $width; $x++) { + $a[$x + $y * $width] = $array[$y][$x]; + } } - $result = self::wrapResult($result); - Utils::debugLog('newFromArray', ['result' => $result]); + $pointer = FFI::vips()-> + vips_image_new_matrix_from_array($width, $height, $a, $n); + if ($pointer == null) { + throw new Exception(); + } + $result = new Image($pointer); + + $result->setType(FFI::gtypes("gdouble"), 'scale', $scale); + $result->setType(FFI::gtypes("gdouble"), 'offset', $offset); return $result; } @@ -980,7 +830,7 @@ public static function newFromArray( /** * Wraps an Image around an area of memory containing a C-style array. * - * @param string $data C-style array. + * @param mixed $data C-style array. * @param int $width Image width in pixels. * @param int $height Image height in pixels. * @param int $bands Number of bands. @@ -991,51 +841,39 @@ public static function newFromArray( * @return Image A new Image. */ public static function newFromMemory( - string $data, + $data, int $width, int $height, int $bands, string $format ): Image { - Utils::debugLog('newFromMemory', [ - 'instance' => null, - 'arguments' => [$data, $width, $height, $bands, $format] - ]); - - $result = vips_image_new_from_memory($data, $width, $height, $bands, $format); - if ($result === -1) { - self::errorVips(); + /* Take a copy of the memory area to avoid lifetime issues. + * + * TODO add a references system instead, see pyvips. + */ + $pointer = FFI::vips()->vips_image_new_from_memory_copy( + $data, + strlen($data), + $width, + $height, + $bands, + $format + ); + if ($pointer == null) { + throw new Exception(); } - $result = self::wrapResult($result); - - Utils::debugLog('newFromMemory', ['result' => $result]); - return $result; + return new Image($pointer); } /** - * Make an interpolator from a name. + * Deprecated thing to make an interpolator. * - * @param string $name Name of the interpolator. - * Possible interpolators are: - * - `'nearest'`: Use nearest neighbour interpolation. - * - `'bicubic'`: Use bicubic interpolation. - * - `'bilinear'`: Use bilinear interpolation (the default). - * - `'nohalo'`: Use Nohalo interpolation. - * - `'lbb'`: Use LBB interpolation. - * - `'vsqbs'`: Use the VSQBS interpolation. - * - * @return resource|null The interpolator, or null on error. + * See Interpolator::newFromName() for the new thing. */ - public static function newInterpolator(string $name) + public static function newInterpolator(string $name): Interpolate { - Utils::debugLog('newInterpolator', [ - 'instance' => null, - 'arguments' => [$name] - ]); - - // added in 1.0.7 of the binary module - return vips_interpolate_new($name); + return Interpolate::newFromName($name); } /** @@ -1056,12 +894,7 @@ public static function newInterpolator(string $name) */ public function newFromImage($value): Image { - Utils::debugLog('newFromImage', [ - 'instance' => $this, - 'arguments' => [$value] - ]); - - $pixel = self::black(1, 1)->add($value)->cast($this->format); + $pixel = static::black(1, 1)->add($value)->cast($this->format); $image = $pixel->embed( 0, 0, @@ -1069,23 +902,53 @@ public function newFromImage($value): Image $this->height, ['extend' => Extend::COPY] ); - $image = $image->copy([ + return $image->copy([ 'interpretation' => $this->interpretation, 'xres' => $this->xres, 'yres' => $this->yres, 'xoffset' => $this->xoffset, 'yoffset' => $this->yoffset ]); + } - Utils::debugLog('newFromImage', ['result' => $image]); + /** + * Find the name of the load operation vips will use to load a VipsSource, for + * example 'VipsForeignLoadJpegSource'. You can use this to work out what + * options to pass to newFromSource(). + * + * @param Source $source The source to test + * @return string|null The name of the load operation, or null. + */ + public static function findLoadSource(Source $source): ?string + { + return FFI::vips()->vips_foreign_find_load_source( + FFI::vips()->cast(FFI::ctypes('VipsSource'), $source->pointer) + ); + } - return $image; + /** + * @throws Exception + */ + public static function newFromSource(Source $source, string $string_options = '', array $options = []): self + { + $loader = self::findLoadSource($source); + if ($loader === null) { + throw new Exception('unable to load from source'); + } + + if ($string_options !== '') { + $options = array_merge([ + "string_options" => $string_options, + ], $options); + } + + return VipsOperation::call($loader, null, [$source], $options); } /** * Write an image to a file. * - * @param string $filename The file to write the image to. + * @param string $name The file to write the image to. * @param array $options Any options to pass on to the selected save * operation. * @@ -1093,17 +956,26 @@ public function newFromImage($value): Image * * @return void */ - public function writeToFile(string $filename, array $options = []) + public function writeToFile(string $name, array $options = []): void { - Utils::debugLog('writeToFile', [ - 'instance' => $this, - 'arguments' => [$filename, $options] - ]); + $filename = Utils::filenameGetFilename($name); + $string_options = Utils::filenameGetOptions($name); + + $saver = FFI::vips()->vips_foreign_find_save($filename); + if ($saver == "") { + throw new Exception(); + } + + if (strlen($string_options) != 0) { + $options = array_merge([ + "string_options" => $string_options, + ], $options); + } + + $result = VipsOperation::call($saver, $this, [$filename], $options); - $options = self::unwrap($options); - $result = vips_image_write_to_file($this->image, $filename, $options); if ($result === -1) { - self::errorVips(); + throw new Exception(); } } @@ -1120,21 +992,48 @@ public function writeToFile(string $filename, array $options = []) */ public function writeToBuffer(string $suffix, array $options = []): string { - Utils::debugLog('writeToBuffer', [ - 'instance' => $this, - 'arguments' => [$suffix, $options] - ]); + $filename = Utils::filenameGetFilename($suffix); + $string_options = Utils::filenameGetOptions($suffix); - $options = self::unwrap($options); - $result = vips_image_write_to_buffer($this->image, $suffix, $options); - if ($result === -1) { - self::errorVips(); + $saver = null; + + // see if we can save with the Target API ... we need 8.9 or later for + // Target, and we need this libvips to have a target saver for this + // format + if (FFI::atLeast(8, 9)) { + FFI::vips()->vips_error_freeze(); + $saver = FFI::vips()->vips_foreign_find_save_target($filename); + FFI::vips()->vips_error_thaw(); } - $result = self::wrapResult($result); - Utils::debugLog('writeToBuffer', ['result' => $result]); + if ($saver !== null) { + $target = Target::newToMemory(); + if (strlen($string_options) != 0) { + $options = array_merge([ + "string_options" => $string_options, + ], $options); + } - return $result; + VipsOperation::call($saver, $this, [$target], $options); + + $buffer = $target->get("blob"); + } else { + // fall back to the old _buffer API + $saver = FFI::vips()->vips_foreign_find_save_buffer($filename); + if ($saver == "") { + throw new Exception(); + } + + if (strlen($string_options) != 0) { + $options = array_merge([ + "string_options" => $string_options, + ], $options); + } + + $buffer = VipsOperation::call($saver, $this, [], $options); + } + + return $buffer; } /** @@ -1146,21 +1045,94 @@ public function writeToBuffer(string $suffix, array $options = []): string */ public function writeToMemory(): string { - Utils::debugLog('writeToMemory', [ - 'instance' => $this, - 'arguments' => [] - ]); + $p_size = FFI::vips()->new("size_t[1]"); - $result = vips_image_write_to_memory($this->image); - if ($result === -1) { - self::errorVips(); + $pointer = FFI::vips()-> + vips_image_write_to_memory($this->pointer, $p_size); + if ($pointer == null) { + throw new Exception(); } - Utils::debugLog('writeToMemory', ['result' => $result]); + // string() takes a copy + $result = \FFI::string($pointer, $p_size[0]); + + FFI::glib()->g_free($pointer); + + return $result; + } + + /** + * Write an image to a PHP array. + * + * Pixels are written as a simple one-dimensional array, for example, if + * you write: + * + * ```php + * $result = $image->crop(100, 100, 10, 1)->writeToArray(); + * ``` + * + * This will crop out 10 pixels and write them to the array. If `$image` + * is an RGB image, then `$array` will contain 30 numbers, with the first + * three being R, G and B for the first pixel. + * + * You'll need to slice and repack the array if you want more dimensions. + * + * This method is much faster than repeatedly calling `getpoint()`. It + * will use a lot of memory. + * + * @throws Exception + * + * @return array The pixel values as a PHP array. + */ + public function writeToArray(): array + { + $p_size = FFI::vips()->new("size_t[1]"); + + $pointer = FFI::vips()-> + vips_image_write_to_memory($this->pointer, $p_size); + if ($pointer == null) { + throw new Exception(); + } + + // wrap pointer up as a C array of the right type + $type_name = FFI::ftypes($this->format); + $n = $this->width * $this->height * $this->bands; + $array = FFI::vips()->cast("{$type_name}[$n]", $pointer); + + // copy to PHP memory as a flat array + $result = []; + for ($i = 0; $i < $n; $i++) { + $result[] = $array[$i]; + } + + // the vips result is not PHP memory, so we must free it + FFI::glib()->g_free($pointer); return $result; } + /** + * @throws Exception + */ + public function writeToTarget(Target $target, string $suffix, array $options = []): void + { + $filename = Utils::filenameGetFilename($suffix); + $string_options = Utils::filenameGetOptions($suffix); + $saver = FFI::vips()->vips_foreign_find_save_target($filename); + + if ($saver === '') { + throw new Exception("can't save to target with given suffix $filename"); + } + + if ($string_options !== '') { + $options = array_merge([ + "string_options" => $string_options, + ], $options); + } + + VipsOperation::call($saver, $this, [$target], $options); + } + /** * Copy to memory. * @@ -1177,20 +1149,11 @@ public function writeToMemory(): string */ public function copyMemory(): Image { - Utils::debugLog('copyMemory', [ - 'instance' => $this, - 'arguments' => [] - ]); - - $result = vips_image_copy_memory($this->image); - if ($result === -1) { - self::errorVips(); + $pointer = FFI::vips()->vips_image_copy_memory($this->pointer); + if ($pointer == null) { + throw new Exception(); } - $result = self::wrapResult($result); - - Utils::debugLog('copyMemory', ['result' => $result]); - - return $result; + return new Image($pointer); } /** @@ -1204,9 +1167,7 @@ public function copyMemory(): Image */ public function __get(string $name) { - $result = vips_image_get($this->image, $name); - self::errorIsArray($result); - return self::wrapResult($result); + return $this->get($name); } /** @@ -1216,10 +1177,11 @@ public function __get(string $name) * @param mixed $value The value to set for this property. * * @return void + * @throws Exception */ - public function __set(string $name, $value) + public function __set(string $name, $value): void { - vips_image_set($this->image, $name, $value); + $this->set($name, $value); } /** @@ -1229,9 +1191,9 @@ public function __set(string $name, $value) * * @return bool */ - public function __isset(string $name) + public function __isset(string $name): bool { - return $this->typeof($name) !== 0; + return $this->getType($name) != 0; } /** @@ -1240,7 +1202,7 @@ public function __isset(string $name) * This is handy for fields whose name * does not match PHP's variable naming conventions, like `'exif-data'`. * - * It will throw an exception if $name does not exist. Use Image::typeof() + * It will throw an exception if $name does not exist. Use Image::getType() * to test for the existence of a field. * * @param string $name The property name. @@ -1251,9 +1213,13 @@ public function __isset(string $name) */ public function get(string $name) { - $result = vips_image_get($this->image, $name); - self::errorIsArray($result); - return self::wrapResult($result); + $gvalue = new GValue(); + if (FFI::vips()-> + vips_image_get($this->pointer, $name, $gvalue->pointer) != 0) { + throw new Exception(); + } + + return $gvalue->get(); } /** @@ -1265,169 +1231,161 @@ public function get(string $name) * * @return integer */ - public function typeof(string $name): int + public function getType(string $name): int { - return vips_image_get_typeof($this->image, $name); + return FFI::vips()->vips_image_get_typeof($this->pointer, $name); } /** - * Set any property on the underlying image. - * - * This is handy for fields whose name - * does not match PHP's variable naming conventions, like `'exif-data'`. + * A deprecated synonym for getType(). * - * @param string $name The property name. - * @param mixed $value The value to set for this property. + * @param string $name The property name. * - * @throws Exception + * @return integer + */ + public function typeOf(string $name): int + { + return $this->getType($name); + } + + /** + * Get the field names available for an image. * - * @return void + * @return Array */ - public function set(string $name, $value) + public function getFields(): array { - $result = vips_image_set($this->image, $name, $value); - if ($result === -1) { - self::errorVips(); + $str_array = FFI::vips()->vips_image_get_fields($this->pointer); + + $fields = []; + for ($i = 0; $str_array[$i] != null; $i++) { + array_push($fields, \FFI::string($str_array[$i])); } + + FFI::glib()->g_free($str_array); + + return $fields; } /** - * Remove a field from the underlying image. + * Set any property on the underlying image. * - * @param string $name The property name. + * This is handy for fields whose name + * does not match PHP's variable naming conventions, like `'exif-data'`. + * + * @param string $name The property name. + * @param mixed $value The value to set for this property. * * @throws Exception * * @return void */ - public function remove(string $name) + public function set(string $name, $value): void { - $result = vips_image_remove($this->image, $name); - if ($result === -1) { - self::errorVips(); + $gvalue = new GValue(); + $gtype = $this->getType($name); + + /* If this is not a known field, guess a sensible type from the value. + */ + if ($gtype == 0) { + if (is_array($value)) { + if (is_int($value[0])) { + $gtype = FFI::gtypes("VipsArrayInt"); + } elseif (is_float($value[0])) { + $gtype = FFI::gtypes("VipsArrayDouble"); + } else { + $gtype = FFI::gtypes("VipsArrayImage"); + } + } elseif (is_int($value)) { + $gtype = FFI::gtypes("gint"); + } elseif (is_float($value)) { + $gtype = FFI::gtypes("gdouble"); + } elseif (is_string($value)) { + $gtype = FFI::gtypes("VipsRefString"); + } else { + $gtype = FFI::gtypes("VipsImage"); + } } - } - /** - * Makes a string-ified version of the Image. - * - * @return string - */ - public function __toString() - { - $array = [ - 'width' => $this->width, - 'height' => $this->height, - 'bands' => $this->bands, - 'format' => $this->format, - 'interpretation' => $this->interpretation, - ]; + $gvalue->setType($gtype); + $gvalue->set($value); - return json_encode($array); + FFI::vips()->vips_image_set($this->pointer, $name, $gvalue->pointer); } /** - * Call any vips operation. The final element of $arguments can be - * (but doesn't have to be) an array of options to pass to the operation. + * Set the type and value for any property on the underlying image. + * + * This is useful if the type of the property cannot be determined from the + * php type of the value. * - * We can't have a separate arg for the options since this will be run from - * __call(), which cannot know which args are required and which are - * optional. See call() below for a version with the options broken out. + * Pass the type name directly, or use Utils::typeFromName() to look up + * types by name. * - * @param string $name The operation name. - * @param Image|null $instance The instance this operation is being invoked - * from. - * @param array $arguments An array of arguments to pass to the - * operation. + * @param string|int $type The type of the property. + * @param string $name The property name. + * @param mixed $value The value to set for this property. * * @throws Exception * - * @return mixed The result(s) of the operation. + * @return void */ - public static function callBase( - string $name, - $instance, - array $arguments - ) { - Utils::debugLog($name, [ - 'instance' => $instance, - 'arguments' => $arguments - ]); - - $arguments = array_merge([$name, $instance], $arguments); - - $arguments = array_values(self::unwrap($arguments)); - $result = vips_call(...$arguments); - self::errorIsArray($result); - $result = self::wrapResult($result); - - Utils::debugLog($name, ['result' => $result]); - - return $result; + public function setType($type, string $name, $value): void + { + $gvalue = new GValue(); + $gvalue->setType($type); + $gvalue->set($value); + FFI::vips()->vips_image_set($this->pointer, $name, $gvalue->pointer); } /** - * Call any vips operation, with an explicit set of options. This is more - * convenient than callBase() if you have a set of known options. + * Remove a field from the underlying image. * - * @param string $name The operation name. - * @param Image|null $instance The instance this operation is being invoked - * from. - * @param array $arguments An array of arguments to pass to the - * operation. - * @param array $options An array of optional arguments to pass to - * the operation. + * @param string $name The property name. * * @throws Exception * - * @return mixed The result(s) of the operation. + * @return void */ - public static function call( - string $name, - $instance, - array $arguments, - array $options = [] - ) { - /* - echo "call: $name \n"; - echo "instance = \n"; - var_dump($instance); - echo "arguments = \n"; - var_dump($arguments); - echo "options = \n"; - var_dump($options); - */ - - return self::callBase($name, $instance, array_merge($arguments, [$options])); + public function remove(string $name): void + { + if (!FFI::vips()->vips_image_remove($this->pointer, $name)) { + throw new Exception(); + } } /** - * Handy for things like self::more. Call a 2-ary vips operator like - * 'more', but if the arg is not an image (ie. it's a constant), call - * 'more_const' instead. + * Enable progress reporting on an image. * - * @param mixed $other The right-hand argument. - * @param string $base The base part of the operation name. - * @param string $op The action to invoke. - * @param array $options An array of options to pass to the operation. + * The preeval, eval and posteval signals will be emitted on the + * most-downstream image for which setProgress() was enabled. @see + * GObject::signalConnect(). * - * @throws Exception + * @param bool $progress TRUE to enable progress reporting. * - * @return mixed The operation result. + * @return void + */ + public function setProgress(bool $progress): void + { + FFI::vips()->vips_image_set_progress($this->pointer, $progress); + } + + /** + * Makes a string-ified version of the Image. * - * @internal + * @return string */ - private function callEnum( - $other, - string $base, - string $op, - array $options = [] - ) { - if (self::isImageish($other)) { - return self::call($base, $this, [$other, $op], $options); - } else { - return self::call($base . '_const', $this, [$op, $other], $options); - } + public function __toString(): string + { + $array = [ + 'width' => $this->width, + 'height' => $this->height, + 'bands' => $this->bands, + 'format' => $this->format, + 'interpretation' => $this->interpretation, + ]; + + return json_encode($array); } /** @@ -1442,7 +1400,7 @@ private function callEnum( */ public function __call(string $name, array $arguments) { - return self::callBase($name, $this, $arguments); + return VipsOperation::callBase($name, $this, $arguments); } /** @@ -1457,7 +1415,7 @@ public function __call(string $name, array $arguments) */ public static function __callStatic(string $name, array $arguments) { - return self::callBase($name, null, $arguments); + return VipsOperation::callBase($name, null, $arguments); } /** @@ -1495,11 +1453,12 @@ public function offsetExists($offset): bool * * @throws Exception * - * @return Image the extracted band. + * @return Image|null the extracted band or null. */ - public function offsetGet($offset): Image + public function offsetGet($offset): ?Image { - return $this->offsetExists($offset) ? $this->extract_band($offset) : null; + return $this->offsetExists($offset) ? + $this->extract_band($offset) : null; } /** @@ -1526,7 +1485,7 @@ public function offsetGet($offset): Image * * @return void */ - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { // no offset means append if ($offset === null) { @@ -1534,7 +1493,8 @@ public function offsetSet($offset, $value) } if (!is_int($offset)) { - throw new \BadMethodCallException('Image::offsetSet: offset is not integer or null'); + throw new \BadMethodCallException('Image::offsetSet: ' . + 'offset is not integer or null'); } // number of bands to the left and right of $value @@ -1545,7 +1505,7 @@ public function offsetSet($offset, $value) // if we are setting a constant as the first element, we must expand it // to an image, since bandjoin must have an image as the first argument if ($n_left === 0 && !($value instanceof Image)) { - $value = self::imageize($this, $value); + $value = $this->imageize($value); } $components = []; @@ -1558,7 +1518,14 @@ public function offsetSet($offset, $value) } $head = array_shift($components); - $this->image = $head->bandjoin($components)->image; + $joined = $head->bandjoin($components); + + /* Overwrite our pointer with the pointer from the new, joined object. + * We have to adjust the refs, yuk! + */ + $joined->ref(); + $this->unref(); + $this->pointer = $joined->pointer; } /** @@ -1572,11 +1539,12 @@ public function offsetSet($offset, $value) * * @return void */ - public function offsetUnset($offset) + public function offsetUnset($offset): void { if (is_int($offset) && $offset >= 0 && $offset < $this->bands) { if ($this->bands === 1) { - throw new \BadMethodCallException('Image::offsetUnset: cannot delete final band'); + throw new \BadMethodCallException('Image::offsetUnset: ' . + 'cannot delete final band'); } $components = []; @@ -1592,9 +1560,14 @@ public function offsetUnset($offset) $head = array_shift($components); if (empty($components)) { - $this->image = $head->image; + $head->ref(); + $this->unref(); + $this->pointer = $head->pointer; } else { - $this->image = $head->bandjoin($components)->image; + $new_image = $head->bandjoin($components); + $new_image->ref(); + $this->unref(); + $this->pointer = $new_image->pointer; } } } @@ -1612,7 +1585,7 @@ public function offsetUnset($offset) public function add($other, array $options = []): Image { if (self::isImageish($other)) { - return self::call('add', $this, [$other], $options); + return VipsOperation::call('add', $this, [$other], $options); } else { return $this->linear(1, $other, $options); } @@ -1631,7 +1604,7 @@ public function add($other, array $options = []): Image public function subtract($other, array $options = []): Image { if (self::isImageish($other)) { - return self::call('subtract', $this, [$other], $options); + return VipsOperation::call('subtract', $this, [$other], $options); } else { $other = self::mapNumeric($other, function ($value) { return -1 * $value; @@ -1653,7 +1626,7 @@ public function subtract($other, array $options = []): Image public function multiply($other, array $options = []): Image { if (self::isImageish($other)) { - return self::call('multiply', $this, [$other], $options); + return VipsOperation::call('multiply', $this, [$other], $options); } else { return $this->linear($other, 0, $options); } @@ -1672,7 +1645,7 @@ public function multiply($other, array $options = []): Image public function divide($other, array $options = []): Image { if (self::isImageish($other)) { - return self::call('divide', $this, [$other], $options); + return VipsOperation::call('divide', $this, [$other], $options); } else { $other = self::mapNumeric($other, function ($value) { return $value ** -1; @@ -1694,9 +1667,14 @@ public function divide($other, array $options = []): Image public function remainder($other, array $options = []): Image { if (self::isImageish($other)) { - return self::call('remainder', $this, [$other], $options); + return VipsOperation::call('remainder', $this, [$other], $options); } else { - return self::call('remainder_const', $this, [$other], $options); + return VipsOperation::call( + 'remainder_const', + $this, + [$other], + $options + ); } } @@ -1712,7 +1690,7 @@ public function remainder($other, array $options = []): Image */ public function pow($other, array $options = []): Image { - return $this->callEnum($other, 'math2', OperationMath2::POW, $options); + return self::callEnum($other, 'math2', OperationMath2::POW, $options); } /** @@ -1727,7 +1705,7 @@ public function pow($other, array $options = []): Image */ public function wop($other, array $options = []): Image { - return $this->callEnum($other, 'math2', OperationMath2::WOP, $options); + return self::callEnum($other, 'math2', OperationMath2::WOP, $options); } /** @@ -1742,7 +1720,7 @@ public function wop($other, array $options = []): Image */ public function lshift($other, array $options = []): Image { - return $this->callEnum($other, 'boolean', OperationBoolean::LSHIFT, $options); + return self::callEnum($other, 'boolean', OperationBoolean::LSHIFT, $options); } /** @@ -1757,7 +1735,12 @@ public function lshift($other, array $options = []): Image */ public function rshift($other, array $options = []): Image { - return $this->callEnum($other, 'boolean', OperationBoolean::RSHIFT, $options); + return self::callEnum( + $other, + 'boolean', + OperationBoolean::RSHIFT, + $options + ); } /** @@ -1774,7 +1757,7 @@ public function rshift($other, array $options = []): Image public function andimage($other, array $options = []): Image { // phpdoc hates OperationBoolean::AND, so use the string form here - return $this->callEnum($other, 'boolean', 'and', $options); + return self::callEnum($other, 'boolean', 'and', $options); } /** @@ -1790,7 +1773,7 @@ public function andimage($other, array $options = []): Image public function orimage($other, array $options = []): Image { // phpdoc hates OperationBoolean::OR, so use the string form here - return $this->callEnum($other, 'boolean', 'or', $options); + return self::callEnum($other, 'boolean', 'or', $options); } /** @@ -1805,7 +1788,12 @@ public function orimage($other, array $options = []): Image */ public function eorimage($other, array $options = []): Image { - return $this->callEnum($other, 'boolean', OperationBoolean::EOR, $options); + return self::callEnum( + $other, + 'boolean', + OperationBoolean::EOR, + $options + ); } /** @@ -1820,7 +1808,12 @@ public function eorimage($other, array $options = []): Image */ public function more($other, array $options = []): Image { - return $this->callEnum($other, 'relational', OperationRelational::MORE, $options); + return self::callEnum( + $other, + 'relational', + OperationRelational::MORE, + $options + ); } /** @@ -1835,7 +1828,12 @@ public function more($other, array $options = []): Image */ public function moreEq($other, array $options = []): Image { - return $this->callEnum($other, 'relational', OperationRelational::MOREEQ, $options); + return self::callEnum( + $other, + 'relational', + OperationRelational::MOREEQ, + $options + ); } /** @@ -1850,7 +1848,12 @@ public function moreEq($other, array $options = []): Image */ public function less($other, array $options = []): Image { - return $this->callEnum($other, 'relational', OperationRelational::LESS, $options); + return self::callEnum( + $other, + 'relational', + OperationRelational::LESS, + $options + ); } /** @@ -1865,7 +1868,12 @@ public function less($other, array $options = []): Image */ public function lessEq($other, array $options = []): Image { - return $this->callEnum($other, 'relational', OperationRelational::LESSEQ, $options); + return self::callEnum( + $other, + 'relational', + OperationRelational::LESSEQ, + $options + ); } /** @@ -1880,7 +1888,12 @@ public function lessEq($other, array $options = []): Image */ public function equal($other, array $options = []): Image { - return $this->callEnum($other, 'relational', OperationRelational::EQUAL, $options); + return self::callEnum( + $other, + 'relational', + OperationRelational::EQUAL, + $options + ); } /** @@ -1895,7 +1908,12 @@ public function equal($other, array $options = []): Image */ public function notEq($other, array $options = []): Image { - return $this->callEnum($other, 'relational', OperationRelational::NOTEQ, $options); + return self::callEnum( + $other, + 'relational', + OperationRelational::NOTEQ, + $options + ); } /** @@ -1931,9 +1949,14 @@ public function bandjoin($other, array $options = []): Image /* We can't use self::bandjoin(), that would just recurse. */ if ($is_const) { - return self::call('bandjoin_const', $this, [$other], $options); + return VipsOperation::call( + 'bandjoin_const', + $this, + [$other], + $options + ); } else { - return self::call( + return VipsOperation::call( 'bandjoin', null, [array_merge([$this], $other)], @@ -1978,7 +2001,7 @@ public function bandrank($other, array $options = []): Image /* bandrank will appear as a static class member, as * Image::bandrank([a, b, c]), but it's better as an instance * method. - * + * * We need to define this by hand. */ @@ -1990,7 +2013,12 @@ public function bandrank($other, array $options = []): Image $other = (array) $other; } - return self::call('bandrank', $this, $other, $options); + return VipsOperation::call( + 'bandrank', + null, + [array_merge([$this], $other)], + $options + ); } /** @@ -2013,16 +2041,18 @@ public function composite($other, $mode, array $options = []): Image } else { $other = (array) $other; } + if (!is_array($mode)) { $mode = [$mode]; } + # composite takes an arrayint, but it's really an array of blend modes + # gvalue doesn't know this, so we must do name -> enum value mapping $mode = array_map(function ($x) { - // Use BlendMode::OVER if a non-existent value is given. - return self::$blendModeToInt[$x] ?? BlendMode::OVER; + return GValue::toEnum(FFI::gtypes("VipsBlendMode"), $x); }, $mode); - return self::call( + return VipsOperation::call( 'composite', null, [array_merge([$this], $other), $mode], @@ -2082,7 +2112,6 @@ public function ifthenelse($then, $else, array $options = []): Image * match each other first, and only if they are both constants do we * match to $this. */ - $match_image = null; foreach ([$then, $else, $this] as $item) { if ($item instanceof Image) { @@ -2092,14 +2121,19 @@ public function ifthenelse($then, $else, array $options = []): Image } if (!($then instanceof Image)) { - $then = self::imageize($match_image, $then); + $then = $match_image->imageize($then); } if (!($else instanceof Image)) { - $else = self::imageize($match_image, $else); + $else = $match_image->imageize($else); } - return self::call('ifthenelse', $this, [$then, $else], $options); + return VipsOperation::call( + 'ifthenelse', + $this, + [$then, $else], + $options + ); } /** @@ -2209,7 +2243,9 @@ public function imag(): Image */ public function polar(): Image { - return $this->complex(OperationComplex::POLAR); + return self::runCmplx(function ($image) { + return $image->complex(OperationComplex::POLAR); + }, $this); } /** @@ -2221,7 +2257,9 @@ public function polar(): Image */ public function rect(): Image { - return $this->complex(OperationComplex::RECT); + return self::runCmplx(function ($image) { + return $image->complex(OperationComplex::RECT); + }, $this); } /** @@ -2236,6 +2274,20 @@ public function conj(): Image return $this->complex(OperationComplex::CONJ); } + /** + * Find the cross-phase of this image with $other. + * + * @param mixed $other The thing to cross-phase by. + * + * @throws Exception + * + * @return Image A new image. + */ + public function crossPhase($other): Image + { + return $this->complex2($other, OperationComplex2::CROSS_PHASE); + } + /** * Return the sine of an image in degrees. * diff --git a/src/ImageAutodoc.php b/src/ImageAutodoc.php index 7461644..a0b2b4d 100644 --- a/src/ImageAutodoc.php +++ b/src/ImageAutodoc.php @@ -47,521 +47,666 @@ * @license https://opensource.org/licenses/MIT MIT * @link https://github.com/jcupitt/php-vips * - * @method static void system(string $cmd_format, array $options = []) Run an external command. + * @method Image CMC2LCh(array $options = []) Transform LCh to CMC. * @throws Exception - * @method Image relational(Image $right, string $relational, array $options = []) Relational operation on two images. - * @see OperationRelational for possible values for $relational + * @method Image CMYK2XYZ(array $options = []) Transform CMYK to XYZ. * @throws Exception - * @method Image boolean(Image $right, string $boolean, array $options = []) Boolean operation on two images. - * @see OperationBoolean for possible values for $boolean + * @method Image HSV2sRGB(array $options = []) Transform HSV to sRGB. * @throws Exception - * @method Image math2(Image $right, string $math2, array $options = []) Binary math operations. - * @see OperationMath2 for possible values for $math2 + * @method Image LCh2CMC(array $options = []) Transform LCh to CMC. * @throws Exception - * @method Image complex2(Image $right, string $cmplx, array $options = []) Complex binary operations on two images. - * @see OperationComplex2 for possible values for $cmplx + * @method Image LCh2Lab(array $options = []) Transform LCh to Lab. * @throws Exception - * @method Image complexform(Image $right, array $options = []) Form a complex image from two real images. + * @method Image Lab2LCh(array $options = []) Transform Lab to LCh. * @throws Exception - * @method static Image sum(Image[]|image $in, array $options = []) Sum an array of images. + * @method Image Lab2LabQ(array $options = []) Transform float Lab to LabQ coding. * @throws Exception - * @method Image invert(array $options = []) Invert an image. + * @method Image Lab2LabS(array $options = []) Transform float Lab to signed short. * @throws Exception - * @method Image linear(float[]|float $a, float[]|float $b, array $options = []) Calculate (a * in + b). + * @method Image Lab2XYZ(array $options = []) Transform CIELAB to XYZ. * @throws Exception - * @method Image math(string $math, array $options = []) Apply a math operation to an image. - * @see OperationMath for possible values for $math + * @method Image LabQ2Lab(array $options = []) Unpack a LabQ image to float Lab. + * @throws Exception + * @method Image LabQ2LabS(array $options = []) Unpack a LabQ image to short Lab. + * @throws Exception + * @method Image LabQ2sRGB(array $options = []) Convert a LabQ image to sRGB. + * @throws Exception + * @method Image LabS2Lab(array $options = []) Transform signed short Lab to float. + * @throws Exception + * @method Image LabS2LabQ(array $options = []) Transform short Lab to LabQ coding. + * @throws Exception + * @method Image XYZ2CMYK(array $options = []) Transform XYZ to CMYK. + * @throws Exception + * @method Image XYZ2Lab(array $options = []) Transform XYZ to Lab. + * @throws Exception + * @method Image XYZ2Yxy(array $options = []) Transform XYZ to Yxy. + * @throws Exception + * @method Image XYZ2scRGB(array $options = []) Transform XYZ to scRGB. + * @throws Exception + * @method Image Yxy2XYZ(array $options = []) Transform Yxy to XYZ. * @throws Exception * @method Image abs(array $options = []) Absolute value of an image. * @throws Exception - * @method Image sign(array $options = []) Unit vector of pixel. + * @method Image addalpha(array $options = []) Append an alpha channel. * @throws Exception - * @method Image round(string $round, array $options = []) Perform a round function on an image. - * @see OperationRound for possible values for $round + * @method Image affine(float[]|float $matrix, array $options = []) Affine transform of an image. * @throws Exception - * @method Image relational_const(string $relational, float[]|float $c, array $options = []) Relational operations against a constant. - * @see OperationRelational for possible values for $relational + * @method static Image analyzeload(string $filename, array $options = []) Load an Analyze6 image. * @throws Exception - * @method Image remainder_const(float[]|float $c, array $options = []) Remainder after integer division of an image and a constant. + * @method static Image arrayjoin(Image[]|Image $in, array $options = []) Join an array of images. + * @throws Exception + * @method Image autorot(array $options = []) Autorotate image by exif tag. + * @throws Exception + * @method float avg(array $options = []) Find image average. + * @throws Exception + * @method Image bandbool(string $boolean, array $options = []) Boolean operation across image bands. + * @see OperationBoolean for possible values for $boolean + * @throws Exception + * @method Image bandfold(array $options = []) Fold up x axis into bands. + * @throws Exception + * @method Image bandjoin_const(float[]|float $c, array $options = []) Append a constant band to an image. + * @throws Exception + * @method Image bandmean(array $options = []) Band-wise average. + * @throws Exception + * @method Image bandunfold(array $options = []) Unfold image bands into x axis. + * @throws Exception + * @method static Image black(integer $width, integer $height, array $options = []) Make a black image. + * @throws Exception + * @method Image boolean(Image $right, string $boolean, array $options = []) Boolean operation on two images. + * @see OperationBoolean for possible values for $boolean * @throws Exception * @method Image boolean_const(string $boolean, float[]|float $c, array $options = []) Boolean operations against a constant. * @see OperationBoolean for possible values for $boolean * @throws Exception - * @method Image math2_const(string $math2, float[]|float $c, array $options = []) Binary math operations with a constant. - * @see OperationMath2 for possible values for $math2 + * @method Image buildlut(array $options = []) Build a look-up table. + * @throws Exception + * @method Image byteswap(array $options = []) Byteswap an image. + * @throws Exception + * @method Image canny(array $options = []) Canny edge detector. + * @throws Exception + * @method Image case(Image[]|Image $cases, array $options = []) Use pixel values to pick cases from an array of images. + * @throws Exception + * @method Image cast(string $format, array $options = []) Cast an image. + * @see BandFormat for possible values for $format + * @throws Exception + * @method Image clamp(array $options = []) Clamp values of an image. + * @throws Exception + * @method Image colourspace(string $space, array $options = []) Convert to a new colorspace. + * @see Interpretation for possible values for $space + * @throws Exception + * @method Image compass(Image $mask, array $options = []) Convolve with rotating mask. * @throws Exception * @method Image complex(string $cmplx, array $options = []) Perform a complex operation on an image. * @see OperationComplex for possible values for $cmplx * @throws Exception + * @method Image complex2(Image $right, string $cmplx, array $options = []) Complex binary operations on two images. + * @see OperationComplex2 for possible values for $cmplx + * @throws Exception + * @method Image complexform(Image $right, array $options = []) Form a complex image from two real images. + * @throws Exception * @method Image complexget(string $get, array $options = []) Get a component from a complex image. * @see OperationComplexget for possible values for $get * @throws Exception - * @method float avg(array $options = []) Find image average. + * @method static Image composite(Image[]|Image $in, integer[]|integer $mode, array $options = []) Blend an array of images with an array of blend modes. * @throws Exception - * @method float min(array $options = []) Find image minimum. + * @method Image composite2(Image $overlay, string $mode, array $options = []) Blend a pair of images with a blend mode. + * @see BlendMode for possible values for $mode * @throws Exception - * @method float max(array $options = []) Find image maximum. + * @method Image conv(Image $mask, array $options = []) Convolution operation. + * @throws Exception + * @method Image conva(Image $mask, array $options = []) Approximate integer convolution. + * @throws Exception + * @method Image convasep(Image $mask, array $options = []) Approximate separable integer convolution. + * @throws Exception + * @method Image convf(Image $mask, array $options = []) Float convolution operation. + * @throws Exception + * @method Image convi(Image $mask, array $options = []) Int convolution operation. + * @throws Exception + * @method Image convsep(Image $mask, array $options = []) Separable convolution operation. + * @throws Exception + * @method Image copy(array $options = []) Copy an image. + * @throws Exception + * @method float countlines(string $direction, array $options = []) Count lines in an image. + * @see Direction for possible values for $direction + * @throws Exception + * @method Image crop(integer $left, integer $top, integer $width, integer $height, array $options = []) Extract an area from an image. + * @throws Exception + * @method static Image csvload(string $filename, array $options = []) Load csv. + * @throws Exception + * @method static Image csvload_source(Source $source, array $options = []) Load csv. + * @throws Exception + * @method void csvsave(string $filename, array $options = []) Save image to csv. + * @throws Exception + * @method void csvsave_target(Target $target, array $options = []) Save image to csv. + * @throws Exception + * @method Image dE00(Image $right, array $options = []) Calculate dE00. + * @throws Exception + * @method Image dE76(Image $right, array $options = []) Calculate dE76. + * @throws Exception + * @method Image dECMC(Image $right, array $options = []) Calculate dECMC. * @throws Exception * @method float deviate(array $options = []) Find image standard deviation. * @throws Exception - * @method Image stats(array $options = []) Find image average. + * @method Image draw_circle(float[]|float $ink, integer $cx, integer $cy, integer $radius, array $options = []) Draw a circle on an image. * @throws Exception - * @method Image hist_find(array $options = []) Find image histogram. + * @method Image draw_flood(float[]|float $ink, integer $x, integer $y, array $options = []) Flood-fill an area. * @throws Exception - * @method Image hist_find_ndim(array $options = []) Find n-dimensional image histogram. + * @method Image draw_image(Image $sub, integer $x, integer $y, array $options = []) Paint an image into another image. * @throws Exception - * @method Image hist_find_indexed(Image $index, array $options = []) Find indexed image histogram. + * @method Image draw_line(float[]|float $ink, integer $x1, integer $y1, integer $x2, integer $y2, array $options = []) Draw a line on an image. * @throws Exception - * @method Image hough_line(array $options = []) Find hough line transform. + * @method Image draw_mask(float[]|float $ink, Image $mask, integer $x, integer $y, array $options = []) Draw a mask on an image. * @throws Exception - * @method Image hough_circle(array $options = []) Find hough circle transform. + * @method Image draw_rect(float[]|float $ink, integer $left, integer $top, integer $width, integer $height, array $options = []) Paint a rectangle on an image. * @throws Exception - * @method array project(array $options = []) Find image projections. - * Return array with: [ - * 'columns' => @type Image Sums of columns. - * 'rows' => @type Image Sums of rows. - * ]; + * @method Image draw_smudge(integer $left, integer $top, integer $width, integer $height, array $options = []) Blur a rectangle on an image. * @throws Exception - * @method array profile(array $options = []) Find image profiles. - * Return array with: [ - * 'columns' => @type Image First non-zero pixel in column. - * 'rows' => @type Image First non-zero pixel in row. - * ]; + * @method void dzsave(string $filename, array $options = []) Save image to deepzoom file. * @throws Exception - * @method Image measure(integer $h, integer $v, array $options = []) Measure a set of patches on a colour chart. + * @method string dzsave_buffer(array $options = []) Save image to dz buffer. * @throws Exception - * @method array getpoint(integer $x, integer $y, array $options = []) Read a point from an image. + * @method void dzsave_target(Target $target, array $options = []) Save image to deepzoom target. + * @throws Exception + * @method Image embed(integer $x, integer $y, integer $width, integer $height, array $options = []) Embed an image in a larger image. + * @throws Exception + * @method Image extract_area(integer $left, integer $top, integer $width, integer $height, array $options = []) Extract an area from an image. + * @throws Exception + * @method Image extract_band(integer $band, array $options = []) Extract band from an image. + * @throws Exception + * @method static Image eye(integer $width, integer $height, array $options = []) Make an image showing the eye's spatial response. + * @throws Exception + * @method Image falsecolour(array $options = []) False-color an image. + * @throws Exception + * @method Image fastcor(Image $ref, array $options = []) Fast correlation. + * @throws Exception + * @method Image fill_nearest(array $options = []) Fill image zeros with nearest non-zero pixel. * @throws Exception * @method array find_trim(array $options = []) Search an image for non-edge areas. * Return array with: [ - * 'left' => @type integer Left edge of image. - * 'top' => @type integer Top edge of extract area. - * 'width' => @type integer Width of extract area. - * 'height' => @type integer Height of extract area. + * 'left' => @type integer Left edge of image + * 'top' => @type integer Top edge of extract area + * 'width' => @type integer Width of extract area + * 'height' => @type integer Height of extract area * ]; * @throws Exception - * @method Image copy(array $options = []) Copy an image. + * @method static Image fitsload(string $filename, array $options = []) Load a FITS image. * @throws Exception - * @method Image tilecache(array $options = []) Cache an image as a set of tiles. + * @method static Image fitsload_source(Source $source, array $options = []) Load FITS from a source. * @throws Exception - * @method Image linecache(array $options = []) Cache an image as a set of lines. + * @method void fitssave(string $filename, array $options = []) Save image to fits file. * @throws Exception - * @method Image sequential(array $options = []) Check sequential access. + * @method Image flatten(array $options = []) Flatten alpha out of an image. * @throws Exception - * @method Image cache(array $options = []) Cache an image. + * @method Image flip(string $direction, array $options = []) Flip an image. + * @see Direction for possible values for $direction * @throws Exception - * @method Image embed(integer $x, integer $y, integer $width, integer $height, array $options = []) Embed an image in a larger image. + * @method Image float2rad(array $options = []) Transform float RGB to Radiance coding. + * @throws Exception + * @method static Image fractsurf(integer $width, integer $height, float $fractal_dimension, array $options = []) Make a fractal surface. + * @throws Exception + * @method Image freqmult(Image $mask, array $options = []) Frequency-domain filtering. + * @throws Exception + * @method Image fwfft(array $options = []) Forward FFT. + * @throws Exception + * @method Image gamma(array $options = []) Gamma an image. + * @throws Exception + * @method Image gaussblur(float $sigma, array $options = []) Gaussian blur. + * @throws Exception + * @method static Image gaussmat(float $sigma, float $min_ampl, array $options = []) Make a gaussian image. + * @throws Exception + * @method static Image gaussnoise(integer $width, integer $height, array $options = []) Make a gaussnoise image. + * @throws Exception + * @method array getpoint(integer $x, integer $y, array $options = []) Read a point from an image. + * @throws Exception + * @method static Image gifload(string $filename, array $options = []) Load GIF with libnsgif. + * @throws Exception + * @method static Image gifload_buffer(string $buffer, array $options = []) Load GIF with libnsgif. + * @throws Exception + * @method static Image gifload_source(Source $source, array $options = []) Load gif from source. + * @throws Exception + * @method void gifsave(string $filename, array $options = []) Save as gif. + * @throws Exception + * @method string gifsave_buffer(array $options = []) Save as gif. + * @throws Exception + * @method void gifsave_target(Target $target, array $options = []) Save as gif. + * @throws Exception + * @method Image globalbalance(array $options = []) Global balance an image mosaic. * @throws Exception * @method Image gravity(string $direction, integer $width, integer $height, array $options = []) Place an image within a larger image with a certain gravity. * @see CompassDirection for possible values for $direction * @throws Exception - * @method Image flip(string $direction, array $options = []) Flip an image. - * @see Direction for possible values for $direction + * @method static Image grey(integer $width, integer $height, array $options = []) Make a grey ramp image. * @throws Exception - * @method Image insert(Image $sub, integer $x, integer $y, array $options = []) Insert image @sub into @main at @x, @y. + * @method Image grid(integer $tile_height, integer $across, integer $down, array $options = []) Grid an image. * @throws Exception - * @method Image join(Image $in2, string $direction, array $options = []) Join a pair of images. - * @see Direction for possible values for $direction + * @method static Image heifload(string $filename, array $options = []) Load a HEIF image. * @throws Exception - * @method static Image arrayjoin(Image[]|image $in, array $options = []) Join an array of images. + * @method static Image heifload_buffer(string $buffer, array $options = []) Load a HEIF image. * @throws Exception - * @method Image smartcrop(integer $width, integer $height, array $options = []) Extract an area from an image. + * @method static Image heifload_source(Source $source, array $options = []) Load a HEIF image. * @throws Exception - * @method Image extract_band(integer $band, array $options = []) Extract band from an image. + * @method void heifsave(string $filename, array $options = []) Save image in HEIF format. * @throws Exception - * @method Image bandjoin_const(float[]|float $c, array $options = []) Append a constant band to an image. + * @method string heifsave_buffer(array $options = []) Save image in HEIF format. * @throws Exception - * @method Image bandmean(array $options = []) Band-wise average. + * @method void heifsave_target(Target $target, array $options = []) Save image in HEIF format. * @throws Exception - * @method Image bandbool(string $boolean, array $options = []) Boolean operation across image bands. - * @see OperationBoolean for possible values for $boolean + * @method Image hist_cum(array $options = []) Form cumulative histogram. * @throws Exception - * @method Image replicate(integer $across, integer $down, array $options = []) Replicate an image. + * @method float hist_entropy(array $options = []) Estimate image entropy. * @throws Exception - * @method Image cast(string $format, array $options = []) Cast an image. - * @see BandFormat for possible values for $format + * @method Image hist_equal(array $options = []) Histogram equalisation. * @throws Exception - * @method Image rot(string $angle, array $options = []) Rotate an image. - * @see Angle for possible values for $angle + * @method Image hist_find(array $options = []) Find image histogram. * @throws Exception - * @method Image rot45(array $options = []) Rotate an image. + * @method Image hist_find_indexed(Image $index, array $options = []) Find indexed image histogram. * @throws Exception - * @method Image autorot(array $options = []) Autorotate image by exif tag. + * @method Image hist_find_ndim(array $options = []) Find n-dimensional image histogram. * @throws Exception - * @method Image recomb(Image $m, array $options = []) Linear recombination with matrix. + * @method bool hist_ismonotonic(array $options = []) Test for monotonicity. * @throws Exception - * @method Image bandfold(array $options = []) Fold up x axis into bands. + * @method Image hist_local(integer $width, integer $height, array $options = []) Local histogram equalisation. * @throws Exception - * @method Image bandunfold(array $options = []) Unfold image bands into x axis. + * @method Image hist_match(Image $ref, array $options = []) Match two histograms. * @throws Exception - * @method Image flatten(array $options = []) Flatten alpha out of an image. + * @method Image hist_norm(array $options = []) Normalise histogram. * @throws Exception - * @method Image premultiply(array $options = []) Premultiply image alpha. + * @method Image hist_plot(array $options = []) Plot histogram. * @throws Exception - * @method Image unpremultiply(array $options = []) Unpremultiply image alpha. + * @method Image hough_circle(array $options = []) Find hough circle transform. * @throws Exception - * @method Image grid(integer $tile_height, integer $across, integer $down, array $options = []) Grid an image. + * @method Image hough_line(array $options = []) Find hough line transform. * @throws Exception - * @method Image scale(array $options = []) Scale an image to uchar. + * @method Image icc_export(array $options = []) Output to device with ICC profile. * @throws Exception - * @method Image wrap(array $options = []) Wrap image origin. + * @method Image icc_import(array $options = []) Import from device with ICC profile. * @throws Exception - * @method Image zoom(integer $xfac, integer $yfac, array $options = []) Zoom an image. + * @method Image icc_transform(string $output_profile, array $options = []) Transform between devices with ICC profiles. * @throws Exception - * @method Image subsample(integer $xfac, integer $yfac, array $options = []) Subsample an image. + * @method static Image identity(array $options = []) Make a 1D image where pixel values are indexes. * @throws Exception - * @method Image msb(array $options = []) Pick most-significant byte from an image. + * @method Image insert(Image $sub, integer $x, integer $y, array $options = []) Insert image @sub into @main at @x, @y. * @throws Exception - * @method Image byteswap(array $options = []) Byteswap an image. + * @method Image invert(array $options = []) Invert an image. * @throws Exception - * @method Image falsecolour(array $options = []) False-colour an image. + * @method Image invertlut(array $options = []) Build an inverted look-up table. * @throws Exception - * @method Image gamma(array $options = []) Gamma an image. + * @method Image invfft(array $options = []) Inverse FFT. * @throws Exception - * @method static Image composite(Image[]|image $in, integer[]|integer $mode, array $options = []) Blend an array of images with an array of blend modes. + * @method Image join(Image $in2, string $direction, array $options = []) Join a pair of images. + * @see Direction for possible values for $direction * @throws Exception - * @method Image composite2(Image $overlay, string $mode, array $options = []) Blend a pair of images with a blend mode. - * @see BlendMode for possible values for $mode + * @method static Image jp2kload(string $filename, array $options = []) Load JPEG2000 image. * @throws Exception - * @method static Image black(integer $width, integer $height, array $options = []) Make a black image. + * @method static Image jp2kload_buffer(string $buffer, array $options = []) Load JPEG2000 image. * @throws Exception - * @method static Image gaussnoise(integer $width, integer $height, array $options = []) Make a gaussnoise image. + * @method static Image jp2kload_source(Source $source, array $options = []) Load JPEG2000 image. * @throws Exception - * @method static Image text(string $text, array $options = []) Make a text image. + * @method void jp2ksave(string $filename, array $options = []) Save image in JPEG2000 format. * @throws Exception - * @method static Image xyz(integer $width, integer $height, array $options = []) Make an image where pixel values are coordinates. + * @method string jp2ksave_buffer(array $options = []) Save image in JPEG2000 format. * @throws Exception - * @method static Image gaussmat(float $sigma, float $min_ampl, array $options = []) Make a gaussian image. + * @method void jp2ksave_target(Target $target, array $options = []) Save image in JPEG2000 format. * @throws Exception - * @method static Image logmat(float $sigma, float $min_ampl, array $options = []) Make a laplacian of gaussian image. + * @method static Image jpegload(string $filename, array $options = []) Load jpeg from file. * @throws Exception - * @method static Image eye(integer $width, integer $height, array $options = []) Make an image showing the eye's spatial response. + * @method static Image jpegload_buffer(string $buffer, array $options = []) Load jpeg from buffer. * @throws Exception - * @method static Image grey(integer $width, integer $height, array $options = []) Make a grey ramp image. + * @method static Image jpegload_source(Source $source, array $options = []) Load image from jpeg source. * @throws Exception - * @method static Image zone(integer $width, integer $height, array $options = []) Make a zone plate. + * @method void jpegsave(string $filename, array $options = []) Save image to jpeg file. * @throws Exception - * @method static Image sines(integer $width, integer $height, array $options = []) Make a 2d sine wave. + * @method string jpegsave_buffer(array $options = []) Save image to jpeg buffer. * @throws Exception - * @method static Image mask_ideal(integer $width, integer $height, float $frequency_cutoff, array $options = []) Make an ideal filter. + * @method void jpegsave_mime(array $options = []) Save image to jpeg mime. * @throws Exception - * @method static Image mask_ideal_ring(integer $width, integer $height, float $frequency_cutoff, float $ringwidth, array $options = []) Make an ideal ring filter. + * @method void jpegsave_target(Target $target, array $options = []) Save image to jpeg target. * @throws Exception - * @method static Image mask_ideal_band(integer $width, integer $height, float $frequency_cutoff_x, float $frequency_cutoff_y, float $radius, array $options = []) Make an ideal band filter. + * @method static Image jxlload(string $filename, array $options = []) Load JPEG-XL image. * @throws Exception - * @method static Image mask_butterworth(integer $width, integer $height, float $order, float $frequency_cutoff, float $amplitude_cutoff, array $options = []) Make a butterworth filter. + * @method static Image jxlload_buffer(string $buffer, array $options = []) Load JPEG-XL image. * @throws Exception - * @method static Image mask_butterworth_ring(integer $width, integer $height, float $order, float $frequency_cutoff, float $amplitude_cutoff, float $ringwidth, array $options = []) Make a butterworth ring filter. + * @method static Image jxlload_source(Source $source, array $options = []) Load JPEG-XL image. * @throws Exception - * @method static Image mask_butterworth_band(integer $width, integer $height, float $order, float $frequency_cutoff_x, float $frequency_cutoff_y, float $radius, float $amplitude_cutoff, array $options = []) Make a butterworth_band filter. + * @method void jxlsave(string $filename, array $options = []) Save image in JPEG-XL format. * @throws Exception - * @method static Image mask_gaussian(integer $width, integer $height, float $frequency_cutoff, float $amplitude_cutoff, array $options = []) Make a gaussian filter. + * @method string jxlsave_buffer(array $options = []) Save image in JPEG-XL format. * @throws Exception - * @method static Image mask_gaussian_ring(integer $width, integer $height, float $frequency_cutoff, float $amplitude_cutoff, float $ringwidth, array $options = []) Make a gaussian ring filter. + * @method void jxlsave_target(Target $target, array $options = []) Save image in JPEG-XL format. * @throws Exception - * @method static Image mask_gaussian_band(integer $width, integer $height, float $frequency_cutoff_x, float $frequency_cutoff_y, float $radius, float $amplitude_cutoff, array $options = []) Make a gaussian filter. + * @method Image labelregions(array $options = []) Label regions in an image. * @throws Exception - * @method static Image mask_fractal(integer $width, integer $height, float $fractal_dimension, array $options = []) Make fractal filter. + * @method Image linear(float[]|float $a, float[]|float $b, array $options = []) Calculate (a * in + b). * @throws Exception - * @method Image buildlut(array $options = []) Build a look-up table. + * @method Image linecache(array $options = []) Cache an image as a set of lines. * @throws Exception - * @method Image invertlut(array $options = []) Build an inverted look-up table. + * @method static Image logmat(float $sigma, float $min_ampl, array $options = []) Make a Laplacian of Gaussian image. * @throws Exception - * @method static Image tonelut(array $options = []) Build a look-up table. + * @method static Image magickload(string $filename, array $options = []) Load file with ImageMagick. * @throws Exception - * @method static Image identity(array $options = []) Make a 1d image where pixel values are indexes. + * @method static Image magickload_buffer(string $buffer, array $options = []) Load buffer with ImageMagick. * @throws Exception - * @method static Image fractsurf(integer $width, integer $height, float $fractal_dimension, array $options = []) Make a fractal surface. + * @method void magicksave(string $filename, array $options = []) Save file with ImageMagick. * @throws Exception - * @method static Image worley(integer $width, integer $height, array $options = []) Make a worley noise image. + * @method string magicksave_buffer(array $options = []) Save image to magick buffer. * @throws Exception - * @method static Image perlin(integer $width, integer $height, array $options = []) Make a perlin noise image. + * @method Image mapim(Image $index, array $options = []) Resample with a map image. * @throws Exception - * @method static Image csvload(string $filename, array $options = []) Load csv from file. + * @method Image maplut(Image $lut, array $options = []) Map an image though a lut. * @throws Exception - * @method static Image matrixload(string $filename, array $options = []) Load matrix from file. + * @method static Image mask_butterworth(integer $width, integer $height, float $order, float $frequency_cutoff, float $amplitude_cutoff, array $options = []) Make a butterworth filter. * @throws Exception - * @method static Image rawload(string $filename, integer $width, integer $height, integer $bands, array $options = []) Load raw data from a file. + * @method static Image mask_butterworth_band(integer $width, integer $height, float $order, float $frequency_cutoff_x, float $frequency_cutoff_y, float $radius, float $amplitude_cutoff, array $options = []) Make a butterworth_band filter. * @throws Exception - * @method static Image vipsload(string $filename, array $options = []) Load vips from file. + * @method static Image mask_butterworth_ring(integer $width, integer $height, float $order, float $frequency_cutoff, float $amplitude_cutoff, float $ringwidth, array $options = []) Make a butterworth ring filter. * @throws Exception - * @method static Image analyzeload(string $filename, array $options = []) Load an analyze6 image. + * @method static Image mask_fractal(integer $width, integer $height, float $fractal_dimension, array $options = []) Make fractal filter. * @throws Exception - * @method static Image ppmload(string $filename, array $options = []) Load ppm from file. + * @method static Image mask_gaussian(integer $width, integer $height, float $frequency_cutoff, float $amplitude_cutoff, array $options = []) Make a gaussian filter. * @throws Exception - * @method static Image radload(string $filename, array $options = []) Load a radiance image from a file. + * @method static Image mask_gaussian_band(integer $width, integer $height, float $frequency_cutoff_x, float $frequency_cutoff_y, float $radius, float $amplitude_cutoff, array $options = []) Make a gaussian filter. * @throws Exception - * @method static Image pdfload(string $filename, array $options = []) Load pdf with libpoppler. + * @method static Image mask_gaussian_ring(integer $width, integer $height, float $frequency_cutoff, float $amplitude_cutoff, float $ringwidth, array $options = []) Make a gaussian ring filter. * @throws Exception - * @method static Image pdfload_buffer(string $buffer, array $options = []) Load pdf with libpoppler. + * @method static Image mask_ideal(integer $width, integer $height, float $frequency_cutoff, array $options = []) Make an ideal filter. * @throws Exception - * @method static Image svgload(string $filename, array $options = []) Load svg with rsvg. + * @method static Image mask_ideal_band(integer $width, integer $height, float $frequency_cutoff_x, float $frequency_cutoff_y, float $radius, array $options = []) Make an ideal band filter. * @throws Exception - * @method static Image svgload_buffer(string $buffer, array $options = []) Load svg with rsvg. + * @method static Image mask_ideal_ring(integer $width, integer $height, float $frequency_cutoff, float $ringwidth, array $options = []) Make an ideal ring filter. * @throws Exception - * @method static Image gifload(string $filename, array $options = []) Load gif with giflib. + * @method Image match(Image $sec, integer $xr1, integer $yr1, integer $xs1, integer $ys1, integer $xr2, integer $yr2, integer $xs2, integer $ys2, array $options = []) First-order match of two images. * @throws Exception - * @method static Image gifload_buffer(string $buffer, array $options = []) Load gif with giflib. + * @method Image math(string $math, array $options = []) Apply a math operation to an image. + * @see OperationMath for possible values for $math * @throws Exception - * @method static Image pngload(string $filename, array $options = []) Load png from file. + * @method Image math2(Image $right, string $math2, array $options = []) Binary math operations. + * @see OperationMath2 for possible values for $math2 * @throws Exception - * @method static Image pngload_buffer(string $buffer, array $options = []) Load png from buffer. + * @method Image math2_const(string $math2, float[]|float $c, array $options = []) Binary math operations with a constant. + * @see OperationMath2 for possible values for $math2 * @throws Exception * @method static Image matload(string $filename, array $options = []) Load mat from file. * @throws Exception - * @method static Image jpegload(string $filename, array $options = []) Load jpeg from file. + * @method Image matrixinvert(array $options = []) Invert a matrix. * @throws Exception - * @method static Image jpegload_buffer(string $buffer, array $options = []) Load jpeg from buffer. + * @method static Image matrixload(string $filename, array $options = []) Load matrix. * @throws Exception - * @method static Image webpload(string $filename, array $options = []) Load webp from file. + * @method static Image matrixload_source(Source $source, array $options = []) Load matrix. * @throws Exception - * @method static Image webpload_buffer(string $buffer, array $options = []) Load webp from buffer. + * @method Image matrixmultiply(Image $right, array $options = []) Multiply two matrices. * @throws Exception - * @method static Image tiffload(string $filename, array $options = []) Load tiff from file. + * @method void matrixprint(array $options = []) Print matrix. * @throws Exception - * @method static Image tiffload_buffer(string $buffer, array $options = []) Load tiff from buffer. + * @method void matrixsave(string $filename, array $options = []) Save image to matrix. * @throws Exception - * @method static Image openslideload(string $filename, array $options = []) Load file with openslide. + * @method void matrixsave_target(Target $target, array $options = []) Save image to matrix. * @throws Exception - * @method static Image magickload(string $filename, array $options = []) Load file with imagemagick. + * @method float max(array $options = []) Find image maximum. * @throws Exception - * @method static Image magickload_buffer(string $buffer, array $options = []) Load buffer with imagemagick. + * @method Image maxpair(Image $right, array $options = []) Maximum of a pair of images. * @throws Exception - * @method static Image fitsload(string $filename, array $options = []) Load a fits image. + * @method Image measure(integer $h, integer $v, array $options = []) Measure a set of patches on a color chart. * @throws Exception - * @method static Image openexrload(string $filename, array $options = []) Load an openexr image. + * @method Image merge(Image $sec, string $direction, integer $dx, integer $dy, array $options = []) Merge two images. + * @see Direction for possible values for $direction * @throws Exception - * @method void csvsave(string $filename, array $options = []) Save image to csv file. + * @method float min(array $options = []) Find image minimum. * @throws Exception - * @method void matrixsave(string $filename, array $options = []) Save image to matrix file. + * @method Image minpair(Image $right, array $options = []) Minimum of a pair of images. * @throws Exception - * @method void matrixprint(array $options = []) Print matrix. + * @method Image morph(Image $mask, string $morph, array $options = []) Morphology operation. + * @see OperationMorphology for possible values for $morph * @throws Exception - * @method void rawsave(string $filename, array $options = []) Save image to raw file. + * @method Image mosaic(Image $sec, string $direction, integer $xref, integer $yref, integer $xsec, integer $ysec, array $options = []) Mosaic two images. + * @see Direction for possible values for $direction * @throws Exception - * @method void rawsave_fd(integer $fd, array $options = []) Write raw image to file descriptor. + * @method Image mosaic1(Image $sec, string $direction, integer $xr1, integer $yr1, integer $xs1, integer $ys1, integer $xr2, integer $yr2, integer $xs2, integer $ys2, array $options = []) First-order mosaic of two images. + * @see Direction for possible values for $direction * @throws Exception - * @method void vipssave(string $filename, array $options = []) Save image to vips file. + * @method Image msb(array $options = []) Pick most-significant byte from an image. * @throws Exception - * @method void ppmsave(string $filename, array $options = []) Save image to ppm file. + * @method static Image niftiload(string $filename, array $options = []) Load NIfTI volume. * @throws Exception - * @method void radsave(string $filename, array $options = []) Save image to radiance file. + * @method static Image niftiload_source(Source $source, array $options = []) Load NIfTI volumes. * @throws Exception - * @method string radsave_buffer(array $options = []) Save image to radiance buffer. + * @method void niftisave(string $filename, array $options = []) Save image to nifti file. * @throws Exception - * @method void dzsave(string $filename, array $options = []) Save image to deepzoom file. + * @method static Image openexrload(string $filename, array $options = []) Load an OpenEXR image. * @throws Exception - * @method string dzsave_buffer(array $options = []) Save image to dz buffer. + * @method static Image openslideload(string $filename, array $options = []) Load file with OpenSlide. * @throws Exception - * @method void pngsave(string $filename, array $options = []) Save image to png file. + * @method static Image openslideload_source(Source $source, array $options = []) Load source with OpenSlide. * @throws Exception - * @method string pngsave_buffer(array $options = []) Save image to png buffer. + * @method static Image pdfload(string $filename, array $options = []) Load PDF from file. * @throws Exception - * @method void jpegsave(string $filename, array $options = []) Save image to jpeg file. + * @method static Image pdfload_buffer(string $buffer, array $options = []) Load PDF from buffer. * @throws Exception - * @method string jpegsave_buffer(array $options = []) Save image to jpeg buffer. + * @method static Image pdfload_source(Source $source, array $options = []) Load PDF from source. * @throws Exception - * @method void jpegsave_mime(array $options = []) Save image to jpeg mime. + * @method integer percent(float $percent, array $options = []) Find threshold for percent of pixels. * @throws Exception - * @method void webpsave(string $filename, array $options = []) Save image to webp file. + * @method static Image perlin(integer $width, integer $height, array $options = []) Make a perlin noise image. * @throws Exception - * @method string webpsave_buffer(array $options = []) Save image to webp buffer. + * @method Image phasecor(Image $in2, array $options = []) Calculate phase correlation. * @throws Exception - * @method void tiffsave(string $filename, array $options = []) Save image to tiff file. + * @method static Image pngload(string $filename, array $options = []) Load png from file. * @throws Exception - * @method string tiffsave_buffer(array $options = []) Save image to tiff buffer. + * @method static Image pngload_buffer(string $buffer, array $options = []) Load png from buffer. * @throws Exception - * @method void fitssave(string $filename, array $options = []) Save image to fits file. + * @method static Image pngload_source(Source $source, array $options = []) Load png from source. * @throws Exception - * @method static Image thumbnail(string $filename, integer $width, array $options = []) Generate thumbnail from file. + * @method void pngsave(string $filename, array $options = []) Save image to file as PNG. * @throws Exception - * @method static Image thumbnail_buffer(string $buffer, integer $width, array $options = []) Generate thumbnail from buffer. + * @method string pngsave_buffer(array $options = []) Save image to buffer as PNG. * @throws Exception - * @method Image thumbnail_image(integer $width, array $options = []) Generate thumbnail from image. + * @method void pngsave_target(Target $target, array $options = []) Save image to target as PNG. * @throws Exception - * @method Image mapim(Image $index, array $options = []) Resample with an mapim image. + * @method static Image ppmload(string $filename, array $options = []) Load ppm from file. * @throws Exception - * @method Image shrink(float $hshrink, float $vshrink, array $options = []) Shrink an image. + * @method static Image ppmload_source(Source $source, array $options = []) Load ppm base class. * @throws Exception - * @method Image shrinkh(integer $hshrink, array $options = []) Shrink an image horizontally. + * @method void ppmsave(string $filename, array $options = []) Save image to ppm file. * @throws Exception - * @method Image shrinkv(integer $vshrink, array $options = []) Shrink an image vertically. + * @method void ppmsave_target(Target $target, array $options = []) Save to ppm. * @throws Exception - * @method Image reduceh(float $hshrink, array $options = []) Shrink an image horizontally. + * @method Image premultiply(array $options = []) Premultiply image alpha. * @throws Exception - * @method Image reducev(float $vshrink, array $options = []) Shrink an image vertically. + * @method Image prewitt(array $options = []) Prewitt edge detector. * @throws Exception - * @method Image reduce(float $hshrink, float $vshrink, array $options = []) Reduce an image. + * @method array profile(array $options = []) Find image profiles. + * Return array with: [ + * 'columns' => @type Image First non-zero pixel in column + * 'rows' => @type Image First non-zero pixel in row + * ]; + * @throws Exception + * @method static string profile_load(string $name, array $options = []) Load named ICC profile. + * @throws Exception + * @method array project(array $options = []) Find image projections. + * Return array with: [ + * 'columns' => @type Image Sums of columns + * 'rows' => @type Image Sums of rows + * ]; * @throws Exception * @method Image quadratic(Image $coeff, array $options = []) Resample an image with a quadratic transform. * @throws Exception - * @method Image affine(float[]|float $matrix, array $options = []) Affine transform of an image. + * @method Image rad2float(array $options = []) Unpack Radiance coding to float RGB. * @throws Exception - * @method Image similarity(array $options = []) Similarity transform of an image. + * @method static Image radload(string $filename, array $options = []) Load a Radiance image from a file. * @throws Exception - * @method Image resize(float $scale, array $options = []) Resize an image. + * @method static Image radload_buffer(string $buffer, array $options = []) Load rad from buffer. * @throws Exception - * @method Image colourspace(string $space, array $options = []) Convert to a new colourspace. - * @see Interpretation for possible values for $space + * @method static Image radload_source(Source $source, array $options = []) Load rad from source. + * @throws Exception + * @method void radsave(string $filename, array $options = []) Save image to Radiance file. * @throws Exception - * @method Image Lab2XYZ(array $options = []) Transform cielab to xyz. + * @method string radsave_buffer(array $options = []) Save image to Radiance buffer. * @throws Exception - * @method Image XYZ2Lab(array $options = []) Transform xyz to lab. + * @method void radsave_target(Target $target, array $options = []) Save image to Radiance target. * @throws Exception - * @method Image Lab2LCh(array $options = []) Transform lab to lch. + * @method Image rank(integer $width, integer $height, integer $index, array $options = []) Rank filter. * @throws Exception - * @method Image LCh2Lab(array $options = []) Transform lch to lab. + * @method static Image rawload(string $filename, integer $width, integer $height, integer $bands, array $options = []) Load raw data from a file. * @throws Exception - * @method Image LCh2CMC(array $options = []) Transform lch to cmc. + * @method void rawsave(string $filename, array $options = []) Save image to raw file. * @throws Exception - * @method Image CMC2LCh(array $options = []) Transform lch to cmc. + * @method string rawsave_buffer(array $options = []) Write raw image to buffer. * @throws Exception - * @method Image XYZ2Yxy(array $options = []) Transform xyz to yxy. + * @method void rawsave_target(Target $target, array $options = []) Write raw image to target. * @throws Exception - * @method Image Yxy2XYZ(array $options = []) Transform yxy to xyz. + * @method Image recomb(Image $m, array $options = []) Linear recombination with matrix. * @throws Exception - * @method Image scRGB2XYZ(array $options = []) Transform scrgb to xyz. + * @method Image reduce(float $hshrink, float $vshrink, array $options = []) Reduce an image. * @throws Exception - * @method Image XYZ2scRGB(array $options = []) Transform xyz to scrgb. + * @method Image reduceh(float $hshrink, array $options = []) Shrink an image horizontally. * @throws Exception - * @method Image LabQ2Lab(array $options = []) Unpack a labq image to float lab. + * @method Image reducev(float $vshrink, array $options = []) Shrink an image vertically. * @throws Exception - * @method Image Lab2LabQ(array $options = []) Transform float lab to labq coding. + * @method Image relational(Image $right, string $relational, array $options = []) Relational operation on two images. + * @see OperationRelational for possible values for $relational * @throws Exception - * @method Image LabQ2LabS(array $options = []) Unpack a labq image to short lab. + * @method Image relational_const(string $relational, float[]|float $c, array $options = []) Relational operations against a constant. + * @see OperationRelational for possible values for $relational * @throws Exception - * @method Image LabS2LabQ(array $options = []) Transform short lab to labq coding. + * @method Image remainder_const(float[]|float $c, array $options = []) Remainder after integer division of an image and a constant. * @throws Exception - * @method Image LabS2Lab(array $options = []) Transform signed short lab to float. + * @method Image remosaic(string $old_str, string $new_str, array $options = []) Rebuild an mosaiced image. * @throws Exception - * @method Image Lab2LabS(array $options = []) Transform float lab to signed short. + * @method Image replicate(integer $across, integer $down, array $options = []) Replicate an image. * @throws Exception - * @method Image rad2float(array $options = []) Unpack radiance coding to float rgb. + * @method Image resize(float $scale, array $options = []) Resize an image. * @throws Exception - * @method Image float2rad(array $options = []) Transform float rgb to radiance coding. + * @method Image rot(string $angle, array $options = []) Rotate an image. + * @see Angle for possible values for $angle * @throws Exception - * @method Image LabQ2sRGB(array $options = []) Convert a labq image to srgb. + * @method Image rot45(array $options = []) Rotate an image. * @throws Exception - * @method Image sRGB2HSV(array $options = []) Transform srgb to hsv. + * @method Image rotate(float $angle, array $options = []) Rotate an image by a number of degrees. * @throws Exception - * @method Image HSV2sRGB(array $options = []) Transform hsv to srgb. + * @method Image round(string $round, array $options = []) Perform a round function on an image. + * @see OperationRound for possible values for $round * @throws Exception - * @method Image icc_import(array $options = []) Import from device with icc profile. + * @method Image sRGB2HSV(array $options = []) Transform sRGB to HSV. * @throws Exception - * @method Image icc_export(array $options = []) Output to device with icc profile. + * @method Image sRGB2scRGB(array $options = []) Convert an sRGB image to scRGB. * @throws Exception - * @method Image icc_transform(string $output_profile, array $options = []) Transform between devices with icc profiles. + * @method Image scRGB2BW(array $options = []) Convert scRGB to BW. * @throws Exception - * @method Image dE76(Image $right, array $options = []) Calculate de76. + * @method Image scRGB2XYZ(array $options = []) Transform scRGB to XYZ. * @throws Exception - * @method Image dE00(Image $right, array $options = []) Calculate de00. + * @method Image scRGB2sRGB(array $options = []) Convert scRGB to sRGB. * @throws Exception - * @method Image dECMC(Image $right, array $options = []) Calculate decmc. + * @method Image scale(array $options = []) Scale an image to uchar. * @throws Exception - * @method Image sRGB2scRGB(array $options = []) Convert an srgb image to scrgb. + * @method Image scharr(array $options = []) Scharr edge detector. * @throws Exception - * @method Image scRGB2BW(array $options = []) Convert scrgb to bw. + * @method static Image sdf(integer $width, integer $height, string $shape, array $options = []) Create an SDF image. + * @see SdfShape for possible values for $shape * @throws Exception - * @method Image scRGB2sRGB(array $options = []) Convert an scrgb image to srgb. + * @method Image sequential(array $options = []) Check sequential access. * @throws Exception - * @method Image maplut(Image $lut, array $options = []) Map an image though a lut. + * @method Image sharpen(array $options = []) Unsharp masking for print. * @throws Exception - * @method integer percent(float $percent, array $options = []) Find threshold for percent of pixels. + * @method Image shrink(float $hshrink, float $vshrink, array $options = []) Shrink an image. * @throws Exception - * @method Image stdif(integer $width, integer $height, array $options = []) Statistical difference. + * @method Image shrinkh(integer $hshrink, array $options = []) Shrink an image horizontally. * @throws Exception - * @method Image hist_cum(array $options = []) Form cumulative histogram. + * @method Image shrinkv(integer $vshrink, array $options = []) Shrink an image vertically. * @throws Exception - * @method Image hist_match(Image $ref, array $options = []) Match two histograms. + * @method Image sign(array $options = []) Unit vector of pixel. * @throws Exception - * @method Image hist_norm(array $options = []) Normalise histogram. + * @method Image similarity(array $options = []) Similarity transform of an image. * @throws Exception - * @method Image hist_equal(array $options = []) Histogram equalisation. + * @method static Image sines(integer $width, integer $height, array $options = []) Make a 2D sine wave. * @throws Exception - * @method Image hist_plot(array $options = []) Plot histogram. + * @method Image smartcrop(integer $width, integer $height, array $options = []) Extract an area from an image. * @throws Exception - * @method Image hist_local(integer $width, integer $height, array $options = []) Local histogram equalisation. + * @method Image sobel(array $options = []) Sobel edge detector. * @throws Exception - * @method bool hist_ismonotonic(array $options = []) Test for monotonicity. + * @method Image spcor(Image $ref, array $options = []) Spatial correlation. * @throws Exception - * @method float hist_entropy(array $options = []) Estimate image entropy. + * @method Image spectrum(array $options = []) Make displayable power spectrum. * @throws Exception - * @method Image conv(Image $mask, array $options = []) Convolution operation. + * @method Image stats(array $options = []) Find many image stats. * @throws Exception - * @method Image conva(Image $mask, array $options = []) Approximate integer convolution. + * @method Image stdif(integer $width, integer $height, array $options = []) Statistical difference. * @throws Exception - * @method Image convf(Image $mask, array $options = []) Float convolution operation. + * @method Image subsample(integer $xfac, integer $yfac, array $options = []) Subsample an image. * @throws Exception - * @method Image convi(Image $mask, array $options = []) Int convolution operation. + * @method static Image sum(Image[]|Image $in, array $options = []) Sum an array of images. * @throws Exception - * @method Image compass(Image $mask, array $options = []) Convolve with rotating mask. + * @method static Image svgload(string $filename, array $options = []) Load SVG with rsvg. * @throws Exception - * @method Image convsep(Image $mask, array $options = []) Seperable convolution operation. + * @method static Image svgload_buffer(string $buffer, array $options = []) Load SVG with rsvg. * @throws Exception - * @method Image convasep(Image $mask, array $options = []) Approximate separable integer convolution. + * @method static Image svgload_source(Source $source, array $options = []) Load svg from source. * @throws Exception - * @method Image fastcor(Image $ref, array $options = []) Fast correlation. + * @method static Image switch(Image[]|Image $tests, array $options = []) Find the index of the first non-zero pixel in tests. * @throws Exception - * @method Image spcor(Image $ref, array $options = []) Spatial correlation. + * @method static void system(string $cmd_format, array $options = []) Run an external command. * @throws Exception - * @method Image sharpen(array $options = []) Unsharp masking for print. + * @method static Image text(string $text, array $options = []) Make a text image. * @throws Exception - * @method Image gaussblur(float $sigma, array $options = []) Gaussian blur. + * @method static Image thumbnail(string $filename, integer $width, array $options = []) Generate thumbnail from file. * @throws Exception - * @method Image fwfft(array $options = []) Forward fft. + * @method static Image thumbnail_buffer(string $buffer, integer $width, array $options = []) Generate thumbnail from buffer. * @throws Exception - * @method Image invfft(array $options = []) Inverse fft. + * @method Image thumbnail_image(integer $width, array $options = []) Generate thumbnail from image. * @throws Exception - * @method Image freqmult(Image $mask, array $options = []) Frequency-domain filtering. + * @method static Image thumbnail_source(Source $source, integer $width, array $options = []) Generate thumbnail from source. * @throws Exception - * @method Image spectrum(array $options = []) Make displayable power spectrum. + * @method static Image tiffload(string $filename, array $options = []) Load tiff from file. * @throws Exception - * @method Image phasecor(Image $in2, array $options = []) Calculate phase correlation. + * @method static Image tiffload_buffer(string $buffer, array $options = []) Load tiff from buffer. * @throws Exception - * @method Image morph(Image $mask, string $morph, array $options = []) Morphology operation. - * @see OperationMorphology for possible values for $morph + * @method static Image tiffload_source(Source $source, array $options = []) Load tiff from source. * @throws Exception - * @method Image rank(integer $width, integer $height, integer $index, array $options = []) Rank filter. + * @method void tiffsave(string $filename, array $options = []) Save image to tiff file. * @throws Exception - * @method float countlines(string $direction, array $options = []) Count lines in an image. - * @see Direction for possible values for $direction + * @method string tiffsave_buffer(array $options = []) Save image to tiff buffer. * @throws Exception - * @method Image labelregions(array $options = []) Label regions in an image. + * @method void tiffsave_target(Target $target, array $options = []) Save image to tiff target. * @throws Exception - * @method Image fill_nearest(array $options = []) Fill image zeros with nearest non-zero pixel. + * @method Image tilecache(array $options = []) Cache an image as a set of tiles. * @throws Exception - * @method Image draw_rect(float[]|float $ink, integer $left, integer $top, integer $width, integer $height, array $options = []) Paint a rectangle on an image. + * @method static Image tonelut(array $options = []) Build a look-up table. * @throws Exception - * @method Image draw_mask(float[]|float $ink, Image $mask, integer $x, integer $y, array $options = []) Draw a mask on an image. + * @method Image transpose3d(array $options = []) Transpose3d an image. * @throws Exception - * @method Image draw_line(float[]|float $ink, integer $x1, integer $y1, integer $x2, integer $y2, array $options = []) Draw a line on an image. + * @method Image unpremultiply(array $options = []) Unpremultiply image alpha. * @throws Exception - * @method Image draw_circle(float[]|float $ink, integer $cx, integer $cy, integer $radius, array $options = []) Draw a circle on an image. + * @method static Image vipsload(string $filename, array $options = []) Load vips from file. * @throws Exception - * @method Image draw_flood(float[]|float $ink, integer $x, integer $y, array $options = []) Flood-fill an area. + * @method static Image vipsload_source(Source $source, array $options = []) Load vips from source. * @throws Exception - * @method Image draw_image(Image $sub, integer $x, integer $y, array $options = []) Paint an image into another image. + * @method void vipssave(string $filename, array $options = []) Save image to file in vips format. * @throws Exception - * @method Image draw_smudge(integer $left, integer $top, integer $width, integer $height, array $options = []) Blur a rectangle on an image. + * @method void vipssave_target(Target $target, array $options = []) Save image to target in vips format. * @throws Exception - * @method Image merge(Image $sec, string $direction, integer $dx, integer $dy, array $options = []) Merge two images. - * @see Direction for possible values for $direction + * @method static Image webpload(string $filename, array $options = []) Load webp from file. * @throws Exception - * @method Image mosaic(Image $sec, string $direction, integer $xref, integer $yref, integer $xsec, integer $ysec, array $options = []) Mosaic two images. - * @see Direction for possible values for $direction + * @method static Image webpload_buffer(string $buffer, array $options = []) Load webp from buffer. * @throws Exception - * @method Image mosaic1(Image $sec, string $direction, integer $xr1, integer $yr1, integer $xs1, integer $ys1, integer $xr2, integer $yr2, integer $xs2, integer $ys2, array $options = []) First-order mosaic of two images. - * @see Direction for possible values for $direction + * @method static Image webpload_source(Source $source, array $options = []) Load webp from source. * @throws Exception - * @method Image match(Image $sec, integer $xr1, integer $yr1, integer $xs1, integer $ys1, integer $xr2, integer $yr2, integer $xs2, integer $ys2, array $options = []) First-order match of two images. + * @method void webpsave(string $filename, array $options = []) Save as WebP. * @throws Exception - * @method Image globalbalance(array $options = []) Global balance an image mosaic. + * @method string webpsave_buffer(array $options = []) Save as WebP. + * @throws Exception + * @method void webpsave_mime(array $options = []) Save image to webp mime. + * @throws Exception + * @method void webpsave_target(Target $target, array $options = []) Save as WebP. + * @throws Exception + * @method static Image worley(integer $width, integer $height, array $options = []) Make a worley noise image. + * @throws Exception + * @method Image wrap(array $options = []) Wrap image origin. + * @throws Exception + * @method static Image xyz(integer $width, integer $height, array $options = []) Make an image where pixel values are coordinates. + * @throws Exception + * @method static Image zone(integer $width, integer $height, array $options = []) Make a zone plate. + * @throws Exception + * @method Image zoom(integer $xfac, integer $yfac, array $options = []) Zoom an image. * @throws Exception - * @method Image extract_area(integer $left, integer $top, integer $width, integer $height, array $options = []) Extract an area from an image. - * @method Image crop(integer $left, integer $top, integer $width, integer $height, array $options = []) Extract an area from an image. * - * @property string $nickname Class nickname - * @property string $description Class description * @property integer $width Image width in pixels * @property integer $height Image height in pixels * @property integer $bands Number of bands in image @@ -571,18 +716,14 @@ * @see Coding for possible values * @property string $interpretation Pixel interpretation * @see Interpretation for possible values - * @property float $xres Horizontal resolution in pixels/mm - * @property float $yres Vertical resolution in pixels/mm * @property integer $xoffset Horizontal offset of origin * @property integer $yoffset Vertical offset of origin + * @property float $xres Horizontal resolution in pixels/mm + * @property float $yres Vertical resolution in pixels/mm * @property string $filename Image filename - * @property string $mode Open mode - * @property bool $kill Block evaluation on this image - * @property string $demand Preferred demand style for this image - * @see DemandStyle for possible values - * @property integer $sizeof_header Offset in bytes from start of file - * @property string $foreign_buffer Pointer to foreign pixels */ -abstract class ImageAutodoc +abstract class ImageAutodoc extends VipsObject { + abstract public function __set(string $name, $value); + abstract public function __get(string $name); } diff --git a/src/Intent.php b/src/Intent.php index fab3249..658264e 100644 --- a/src/Intent.php +++ b/src/Intent.php @@ -53,4 +53,5 @@ abstract class Intent const RELATIVE = 'relative'; const SATURATION = 'saturation'; const ABSOLUTE = 'absolute'; + const AUTO = 'auto'; } diff --git a/src/Interesting.php b/src/Interesting.php index 2d64c21..b7166cf 100644 --- a/src/Interesting.php +++ b/src/Interesting.php @@ -53,4 +53,7 @@ abstract class Interesting const CENTRE = 'centre'; const ENTROPY = 'entropy'; const ATTENTION = 'attention'; + const LOW = 'low'; + const HIGH = 'high'; + const ALL = 'all'; } diff --git a/src/Interpolate.php b/src/Interpolate.php new file mode 100644 index 0000000..e20ba12 --- /dev/null +++ b/src/Interpolate.php @@ -0,0 +1,104 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/libvips/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * This class holds a pointer to a VipsInterpolate (the libvips + * base class for interpolators) and manages argument introspection and + * operation call. + * + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/libvips/php-vips + */ +class Interpolate extends VipsObject +{ + /** + * A pointer to the underlying Interpolate. This is the same as the + * GObject, just cast to Interpolate to help FFI. + * + * @internal + */ + public \FFI\CData $pointer; + + public function __construct(\FFI\CData $pointer) + { + $this->pointer = FFI::vips()->cast(FFI::ctypes("VipsInterpolate"), $pointer); + + parent::__construct($pointer); + } + + /** + * Make an interpolator from a name. + * + * @param string $name Name of the interpolator. + * + * Possible interpolators are: + * - `'nearest'`: Use nearest neighbour interpolation. + * - `'bicubic'`: Use bicubic interpolation. + * - `'bilinear'`: Use bilinear interpolation (the default). + * - `'nohalo'`: Use Nohalo interpolation. + * - `'lbb'`: Use LBB interpolation. + * - `'vsqbs'`: Use the VSQBS interpolation. + * + * @return Interpolate The interpolator. + * @throws Exception If unable to make a new interpolator from $name. + */ + public static function newFromName(string $name): Interpolate + { + $pointer = FFI::vips()->vips_interpolate_new($name); + if ($pointer == null) { + throw new Exception(); + } + + return new Interpolate($pointer); + } +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + * vim600: expandtab sw=4 ts=4 fdm=marker + * vim<600: expandtab sw=4 ts=4 + */ diff --git a/src/Introspect.php b/src/Introspect.php new file mode 100644 index 0000000..489364d --- /dev/null +++ b/src/Introspect.php @@ -0,0 +1,249 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/libvips/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * Introspect a VipsOperation and discover everything we can. This is called + * on demand once per operation and the results held in a cache. + * + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/libvips/php-vips + */ +class Introspect +{ + /** + * The operation nickname (eg. "add"). + */ + public string $name; + + /** + * The operation description (e.g. "add two images"). + */ + public string $description; + + /** + * The operation flags (e.g. SEQUENTIAL | DEPRECATED). + */ + public int $flags; + + /** + * A hash from arg name to a hash of details. + */ + public array $arguments; + + /** + * Arrays of arg names, in order and by category, eg. $this->required_input + * = ["filename"]. + */ + public array $required_input; + public array $optional_input; + public array $required_output; + public array $optional_output; + + /** + * The name of the arg this operation uses as "this". + */ + public string $member_this; + + /** + * And the required input args, without the "this". + */ + public array $method_args; + + /** + * @throws Exception + */ + public function __construct($operation_name) + { + $this->name = $operation_name; + + $operation = VipsOperation::newFromName($operation_name); + + $this->description = $operation->getDescription(); + + $p_names = FFI::vips()->new("char**[1]"); + $p_flags = FFI::vips()->new("int*[1]"); + $p_n_args = FFI::vips()->new("int[1]"); + $result = FFI::vips()->vips_object_get_args( + FFI::vips()->cast(FFI::ctypes("VipsObject"), $operation->pointer), + $p_names, + $p_flags, + $p_n_args + ); + if ($result != 0) { + throw new Exception(); + } + $p_names = $p_names[0]; + $p_flags = $p_flags[0]; + $n_args = $p_n_args[0]; + + # make a hash from arg name to flags + $argumentFlags = []; + for ($i = 0; $i < $n_args; $i++) { + if (($p_flags[$i] & ArgumentFlags::CONSTRUCT) != 0) { + # make sure we're using "_" to separate arg components, though + # I think libvips is "_" everywhere now + $name = \FFI::string($p_names[$i]); + $name = str_replace("-", "_", $name); + $argumentFlags[$name] = $p_flags[$i]; + } + } + + # make a hash from arg name to detailed arg info + $this->arguments = []; + foreach ($argumentFlags as $name => $flags) { + $this->arguments[$name] = [ + "name" => $name, + "flags" => $flags, + "blurb" => $operation->getBlurb($name), + "type" => $operation->getType($name) + ]; + } + + # split args into categories + $this->required_input = []; + $this->optional_input = []; + $this->required_output = []; + $this->optional_output = []; + + foreach ($this->arguments as $name => $details) { + $flags = $details["flags"]; + + if (($flags & ArgumentFlags::INPUT) && + ($flags & ArgumentFlags::REQUIRED) && + !($flags & ArgumentFlags::DEPRECATED)) { + $this->required_input[] = $name; + + # required inputs which we MODIFY are also required outputs + if ($flags & ArgumentFlags::MODIFY) { + $this->required_output[] = $name; + } + } + + if (($flags & ArgumentFlags::OUTPUT) && + ($flags & ArgumentFlags::REQUIRED) && + !($flags & ArgumentFlags::DEPRECATED)) { + $this->required_output[] = $name; + } + + # we let deprecated optional args through, but warn about them + # if they get used, see below + if (($flags & ArgumentFlags::INPUT) && + !($flags & ArgumentFlags::REQUIRED)) { + $this->optional_input[] = $name; + } + + if (($flags & ArgumentFlags::OUTPUT) && + !($flags & ArgumentFlags::REQUIRED)) { + $this->optional_output[] = $name; + } + } + + # find the first required input image arg, if any ... that will be self + $this->member_this = ""; + foreach ($this->required_input as $name) { + $type = $this->arguments[$name]["type"]; + if ($type == FFI::gtypes("VipsImage")) { + $this->member_this = $name; + break; + } + } + + # method args are required args, but without the image they are a + # method on + $this->method_args = $this->required_input; + if ($this->member_this != "") { + $index = array_search($this->member_this, $this->method_args); + array_splice($this->method_args, $index); + } + + Utils::debugLog($operation_name, ['introspect' => strval($this)]); + } + + public function __toString(): string + { + $result = "$this->name:\n"; + + foreach ($this->arguments as $name => $details) { + $flags = $details["flags"]; + $blurb = $details["blurb"]; + $type = $details["type"]; + $typeName = FFI::gobject()->g_type_name($type); + + $result .= " $name:\n"; + + $result .= " flags: $flags\n"; + foreach (ArgumentFlags::NAMES as $flag_name => $flag) { + if ($flags & $flag) { + $result .= " $flag_name\n"; + } + } + + $result .= " blurb: $blurb\n"; + $result .= " type: $typeName\n"; + } + + $info = implode(", ", $this->required_input); + $result .= "required input: $info\n"; + $info = implode(", ", $this->required_output); + $result .= "required output: $info\n"; + $info = implode(", ", $this->optional_input); + $result .= "optional input: $info\n"; + $info = implode(", ", $this->optional_output); + $result .= "optional output: $info\n"; + $result .= "member_this: $this->member_this\n"; + $info = implode(", ", $this->method_args); + $result .= "method args: $info\n"; + + return $result; + } +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + * vim600: expandtab sw=4 ts=4 fdm=marker + * vim<600: expandtab sw=4 ts=4 + */ diff --git a/src/Kernel.php b/src/Kernel.php index 43bb236..27b335c 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -52,6 +52,9 @@ abstract class Kernel const NEAREST = 'nearest'; const LINEAR = 'linear'; const CUBIC = 'cubic'; + const MITCHELL = 'mitchell'; const LANCZOS2 = 'lanczos2'; const LANCZOS3 = 'lanczos3'; + const MKS2013 = 'mks2013'; + const MKS2021 = 'mks2021'; } diff --git a/src/OperationBoolean.php b/src/OperationBoolean.php index 78f6f40..5b69509 100644 --- a/src/OperationBoolean.php +++ b/src/OperationBoolean.php @@ -49,8 +49,8 @@ */ abstract class OperationBoolean { - const AND = 'and'; - const OR = 'or'; + const AND1 = 'and'; + const OR1 = 'or'; const EOR = 'eor'; const LSHIFT = 'lshift'; const RSHIFT = 'rshift'; diff --git a/src/OperationMath.php b/src/OperationMath.php index 52ffef1..857b488 100644 --- a/src/OperationMath.php +++ b/src/OperationMath.php @@ -59,4 +59,10 @@ abstract class OperationMath const LOG10 = 'log10'; const EXP = 'exp'; const EXP10 = 'exp10'; + const SINH = 'sinh'; + const COSH = 'cosh'; + const TANH = 'tanh'; + const ASINH = 'asinh'; + const ACOSH = 'acosh'; + const ATANH = 'atanh'; } diff --git a/src/OperationMath2.php b/src/OperationMath2.php index 9e86093..b1fc69e 100644 --- a/src/OperationMath2.php +++ b/src/OperationMath2.php @@ -51,4 +51,5 @@ abstract class OperationMath2 { const POW = 'pow'; const WOP = 'wop'; + const ATAN2 = 'atan2'; } diff --git a/src/RegionShrink.php b/src/RegionShrink.php new file mode 100644 index 0000000..48773d0 --- /dev/null +++ b/src/RegionShrink.php @@ -0,0 +1,58 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * The RegionShrink enum. + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +abstract class RegionShrink +{ + const MEAN = 'mean'; + const MEDIAN = 'median'; + const MODE = 'mode'; + const MAX = 'max'; + const MIN = 'min'; + const NEAREST = 'nearest'; +} diff --git a/src/SdfShape.php b/src/SdfShape.php new file mode 100644 index 0000000..cbb590e --- /dev/null +++ b/src/SdfShape.php @@ -0,0 +1,56 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * The SdfShape enum. + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/jcupitt/php-vips + */ +abstract class SdfShape +{ + const CIRCLE = 'circle'; + const BOX = 'box'; + const ROUNDED_BOX = 'rounded-box'; + const LINE = 'line'; +} diff --git a/src/Source.php b/src/Source.php new file mode 100644 index 0000000..9a028a7 --- /dev/null +++ b/src/Source.php @@ -0,0 +1,80 @@ +pointer = FFI::vips()->cast(FFI::ctypes('VipsSource'), $pointer); + parent::__construct($pointer); + } + + /** + * Make a new source from a file descriptor (a small integer). + * Make a new source that is attached to the descriptor. For example: + * $source = VipsSource::newFromDescriptor(0) + * Makes a descriptor attached to stdin. + * You can pass this source to (for example) @see Image::newFromSource() + * @throws Exception + */ + public static function newFromDescriptor(int $descriptor): self + { + $pointer = FFI::vips()->vips_source_new_from_descriptor($descriptor); + + if ($pointer === null) { + throw new Exception("can't create source from descriptor $descriptor"); + } + + return new self($pointer); + } + + /** + * Make a new source from a filename. + * Make a new source that is attached to the named file. For example: + * $source = VipsSource::newFromFile("myfile.jpg") + * You can pass this source to (for example) @see Image::newFromSource() + * @throws Exception + */ + public static function newFromFile(string $filename): self + { + $pointer = FFI::vips()->vips_source_new_from_file($filename); + + if ($pointer === null) { + throw new Exception("can't create source from filename $filename"); + } + + return new self($pointer); + } + + /** + * Make a new source from a filename. + * Make a new source that uses the provided $data. For example: + * $source = VipsSource::newFromFile(file_get_contents("myfile.jpg")) + * You can pass this source to (for example) @see Image::newFromSource() + * @throws Exception + */ + public static function newFromMemory(string $data): self + { + # we need to set the memory to a copy of the data that vips_lib + # can own and free + $n = strlen($data); + $memory = FFI::vips()->new("char[$n]", false, true); + \FFI::memcpy($memory, $data, $n); + $pointer = FFI::vips()->vips_source_new_from_memory($memory, $n); + + if ($pointer === null) { + throw new Exception("can't create source from memory"); + } + + return new self($pointer); + } +} diff --git a/src/SourceCustom.php b/src/SourceCustom.php new file mode 100644 index 0000000..20ecc7c --- /dev/null +++ b/src/SourceCustom.php @@ -0,0 +1,53 @@ +pointer = FFI::vips()->vips_source_custom_new(); + parent::__construct($this->pointer); + } + + /** + * Attach a read handler. + * + * The interface is similar to fread. The handler is given a number + * of bytes to fetch, and should return a bytes-like object containing up + * to that number of bytes. If there is no more data available, it should + * return null. + */ + public function onRead(callable $callback): void + { + $this->signalConnect('read', $callback); + } + + /** + * Attach a seek handler. + * + * The interface is the same as fseek, so the handler is passed + * parameters for $offset and $whence with the same meanings. + * However, the handler MUST return the new seek position. A simple way + * to do this is to call ftell() and return that result. + * Seek handlers are optional. If you do not set one, your source will be + * treated as unseekable and libvips will do extra caching. + * + * $whence in particular: + * - 0 => start + * - 1 => current position + * - 2 => end + */ + public function onSeek(callable $callback): void + { + $this->signalConnect('seek', $callback); + } +} diff --git a/src/SourceResource.php b/src/SourceResource.php new file mode 100644 index 0000000..9eaefdd --- /dev/null +++ b/src/SourceResource.php @@ -0,0 +1,40 @@ +resource = $resource; + parent::__construct(); + + $this->onRead(static function (int $length) use (&$resource): ?string { + return fread($resource, $length) ?: null; + }); + + if (stream_get_meta_data($resource)['seekable']) { + $this->onSeek(static function (int $offset, int $whence) use (&$resource): int { + fseek($resource, $offset, $whence); + return ftell($resource); + }); + } + } + + public function __destruct() + { + fclose($this->resource); + parent::__destruct(); + } +} diff --git a/src/Target.php b/src/Target.php new file mode 100644 index 0000000..eda298d --- /dev/null +++ b/src/Target.php @@ -0,0 +1,74 @@ +pointer = FFI::vips()->cast(FFI::ctypes('VipsTarget'), $pointer); + parent::__construct($pointer); + } + + /** + * Make a new target to write to a file descriptor (a small integer). + * Make a new target that is attached to the descriptor. For example:: + * $target = VipsTarget.newToDescriptor(1) + * Makes a descriptor attached to stdout. + * You can pass this target to (for example) @see Image::writeToTarget() + * @throws Exception + */ + public static function newToDescriptor(int $descriptor): self + { + $pointer = FFI::vips()->vips_target_new_to_descriptor($descriptor); + if ($pointer === null) { + throw new Exception("can't create output target from descriptor $descriptor"); + } + + return new self($pointer); + } + + /** + * Make a new target to write to a file name. + * Make a new target that is attached to the file name. For example:: + * $target = VipsTarget.newToFile("myfile.jpg") + * You can pass this target to (for example) @see Image::writeToTarget() + * @throws Exception + */ + public static function newToFile(string $filename): self + { + $pointer = FFI::vips()->vips_target_new_to_file($filename); + + if ($pointer === null) { + throw new Exception("can't create output target from filename $filename"); + } + + return new self($pointer); + } + + /** + * Make a new target to write to a memory buffer. + * For example:: + * $target = VipsTarget.newToMemory() + * You can pass this target to (for example) @see Image::writeToTarget() + * @throws Exception + */ + public static function newToMemory(): self + { + $pointer = FFI::vips()->vips_target_new_to_memory(); + + if ($pointer === null) { + throw new Exception("can't create output target from memory"); + } + + return new self($pointer); + } +} diff --git a/src/TargetCustom.php b/src/TargetCustom.php new file mode 100644 index 0000000..a30b677 --- /dev/null +++ b/src/TargetCustom.php @@ -0,0 +1,111 @@ +pointer = FFI::vips()->vips_target_custom_new(); + parent::__construct($this->pointer); + } + + /** + * Attach a write handler. + * + * The interface is similar to fwrite(). The handler is given a + * bytes-like object to write, and should return the number of bytes + * written. + * + * Unlike fwrite, you should return -1 on error. + * + * @throws Exception + */ + public function onWrite(callable $callback): void + { + $this->signalConnect('write', $callback); + } + + /** + * Attach a read handler. + * + * The interface is similar to fread(). The handler is given a number + * of bytes to fetch, and should return a bytes-like object containing up + * to that number of bytes. If there is no more data available, it should + * return null. + * + * Read handlers on VipsTarget are optional. If you do not set one, your + * target will be treated as unreadable and libvips will be unable to + * write some file types (just TIFF, as of the time of writing). + */ + public function onRead(callable $callback): void + { + if (FFI::atLeast(8, 13)) { + $this->signalConnect('read', $callback); + } + } + + /** + * Attach a seek handler. + * + * The interface is similar to fseek, so the handler is passed + * parameters for $offset and $whence with the same meanings. + * However, the handler MUST return the new seek position. A simple way + * to do this is to call ftell() and return that result. + * + * Seek handlers are optional. If you do not set one, your source will be + * treated as unseekable and libvips will do extra caching. + * + * $whence in particular: + * - 0 => start + * - 1 => current position + * - 2 => end + */ + public function onSeek(callable $callback): void + { + if (FFI::atLeast(8, 13)) { + $this->signalConnect('seek', $callback); + } + } + + /** + * Attach an end handler. + * + * This optional handler is called at the end of write. It should do any + * cleaning up necessary, and return 0 on success and -1 on error. + * + * Automatically falls back to onFinish if libvips <8.13. + * + * @throws Exception + */ + public function onEnd(callable $callback): void + { + if (FFI::atLeast(8, 13)) { + $this->signalConnect('end', $callback); + } else { + $this->onFinish($callback); + } + } + + /** + * Attach a finish handler. + * + * For libvips 8.13 and later, this method is deprecated in favour of + * onEnd(). + * + * @see TargetCustom::onEnd() + * @throws Exception + */ + public function onFinish(callable $callback): void + { + $this->signalConnect('finish', $callback); + } +} diff --git a/src/TargetResource.php b/src/TargetResource.php new file mode 100644 index 0000000..b8e90a6 --- /dev/null +++ b/src/TargetResource.php @@ -0,0 +1,54 @@ +resource = $resource; + parent::__construct(); + + $this->onWrite(static function (string $buffer) use (&$resource): int { + return fwrite($resource, $buffer) ?: 0; + }); + + $this->onEnd(static function () use (&$resource): void { + fclose($resource); + }); + + $meta = stream_get_meta_data($resource); + // See: https://www.php.net/manual/en/function.fopen.php + if (substr($meta['mode'], -1) === '+') { + $this->onRead(static function (int $length) use (&$resource): ?string { + return fread($resource, $length) ?: null; + }); + } + + if ($meta['seekable']) { + $this->onSeek(static function (int $offset, int $whence) use (&$resource): int { + fseek($resource, $offset, $whence); + return ftell($resource); + }); + } + } + + public function __destruct() + { + if (is_resource($this->resource)) { + fclose($this->resource); + } + parent::__destruct(); + } +} diff --git a/src/ImageType.php b/src/TextWrap.php similarity index 85% rename from src/ImageType.php rename to src/TextWrap.php index 83648f5..db6ff14 100644 --- a/src/ImageType.php +++ b/src/TextWrap.php @@ -39,7 +39,7 @@ namespace Jcupitt\Vips; /** - * The ImageType enum. + * The TextWrap enum. * @category Images * @package Jcupitt\Vips * @author John Cupitt @@ -47,15 +47,10 @@ * @license https://opensource.org/licenses/MIT MIT * @link https://github.com/jcupitt/php-vips */ -abstract class ImageType +abstract class TextWrap { - const ERROR = 'error'; + const WORD = 'word'; + const CHAR = 'char'; + const WORD_CHAR = 'word-char'; const NONE = 'none'; - const SETBUF = 'setbuf'; - const SETBUF_FOREIGN = 'setbuf-foreign'; - const OPENIN = 'openin'; - const MMAPIN = 'mmapin'; - const MMAPINRW = 'mmapinrw'; - const OPENOUT = 'openout'; - const PARTIAL = 'partial'; } diff --git a/src/Utils.php b/src/Utils.php index 0e1d20b..31e26fc 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -38,10 +38,8 @@ namespace Jcupitt\Vips; -use Psr\Log\LoggerInterface; - /** - * Various utilities. For now, just loggers. + * Various utilities. * * @category Images * @package Jcupitt\Vips @@ -60,7 +58,7 @@ class Utils * * @return void */ - public static function debugLog(string $name, array $arguments) + public static function debugLog(string $name, array $arguments): void { $logger = Config::getLogger(); if ($logger) { @@ -71,18 +69,49 @@ public static function debugLog(string $name, array $arguments) /** * Log an error message. * - * @param string $message The error message. - * @param \Exception $exception The exception. + * @param string $message The error message. + * @param \Exception|null $exception The exception, if any. * * @return void */ - public static function errorLog(string $message, \Exception $exception) + public static function errorLog(string $message, ?\Exception $exception = null): void { $logger = Config::getLogger(); if ($logger) { - $logger->error($message, ['exception' => $exception]); + $logger->error($message, $exception == null ? [] : ['exception' => $exception]); } } + + /** + * Look up the GTyoe from a type name. If the type does not exist, + * return 0. + * + * @param string $name The type name. + * + * @return int + */ + public static function typeFromName(string $name): int + { + return FFI::gobject()->g_type_from_name($name); + } + + public static function filenameGetFilename(string $name): string + { + $pointer = FFI::vips()->vips_filename_get_filename($name); + $filename = \FFI::string($pointer); + FFI::glib()->g_free($pointer); + + return $filename; + } + + public static function filenameGetOptions(string $name): string + { + $pointer = FFI::vips()->vips_filename_get_options($name); + $options = \FFI::string($pointer); + FFI::glib()->g_free($pointer); + + return $options; + } } /* diff --git a/src/VipsObject.php b/src/VipsObject.php new file mode 100644 index 0000000..e0b18a5 --- /dev/null +++ b/src/VipsObject.php @@ -0,0 +1,207 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/libvips/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * This class holds a pointer to a VipsObject (the libvips base class) and + * manages properties. + * + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/libvips/php-vips + */ +abstract class VipsObject extends GObject +{ + /** + * A pointer to the underlying VipsObject. + * + * @internal + */ + private \FFI\CData $pointer; + + /** + * A pointer to the underlying GObject. This is the same as the + * VipsObject, just cast. + * + * @internal + */ + private \FFI\CData $gObject; + + public function __construct(\FFI\CData $pointer) + { + $this->pointer = FFI::vips()->cast(FFI::ctypes("VipsObject"), $pointer); + $this->gObject = FFI::vips()->cast(FFI::ctypes("GObject"), $pointer); + + parent::__construct($pointer); + } + + // print a table of all active vipsobjects ... handy for debugging + public static function printAll(): void + { + FFI::vips()->vips_object_print_all(); + } + + public function getDescription(): string + { + return FFI::vips()->vips_object_get_description($this->pointer); + } + + // get the pspec for a property + // NULL for no such name + // very slow! avoid if possible + // FIXME add a cache for this thing, see code in pyvips + public function getPspec(string $name): ?\FFI\CData + { + $name = str_replace("-", "_", $name); + $pspec_array = FFI::gobject()->new("GParamSpec*[1]"); + $argument_class_array = FFI::vips()->new("VipsArgumentClass*[1]"); + $argument_instance_array = FFI::vips()->new("VipsArgumentInstance*[1]"); + $result = FFI::vips()->vips_object_get_argument( + $this->pointer, + $name, + $pspec_array, + $argument_class_array, + $argument_instance_array + ); + + if ($result != 0) { + $pspec = null; + } else { + /* php-ffi seems to leak if we do the obvious $pspec_array[0] to + * get the return result ... instead, we must make a new pointer + * object and copy the value ourselves + * + * the returns values from vips_object_get_argument() are static, + * so this is safe + */ + $pspec = FFI::gobject()->new("GParamSpec*"); + $to_pointer = \FFI::addr($pspec); + $from_pointer = \FFI::addr($pspec_array); + $size = \FFI::sizeof($from_pointer); + \FFI::memcpy($to_pointer, $from_pointer, $size); + } + + return $pspec; + } + + // get the type of a property from a VipsObject + // 0 if no such property + public function getType(string $name): int + { + $pspec = $this->getPspec($name); + if (\FFI::isNull($pspec)) { + # need to clear any error, this is horrible + FFI::vips()->vips_error_clear(); + return 0; + } else { + return $pspec->value_type; + } + } + + public function getBlurb(string $name): string + { + $pspec = $this->getPspec($name); + return FFI::gobject()->g_param_spec_get_blurb($pspec); + } + + public function getArgumentDescription(string $name): string + { + $pspec = $this->getPspec($name); + return FFI::gobject()->g_param_spec_get_description($pspec); + } + + /** + * @throws Exception + */ + public function get(string $name) + { + $name = str_replace("-", "_", $name); + $gvalue = new GValue(); + $gvalue->setType($this->getType($name)); + + FFI::gobject()-> + g_object_get_property($this->gObject, $name, $gvalue->pointer); + $value = $gvalue->get(); + + Utils::debugLog("get", [$name => var_export($value, true)]); + + return $value; + } + + /** + * @throws Exception + */ + public function set(string $name, $value): void + { + Utils::debugLog("set", [$name => $value]); + + $name = str_replace("-", "_", $name); + $gvalue = new GValue(); + $gvalue->setType($this->getType($name)); + $gvalue->set($value); + + FFI::gobject()-> + g_object_set_property($this->gObject, $name, $gvalue->pointer); + } + + public function setString(string $string_options): bool + { + $result = FFI::vips()-> + vips_object_set_from_string($this->pointer, $string_options); + + return $result == 0; + } + + public function unrefOutputs(): void + { + FFI::vips()->vips_object_unref_outputs($this->pointer); + } +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + * vim600: expandtab sw=4 ts=4 fdm=marker + * vim<600: expandtab sw=4 ts=4 + */ diff --git a/src/VipsOperation.php b/src/VipsOperation.php new file mode 100644 index 0000000..cf212af --- /dev/null +++ b/src/VipsOperation.php @@ -0,0 +1,386 @@ + + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/libvips/php-vips + */ + +namespace Jcupitt\Vips; + +/** + * This class holds a pointer to a VipsOperation (the libvips operation base + * class) and manages argument introspection and operation call. + * + * @category Images + * @package Jcupitt\Vips + * @author John Cupitt + * @copyright 2016 John Cupitt + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/libvips/php-vips + */ +class VipsOperation extends VipsObject +{ + /** + * A pointer to the underlying VipsOperation. This is the same as the + * GObject, just cast to VipsOperation to help FFI. + * + * @internal + */ + public \FFI\CData $pointer; + + /** + * Introspection data for this operation. + */ + public Introspect $introspect; + + public function __construct(\FFI\CData $pointer) + { + $this->pointer = FFI::vips()-> + cast(FFI::ctypes("VipsOperation"), $pointer); + + parent::__construct($pointer); + } + + /** + * @throws Exception + */ + public static function newFromName($name): VipsOperation + { + $pointer = FFI::vips()->vips_operation_new($name); + if ($pointer == null) { + throw new Exception(); + } + $operation = new VipsOperation($pointer); + + return $operation; + } + + public function setMatch($name, $match_image, $value): void + { + $flags = $this->introspect->arguments[$name]["flags"]; + $gtype = $this->introspect->arguments[$name]["type"]; + + if ($match_image != null) { + if ($gtype == FFI::gtypes("VipsImage")) { + $value = $match_image->imageize($value); + } elseif ($gtype == FFI::gtypes("VipsArrayImage") && + is_array($value)) { + $new_value = []; + foreach ($value as $x) { + $new_value[] = $match_image->imageize($x); + } + $value = $new_value; + } + } + + # MODIFY args need to be copied before they are set + if (($flags & ArgumentFlags::MODIFY) != 0) { + # logger.debug('copying MODIFY arg %s', name) + # make sure we have a unique copy + $value = $value->copyMemory(); + } + + parent::set($name, $value); + } + + private static function introspect($name): Introspect + { + static $cache = []; + + if (!array_key_exists($name, $cache)) { + $cache[$name] = new Introspect($name); + } + + return $cache[$name]; + } + + private static function findInside($predicate, $x) + { + if ($predicate($x)) { + return $x; + } + + if (is_array($x)) { + foreach ($x as $y) { + $result = self::findInside($predicate, $y); + + if ($result != null) { + return $result; + } + } + } + + return null; + } + + /** + * Is $value a VipsImage. + * + * @param mixed $value The thing to test. + * + * @return bool true if this is a ffi VipsImage*. + * + * @internal + */ + private static function isImagePointer($value): bool + { + return $value instanceof \FFI\CData && + \FFI::typeof($value) == FFI::ctypes("VipsImage"); + } + + /** + * Wrap up the result of a vips_ call ready to return it to PHP. We do + * two things: + * + * - If the array is a singleton, we strip it off. For example, many + * operations return a single result and there's no sense handling + * this as an array of values, so we transform ['out' => x] -> x. + * + * - Any VipsImage resources are rewrapped as instances of Image. + * + * @param mixed $result Wrap this up. + * + * @return mixed $result, but wrapped up as a php class. + * + * @internal + */ + private static function wrapResult($result) + { + if (!is_array($result)) { + $result = ['x' => $result]; + } + + array_walk_recursive($result, function (&$item) { + if (self::isImagePointer($item)) { + $item = new Image($item); + } + }); + + if (count($result) === 1) { + $result = array_shift($result); + } + + return $result; + } + + /** + * Call any vips operation. The final element of $arguments can be + * (but doesn't have to be) an array of options to pass to the operation. + * + * We can't have a separate arg for the options since this will be run from + * __call(), which cannot know which args are required and which are + * optional. See call() below for a version with the options broken out. + * + * @param string $operation_name The operation name. + * @param Image|null $instance The instance this operation is being invoked + * from. + * @param array $arguments An array of arguments to pass to the + * operation. + * + * @throws Exception + * + * @return mixed The result(s) of the operation. + */ + public static function callBase( + string $operation_name, + ?Image $instance, + array $arguments + ) { + Utils::debugLog($operation_name, [ + 'instance' => $instance, + 'arguments' => $arguments + ]); + + $operation = self::newFromName($operation_name); + $operation->introspect = self::introspect($operation_name); + $introspect = $operation->introspect; + + /* the first image argument is the thing we expand constants to + * match ... look inside tables for images, since we may be passing + * an array of images as a single param. + */ + if ($instance != null) { + $match_image = $instance; + } else { + $match_image = self::findInside( + fn($x) => $x instanceof Image, + $arguments + ); + } + + /* Because of the way php callStatic works, we can sometimes be given + * an instance even when no instance was given. + * + * We must loop over the required args and set them from the supplied + * args, using instance if required, and only check the nargs after + * this pass. + */ + $n_required = count($introspect->required_input); + $n_supplied = count($arguments); + $n_used = 0; + foreach ($introspect->required_input as $name) { + if ($name == $introspect->member_this) { + if (!$instance) { + $operation->unrefOutputs(); + throw new Exception("instance argument not supplied"); + } + $operation->setMatch($name, $match_image, $instance); + } elseif ($n_used < $n_supplied) { + $operation->setMatch($name, $match_image, $arguments[$n_used]); + $n_used += 1; + } else { + $operation->unrefOutputs(); + throw new Exception("$n_required arguments required, " . + "but $n_supplied supplied"); + } + } + + /* If there's one extra arg and it's an array, use it as our options. + */ + $options = []; + if ($n_supplied == $n_used + 1 && is_array($arguments[$n_used])) { + $options = array_pop($arguments); + $n_supplied -= 1; + } + + if ($n_supplied != $n_used) { + $operation->unrefOutputs(); + throw new Exception("$n_required arguments required, " . + "but $n_supplied supplied"); + } + + /* set any string options before any args so they can't be + * overridden. + */ + if (array_key_exists("string_options", $options)) { + $string_options = $options["string_options"]; + unset($options["string_options"]); + $operation->setString($string_options); + } + + /* Set optional. + */ + foreach ($options as $name => $value) { + $name = str_replace("-", "_", $name); + if (!in_array($name, $introspect->optional_input) && + !in_array($name, $introspect->optional_output)) { + $operation->unrefOutputs(); + throw new Exception("optional argument '$name' does not exist"); + } + + $operation->setMatch($name, $match_image, $value); + } + + /* Build the operation + */ + $pointer = FFI::vips()-> + vips_cache_operation_build($operation->pointer); + if ($pointer == null) { + $operation->unrefOutputs(); + throw new Exception(); + } + $operation = new VipsOperation($pointer); + $operation->introspect = $introspect; + + # TODO .. need to attach input refs to output, see _find_inside in + # pyvips + + /* Fetch required output args (and modified input args). + */ + $result = []; + foreach ($introspect->required_output as $name) { + $result[$name] = $operation->get($name); + } + + /* Any optional output args. + */ + $option_keys = array_keys($options); + foreach ($introspect->optional_output as $name) { + $name = str_replace("-", "_", $name); + if (in_array($name, $option_keys)) { + $result[$name] = $operation->get($name); + } + } + + /* Free any outputs we've not used. + */ + $operation->unrefOutputs(); + + $result = self::wrapResult($result); + + Utils::debugLog($operation_name, [ + 'result' => var_export($result, true) + ]); + + return $result; + } + + /** + * Call any vips operation, with an explicit set of options. This is more + * convenient than callBase() if you have a set of known options. + * + * @param string $name The operation name. + * @param Image|null $instance The instance this operation is being invoked + * from. + * @param array $arguments An array of arguments to pass to the + * operation. + * @param array $options An array of optional arguments to pass to + * the operation. + * + * @throws Exception + * + * @return mixed The result(s) of the operation. + */ + public static function call( + string $name, + ?Image $instance, + array $arguments, + array $options = [] + ) { + return self::callBase( + $name, + $instance, + array_merge($arguments, [$options]) + ); + } +} + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + * vim600: expandtab sw=4 ts=4 fdm=marker + * vim<600: expandtab sw=4 ts=4 + */ diff --git a/tests/CallTest.php b/tests/CallTest.php index 1406ec9..99cf97f 100644 --- a/tests/CallTest.php +++ b/tests/CallTest.php @@ -10,13 +10,27 @@ class CallTest extends TestCase public function testVipsCall() { $image = Vips\Image::newFromArray([1, 2, 3]); - $image = $image->embed(10, 20, 3000, 2000, ['extend' => Vips\Extend::COPY]); + $image = $image->embed(10, 20, 3000, 2000, [ + 'extend' => Vips\Extend::COPY + ]); $this->assertEquals($image->width, 3000); $this->assertEquals($image->height, 2000); $this->assertEquals($image->bands, 1); } + public function testVipsCallHyphen() + { + # should work with "-" as well as "_" + $image = Vips\Image::worley(64, 64, [ + "cell-size" => 8 + ]); + + $this->assertEquals($image->width, 64); + $this->assertEquals($image->height, 64); + $this->assertEquals($image->bands, 1); + } + public function testVipsCallStatic() { $image = Vips\Image::black(1, 4, ['bands' => 3]); diff --git a/tests/ConvenienceTest.php b/tests/ConvenienceTest.php index 49492ef..48c7bf7 100644 --- a/tests/ConvenienceTest.php +++ b/tests/ConvenienceTest.php @@ -17,13 +17,19 @@ class ConvenienceTest extends TestCase */ private $pixel; - protected function setUp() + protected function setUp(): void { $filename = __DIR__ . '/images/img_0076.jpg'; $this->image = Vips\Image::newFromFile($filename); $this->pixel = $this->image->getpoint(0, 0); } + protected function tearDown(): void + { + unset($this->image); + unset($this->pixel); + } + public function testVipsBandjoin() { $image = Vips\Image::newFromArray([[1, 2, 3], [4, 5, 6]]); diff --git a/tests/ExceptionTest.php b/tests/ExceptionTest.php index b38c43b..343b521 100644 --- a/tests/ExceptionTest.php +++ b/tests/ExceptionTest.php @@ -12,16 +12,15 @@ class ExceptionTest extends TestCase */ private $image; - /** - * The original value of pixel (0, 0). - */ - private $pixel; - - protected function setUp() + protected function setUp(): void { $filename = __DIR__ . '/images/img_0076.jpg'; $this->image = Vips\Image::newFromFile($filename); - $this->pixel = $this->image->getpoint(0, 0); + } + + protected function tearDown(): void + { + unset($this->image); } public function testVipsNewFromFileException() diff --git a/tests/LoggerTest.php b/tests/LoggerTest.php index ea79e67..c7f1d37 100644 --- a/tests/LoggerTest.php +++ b/tests/LoggerTest.php @@ -33,7 +33,7 @@ public function testSetLoggerCall() * * @return void */ - public function log($level, $message, array $context = array()) + public function log($level, $message, array $context = array()): void { // Do logging logic here. } diff --git a/tests/MetaTest.php b/tests/MetaTest.php index 9fd3cb5..8b8bb06 100644 --- a/tests/MetaTest.php +++ b/tests/MetaTest.php @@ -17,7 +17,7 @@ class MetaTest extends TestCase */ private $png_image; - protected function setUp() + protected function setUp(): void { $filename = __DIR__ . '/images/img_0076.jpg'; $this->image = Vips\Image::newFromFile($filename); @@ -26,6 +26,12 @@ protected function setUp() $this->png_image = Vips\Image::newFromFile($png_filename); } + protected function tearDown(): void + { + unset($this->image); + unset($this->png_image); + } + public function testVipsSetGet() { $this->image->poop = 'banana'; diff --git a/tests/NewTest.php b/tests/NewTest.php index 3a97c6b..bf72426 100644 --- a/tests/NewTest.php +++ b/tests/NewTest.php @@ -57,14 +57,6 @@ public function testVipsNewFromImage() $this->assertEquals($image2->avg(), 2); } - public function testVipsFindLoad() - { - $filename = __DIR__ . '/images/img_0076.jpg'; - $loader = Vips\Image::findLoad($filename); - - $this->assertEquals($loader, 'VipsForeignLoadJpegFile'); - } - public function testVipsNewFromBuffer() { $filename = __DIR__ . '/images/img_0076.jpg'; @@ -76,15 +68,6 @@ public function testVipsNewFromBuffer() $this->assertEquals($image->bands, 3); } - public function testVipsFindLoadBuffer() - { - $filename = __DIR__ . '/images/img_0076.jpg'; - $buffer = file_get_contents($filename); - $loader = Vips\Image::findLoadBuffer($buffer); - - $this->assertEquals($loader, 'VipsForeignLoadJpegBuffer'); - } - public function testVipsCopyMemory() { $filename = __DIR__ . '/images/img_0076.jpg'; diff --git a/tests/ShortcutTest.php b/tests/ShortcutTest.php index d66798c..4be1a46 100644 --- a/tests/ShortcutTest.php +++ b/tests/ShortcutTest.php @@ -30,13 +30,19 @@ public static function mapNumeric($value, $func) return $value; } - protected function setUp() + protected function setUp(): void { $filename = __DIR__ . '/images/img_0076.jpg'; $this->image = Vips\Image::newFromFile($filename, ['shrink' => 8]); $this->pixel = $this->image->getpoint(0, 0); } + protected function tearDown(): void + { + unset($this->image); + unset($this->pixel); + } + public function testVipsPow() { $real = self::mapNumeric($this->pixel, function ($value) { @@ -207,6 +213,8 @@ public function testOffsetSet() // replace band with image $test = $image->copy(); $test[1] = $base; + $avg = $test->avg(); + $this->assertTrue(abs($avg - 2.666) < 0.001); $this->assertEquals($test->bands, 3); $this->assertEquals($test[0]->avg(), 2); $this->assertEquals($test[1]->avg(), 2); diff --git a/tests/StreamingTest.php b/tests/StreamingTest.php new file mode 100644 index 0000000..7efcc90 --- /dev/null +++ b/tests/StreamingTest.php @@ -0,0 +1,109 @@ +markTestSkipped('libvips too old for streaming tests'); + } + } + + /** + * @throws Exception + */ + public function sourceAndTargetProvider(): Generator + { + $sources = [ + 'File' => fn() => Source::newFromFile(__DIR__ . '/images/img_0076.jpg'), + 'Memory' => fn() => Source::newFromMemory(file_get_contents(__DIR__ . '/images/img_0076.jpg')), + 'Resource' => fn() => new SourceResource(fopen(__DIR__ . '/images/img_0076.jpg', 'rb')) + ]; + $targets = [ + 'File' => fn() => Target::newToFile(tempnam(sys_get_temp_dir(), 'image')), + 'Memory' => fn() => Target::newToMemory(), + 'Resource' => fn() => new TargetResource(fopen('php://memory', 'wb+')), + 'Resource (not readable)' => fn() => new TargetResource(fopen('php://memory', 'wb')) + ]; + + foreach ($sources as $sourceName => $source) { + foreach ($targets as $targetName => $target) { + yield "$sourceName => $targetName" => [$source(), $target()]; + } + } + } + + /** + * @dataProvider sourceAndTargetProvider + */ + public function testFromSourceToTarget(Source $source, Target $target): void + { + $image = Image::newFromSource($source); + $image->writeToTarget($target, '.jpg[Q=95]'); + + // Try delete temporary file + if ($target->filename() !== null) { + @unlink($target->filename()); + } + } + + /** + * This test case is extra since it's the easiest to make sure we can "reload" the saved image + */ + public function testFromFileToFile(): void + { + $source = Source::newFromFile(__DIR__ . '/images/img_0076.jpg'); + $target = Target::newToFile(tempnam(sys_get_temp_dir(), 'image')); + $image = Image::newFromSource($source); + $image->writeToTarget($target, '.jpg[Q=95]'); + + // Make sure we can load the file + $image = Image::newFromFile($target->filename()); + $image->writeToBuffer('.jpg[Q=95]'); + unlink($target->filename()); + } + + public function testNoLeak(): void + { + $lastUsage = 0; + $leaked = false; + for ($i = 0; $i < 10; $i++) { + $filename = tempnam(sys_get_temp_dir(), 'image'); + $source = new SourceResource(fopen(__DIR__ . '/images/img_0076.jpg', 'rb')); + $target = new TargetResource(fopen($filename, 'wb+')); + $image = Image::newFromSource($source); + $image->writeToTarget($target, '.jpg[Q=95]'); + unlink($filename); + $usage = memory_get_peak_usage(true); + $diff = $usage - $lastUsage; + if ($lastUsage !== 0 && $diff > 0) { + $leaked = true; + } + $lastUsage = $usage; + } + + $this->assertFalse($leaked, 'Streaming leaked memory'); + } + + public function testFromFileToDescriptor(): void + { + // NOTE(L3tum): There is no way to get a file descriptor in PHP :) + // In theory we could use the known fds like stdin or stdout, + // but that would spam those channels full with an entire image file. + // Because of that I've chosen to omit this test. + } +} diff --git a/tests/WriteTest.php b/tests/WriteTest.php index 6fb5dc4..ed592f7 100644 --- a/tests/WriteTest.php +++ b/tests/WriteTest.php @@ -12,12 +12,12 @@ class WriteTest extends TestCase */ private $tmps; - protected function setUp() + protected function setUp(): void { $this->tmps = []; } - protected function tearDown() + protected function tearDown(): void { foreach ($this->tmps as $tmp) { @unlink($tmp); @@ -52,7 +52,7 @@ public function testVipsWriteToBuffer() $filename = __DIR__ . '/images/img_0076.jpg'; $image = Vips\Image::newFromFile($filename, ['shrink' => 8]); - $buffer1 = $image->writeToBuffer('.jpg'); + $buffer1 = $image->writeToBuffer('.jpg', ['Q' => 75]); $output_filename = $this->tmp('.jpg'); $image->writeToFile($output_filename); $buffer2 = file_get_contents($output_filename); @@ -68,6 +68,18 @@ public function testVipsWriteToMemory() $this->assertEquals($binaryStr, $memStr); } + + public function testVipsWriteToArray() + { + $filename = __DIR__ . '/images/img_0076.jpg'; + $image = Vips\Image::newFromFile($filename, ['shrink' => 8]); + $array = $image->crop(0, 0, 2, 2)->writeToArray(); + + $this->assertEquals( + $array, + [34, 39, 35, 44, 49, 45, 67, 52, 49, 120, 105, 102] + ); + } } /*