diff --git a/CHANGELOG.md b/CHANGELOG.md index b798b57..9139f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,27 @@ # Changelog + All notable changes to `:vips` will be documented in this file. +## 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 @@ -18,7 +39,7 @@ All notable changes to `:vips` will be documented in this file. ### Security - Nothing -### 1.0.8 - 2020-08-29 +## 1.0.8 - 2020-08-29 ### Added - allow type names as type params to Image::setType() -- fixes issue with GType @@ -36,7 +57,7 @@ All notable changes to `:vips` will be documented in this file. ### Security - Nothing -### 1.0.7 - 2020-08-28 +## 1.0.7 - 2020-08-28 ### Added - use nullable types and void return type where possible diff --git a/README.md b/README.md index 447f4ef..592ef6b 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![Build Status](https://travis-ci.org/libvips/php-vips.svg?branch=master)](https://travis-ci.org/libvips/php-vips) -`php-vips` is a binding for [libvips](https://github.com/libvips/libvips) for -PHP 7. +`php-vips` is a binding for [libvips](https://github.com/libvips/libvips) 8.7 +and later for 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,54 +17,30 @@ 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/libvips/php-vips-ext +You need to [install the libvips +library](https://libvips.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: -You'll need to install that first. It's tested on Linux and macOS --- -Windows would need some work, but should be possible. - -See the README there, but briefly: - -1. [Install the libvips library and - headers](https://libvips.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: - - ``` - sudo apt-get install libvips-dev - ``` - - Or macOS: - - ``` - brew install vips - ``` - -2. Install the binary PHP extension. You'll need a PHP development environment - for this, since it will download and build the sources for the extension. - For example, on Debian: - - ``` - sudo apt-get install php-pear - ``` - - Then to download and build the extension it's: +``` +sudo apt-get install libvips-dev +``` - ``` - pecl install vips - ``` +Or macOS: - You may need to add `extension=vips.so` or equivalent to `php.ini`, see the - output of pecl. +``` +brew install vips +``` -3. Add vips to your `composer.json`: +Then add vips to your `composer.json`: - ``` - "require": { - "jcupitt/vips" : "1.0.7" - } - ``` +``` +"require": { + "jcupitt/vips" : "2.0.0" +} +``` ### Example @@ -156,17 +132,17 @@ introduction: https://libvips.github.io/libvips/API/current -### How it works +### TODO after merge + +- Support preloading, see https://www.php.net/manual/en/class.ffi.php + +- Rewrite the enum and doc generator in php. + +- Add source/target API -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. +- Add progress callbacks etc. -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. +- Add mutable. ### Test and install diff --git a/RELEASE-1.0.8 b/RELEASE-2.0.0 similarity index 100% rename from RELEASE-1.0.8 rename to RELEASE-2.0.0 diff --git a/composer.json b/composer.json index fa0b05c..2c61251 100644 --- a/composer.json +++ b/composer.json @@ -17,15 +17,15 @@ } ], "require": { - "php": ">=7.2", - "ext-vips": ">=0.1.2", + "php": ">=7.4", + "ext-ffi": "*", "psr/log": "^1.1.3|^2.0|^3.0" }, "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpdocumentor/phpdocumentor": "3.0.0", - "phpunit/phpunit": "^9.3", - "squizlabs/php_codesniffer": "^3.5" + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpdocumentor/shim": "^3.3", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.6" }, "autoload": { "psr-4": { @@ -39,7 +39,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "scripts": { diff --git a/examples/composer.json b/examples/composer.json new file mode 100644 index 0000000..2de9b0e --- /dev/null +++ b/examples/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "jcupitt/vips": "2.0.0" + } +} diff --git a/examples/watermark-image.php b/examples/watermark-image.php new file mode 100755 index 0000000..195343f --- /dev/null +++ b/examples/watermark-image.php @@ -0,0 +1,44 @@ +#!/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\Config::shutDown(); diff --git a/examples/watermark-text.php b/examples/watermark-text.php index 3a0377d..654cada 100755 --- a/examples/watermark-text.php +++ b/examples/watermark-text.php @@ -4,6 +4,8 @@ require __DIR__ . '/vendor/autoload.php'; use Jcupitt\Vips; +#Vips\Config::setLogger(new Vips\DebugLogger()); + if (count($argv) != 4) { echo("usage: ./watermark-text.php input output \"some text\"\n"); exit(1); @@ -30,7 +32,7 @@ $foreground = [255, 255, 255, 50]; $background = [0, 0, 255, 50]; -// and a 10-pixel marghin +// and a 10-pixel margin $margin = 10; $overlay = $text_mask->ifthenelse($foreground, $background, [ 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/Config.php b/src/Config.php index b546870..ed720df 100644 --- a/src/Config.php +++ b/src/Config.php @@ -60,6 +60,32 @@ class Config */ private static $logger; + /** + * The FFI handle we use for the libvips binary. + * + * @internal + */ + private static \FFI $ffi; + private static $ffi_inited = false; + + /** + * The library version number we detect. + * + * @internal + */ + private static int $library_major; + private static int $library_minor; + private static int $library_micro; + + /** + * Look up these once. + * + * @internal + */ + private static array $ctypes; + private static array $gtypes; + private static array $ftypes; + /** * Sets a logger. This can be handy for debugging. For example: * @@ -96,7 +122,7 @@ public static function getLogger(): ?LoggerInterface */ public static function cacheSetMax(int $value): void { - vips_cache_set_max($value); + Config::ffi()->vips_cache_set_max($value); } /** @@ -110,7 +136,7 @@ public static function cacheSetMax(int $value): void */ public static function cacheSetMaxMem(int $value): void { - vips_cache_set_max_mem($value); + Config::ffi()->vips_cache_set_max_mem($value); } /** @@ -123,7 +149,7 @@ public static function cacheSetMaxMem(int $value): void */ public static function cacheSetMaxFiles(int $value): void { - vips_cache_set_max_files($value); + Config::ffi()->vips_cache_set_max_files($value); } /** @@ -137,7 +163,7 @@ public static function cacheSetMaxFiles(int $value): void */ public static function concurrencySet(int $value): void { - vips_concurrency_set($value); + Config::ffi()->vips_concurrency_set($value); } /** @@ -148,7 +174,660 @@ public static function concurrencySet(int $value): void */ public static function version(): string { - return vips_version(); + Config::ffi(); + + return self::$library_major . "." . + self::$library_minor . ".". + self::$library_micro; + } + + /** + * Handy for debugging. + */ + public static function printAll() + { + Config::ffi()->vips_object_print_all(); + } + + public static function ffi() + { + if (!self::$ffi_inited) { + self::init(); + self::$ffi_inited = true; + } + + return self::$ffi; + } + + public static function ctypes(string $name) + { + Config::ffi(); + + return self::$ctypes[$name]; + } + + public static function gtypes(string $name) + { + Config::ffi(); + + return self::$gtypes[$name]; + } + + public static function ftypes(string $name) + { + Config::ffi(); + + return self::$ftypes[$name]; + } + + /** + * Throw a vips error as an exception. + * + * @throws Exception + * + * @return void + * + * @internal + */ + public static function error(string $message = "") + { + if ($message == "") { + $message = Config::ffi()->vips_error_buffer(); + Config::ffi()->vips_error_clear(); + } + $exception = new Exception($message); + Utils::errorLog($message, $exception); + throw $exception; + } + + /** + * Shut down libvips. Call this just before process exit. + * + * @throws Exception + * + * @return void + * + * @internal + */ + public static function shutDown() + { + Config::ffi()->vips_shutdown(); + } + + public static function filenameGetFilename($name) + { + $pointer = Config::ffi()->vips_filename_get_filename($name); + $filename = \FFI::string($pointer); + Config::ffi()->g_free($pointer); + + return $filename; + } + + public static function filenameGetOptions($name) + { + $pointer = Config::ffi()->vips_filename_get_options($name); + $options = \FFI::string($pointer); + Config::ffi()->g_free($pointer); + + return $options; + } + + public static function atLeast($need_major, $need_minor) + { + return $need_major < self::$library_major || + ($need_major == self::$library_major && + $need_minor <= self::$library_minor); + } + + private static function libraryName($name, $abi) + { + switch (PHP_OS_FAMILY) { + case "Windows": + return "$name-$abi.dll"; + + case "OSX": + return "$name.$abi.dylib"; + + default: + // most *nix + return "$name.so.$abi"; + } + } + + private static function init() + { + $library = self::libraryName("libvips", 42); + + Utils::debugLog("init", ["libray" => $library]); + + /* FIXME ... maybe display a helpful message on failure? This will + * probably be the main point of failure. + */ + $ffi = \FFI::cdef(<<vips_init(""); + if ($result != 0) { + $msg = $ffi->vips_error_buffer(); + throw new Vips\Exception("libvips error: $msg"); + } + Utils::debugLog("init", ["vips_init" => $result]); + + # get the library version number, then we can build the API + self::$library_major = $ffi->vips_version(0); + self::$library_minor = $ffi->vips_version(1); + self::$library_micro = $ffi->vips_version(2); + Utils::debugLog("init", [ + "libvips version" => [ + self::$library_major, + self::$library_minor, + self::$library_micro + ] + ]); + + if (!self::atLeast(8, 7)) { + throw new Vips\Exception("your libvips is too old -- " . + "8.7 or later required"); + } + + if (PHP_INT_SIZE != 8) { + # we could maybe fix this if it's important ... it's mostly + # necessary since GType is the size of a pointer, and there's no + # easy way to discover if php is running on a 32 or 64-bit + # systems (as far as I can see) + throw new Vips\Exception("your php only supports 32-bit ints -- " . + "64 bit ints required"); + } + + # the whole libvips API, mostly adapted from pyvips + $header = <<vips_blend_mode_get_type(); + self::$ffi->vips_interpretation_get_type(); + self::$ffi->vips_operation_flags_get_type(); + self::$ffi->vips_band_format_get_type(); + self::$ffi->vips_token_get_type(); + self::$ffi->vips_saveable_get_type(); + self::$ffi->vips_image_type_get_type(); + + // look these up in advance + self::$ctypes = [ + "GObject" => self::$ffi->type("GObject*"), + "VipsObject" => self::$ffi->type("VipsObject*"), + "VipsOperation" => self::$ffi->type("VipsOperation*"), + "VipsImage" => self::$ffi->type("VipsImage*"), + "VipsInterpolate" => self::$ffi->type("VipsInterpolate*"), + ]; + + self::$gtypes = [ + "gboolean" => self::$ffi->g_type_from_name("gboolean"), + "gint" => self::$ffi->g_type_from_name("gint"), + "gint64" => self::$ffi->g_type_from_name("gint64"), + "guint64" => self::$ffi->g_type_from_name("guint64"), + "gdouble" => self::$ffi->g_type_from_name("gdouble"), + "gchararray" => self::$ffi->g_type_from_name("gchararray"), + "VipsRefString" => self::$ffi->g_type_from_name("VipsRefString"), + + "GEnum" => self::$ffi->g_type_from_name("GEnum"), + "GFlags" => self::$ffi->g_type_from_name("GFlags"), + "VipsBandFormat" => self::$ffi->g_type_from_name("VipsBandFormat"), + "VipsBlendMode" => self::$ffi->g_type_from_name("VipsBlendMode"), + "VipsArrayInt" => self::$ffi->g_type_from_name("VipsArrayInt"), + "VipsArrayDouble" => + self::$ffi->g_type_from_name("VipsArrayDouble"), + "VipsArrayImage" => self::$ffi->g_type_from_name("VipsArrayImage"), + "VipsBlob" => self::$ffi->g_type_from_name("VipsBlob"), + + "GObject" => self::$ffi->g_type_from_name("GObject"), + "VipsImage" => self::$ffi->g_type_from_name("VipsImage"), + ]; + + // 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"]); } } diff --git a/src/GObject.php b/src/GObject.php new file mode 100644 index 0000000..74c35ef --- /dev/null +++ b/src/GObject.php @@ -0,0 +1,107 @@ + + * @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 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 \FFI\CData $pointer; + + /** + * 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 FFI\CData $pointer The underlying pointer that this + * object should wrap. + * + * @internal + */ + public function __construct($pointer) + { + $this->pointer = \FFI::cast(Config::ctypes("GObject"), $pointer); + } + + public function __destruct() + { + $this->unref(); + } + + public function __clone() + { + $this->ref(); + } + + public function ref() + { + Config::ffi()->g_object_ref($this->pointer); + } + + public function unref() + { + Config::ffi()->g_object_unref($this->pointer); + } + + // TODO signal marshalling to go in +} + +/* + * 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..c0ce60d --- /dev/null +++ b/src/GValue.php @@ -0,0 +1,342 @@ + + * @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 = Config::ffi()->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 + */ + public static function toEnum($gtype, $value) + { + if (is_string($value)) { + $enum_value = Config::ffi()-> + vips_enum_from_nick("php-vips", $gtype, $value); + if ($enum_value < 0) { + echo "gtype = " . $gtype . "\n"; + echo "value = " . $value . "\n"; + Config::error(); + } + } else { + $enum_value = $value; + } + + return $enum_value; + } + + public static function fromEnum($gtype, $value) + { + $result = Config::ffi()->vips_enum_nick($gtype, $value); + if ($result === null) { + Config::error("value not in enum"); + } + + return $result; + } + + public function __destruct() + { + Config::ffi()->g_value_unset($this->pointer); + } + + public function setType(int $gtype) + { + Config::ffi()->g_value_init($this->pointer, $gtype); + } + + public function getType(): int + { + return $this->pointer->g_type; + } + + public function set($value) + { + $gtype = $this->getType(); + + switch ($gtype) { + case Config::gtypes("gboolean"): + Config::ffi()->g_value_set_boolean($this->pointer, $value); + break; + + case Config::gtypes("gint"): + Config::ffi()->g_value_set_int($this->pointer, $value); + break; + + case Config::gtypes("gint64"): + Config::ffi()->g_value_set_int64($this->pointer, $value); + break; + + case Config::gtypes("guint64"): + Config::ffi()->g_value_set_uint64($this->pointer, $value); + break; + + case Config::gtypes("gdouble"): + Config::ffi()->g_value_set_double($this->pointer, $value); + break; + + case Config::gtypes("gchararray"): + Config::ffi()->g_value_set_string($this->pointer, $value); + break; + + case Config::gtypes("VipsRefString"): + Config::ffi()-> + vips_value_set_ref_string($this->pointer, $value); + break; + + case Config::gtypes("VipsArrayInt"): + if (!is_array($value)) { + $value = [$value]; + } + $n = count($value); + $ctype = \FFI::arrayType(\FFI::type("int"), [$n]); + $array = \FFI::new($ctype); + for ($i = 0; $i < $n; $i++) { + $array[$i] = $value[$i]; + } + Config::ffi()-> + vips_value_set_array_int($this->pointer, $array, $n); + break; + + case Config::gtypes("VipsArrayDouble"): + if (!is_array($value)) { + $value = [$value]; + } + $n = count($value); + $ctype = \FFI::arrayType(\FFI::type("double"), [$n]); + $array = \FFI::new($ctype); + for ($i = 0; $i < $n; $i++) { + $array[$i] = $value[$i]; + } + Config::ffi()-> + vips_value_set_array_double($this->pointer, $array, $n); + break; + + case Config::gtypes("VipsArrayImage"): + if (!is_array($value)) { + $value = [$value]; + } + $n = count($value); + Config::ffi()->vips_value_set_array_image($this->pointer, $n); + $array = Config::ffi()-> + 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 Config::gtypes("VipsBlob"): + # we need to set the blob to a copy of the data that vips_lib + # can own and free + $n = strlen($value); + $ctype = \FFI::arrayType(\FFI::type("char"), [$n]); + $memory = \FFI::new($ctype, false, true); + for ($i = 0; $i < $n; $i++) { + $memory[$i] = $value[$i]; + } + Config::ffi()-> + vips_value_set_blob_free($this->pointer, $memory, $n); + break; + + default: + $fundamental = Config::ffi()->g_type_fundamental($gtype); + switch ($fundamental) { + case Config::gtypes("GObject"): + Config::ffi()-> + g_value_set_object($this->pointer, $value->pointer); + break; + + case Config::gtypes("GEnum"): + Config::ffi()->g_value_set_enum( + $this->pointer, + self::toEnum($gtype, $value) + ); + break; + + case Config::gtypes("GFlags"): + /* Just set as int. + */ + Config::ffi()-> + g_value_set_flags($this->pointer, $value); + break; + + default: + $typeName = Config::ffi()->g_type_name($gtype); + throw new \BadMethodCallException( + "gtype $typeName ($gtype) not implemented" + ); + break; + } + } + } + + public function get() + { + $gtype = $this->getType(); + $result = null; + + switch ($gtype) { + case Config::gtypes("gboolean"): + $result = Config::ffi()->g_value_get_boolean($this->pointer); + break; + + case Config::gtypes("gint"): + $result = Config::ffi()->g_value_get_int($this->pointer); + break; + + case Config::gtypes("gint64"): + $result = Config::ffi()->g_value_get_int64($this->pointer); + break; + + case Config::gtypes("guint64"): + $result = Config::ffi()->g_value_get_uint64($this->pointer); + break; + + case Config::gtypes("gdouble"): + $result = Config::ffi()->g_value_get_double($this->pointer); + break; + + case Config::gtypes("gchararray"): + $result = Config::ffi()->g_value_get_string($this->pointer); + break; + + case Config::gtypes("VipsRefString"): + $p_size = Config::ffi()->new("size_t[1]"); + $result = Config::ffi()-> + 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 Config::gtypes("VipsImage"): + $pointer = Config::ffi()->g_value_get_object($this->pointer); + $result = new Image($pointer); + // get_object does not increment the ref count + $result->ref(); + break; + + case Config::gtypes("VipsArrayInt"): + $p_len = Config::ffi()->new("int[1]"); + $pointer = Config::ffi()-> + vips_value_get_array_int($this->pointer, $p_len); + $result = []; + for ($i = 0; $i < $p_len[0]; $i++) { + $result[] = $pointer[$i]; + } + break; + + case Config::gtypes("VipsArrayDouble"): + $p_len = Config::ffi()->new("int[1]"); + $pointer = Config::ffi()-> + vips_value_get_array_double($this->pointer, $p_len); + $result = []; + for ($i = 0; $i < $p_len[0]; $i++) { + $result[] = $pointer[$i]; + } + break; + + case Config::gtypes("VipsArrayImage"): + $p_len = Config::ffi()->new("int[1]"); + $pointer = Config::ffi()-> + 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 Config::gtypes("VipsBlob"): + $p_len = Config::ffi()->new("size_t[1]"); + $pointer = Config::ffi()-> + vips_value_get_blob($this->pointer, $p_len); + $result = \FFI::string($pointer, $p_len[0]); + break; + + default: + $fundamental = Config::ffi()->g_type_fundamental($gtype); + switch ($fundamental) { + case Config::gtypes("GEnum"): + $result = Config::ffi()-> + g_value_get_enum($this->pointer); + $result = self::fromEnum($gtype, $result); + break; + + case Config::gtypes("GFlags"): + /* Just get as int. + */ + $result = Config::ffi()-> + g_value_get_flags($this->pointer); + break; + + default: + $typeName = Config::ffi()->g_type_name($gtype); + throw new \BadMethodCallException( + "gtype $typeName ($gtype) not implemented" + ); + break; + } + } + + 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/Image.php b/src/Image.php index 1f364bc..c6de83a 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 required PHP 7.4 + * and later. * * # Example * @@ -213,26 +210,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 @@ -249,8 +226,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(); @@ -500,102 +477,26 @@ 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::XOR1 => 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($pointer) { - $this->image = $image; + $this->pointer = Config::ffi()-> + cast(Config::ctypes("VipsImage"), $pointer); + parent::__construct($pointer); } /** @@ -663,16 +564,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 @@ -681,124 +581,14 @@ 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 $result; - } - - /** - * Unwrap an array of stuff ready to pass down to the vips_ layer. We - * swap instances of the Image for the plain resource. - * - * @param array $result Unwrap this. - * - * @return array $result unwrapped, ready for vips. - * - * @internal - */ - private static function unwrap(array $result): array - { - array_walk_recursive($result, function (&$value) { - if ($value instanceof Image) { - $value = $value->image; - } - }); - - return $result; - } - - /** - * 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'; - } - - /** - * 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::isImage($item)) { - $item = new self($item); - } - }); - - if (count($result) === 1) { - $result = array_shift($result); - } - - return $result; - } - - /** - * Throw a vips error as an exception. - * - * @throws Exception - * - * @return void - * - * @internal - */ - private static function errorVips(): void - { - $message = vips_error_buffer(); - $exception = new Exception($message); - Utils::errorLog($message, $exception); - throw $exception; - } - - /** - * 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 - * - * @return void - * - * @internal - */ - private static function errorIsArray($result): void - { - if (!is_array($result)) { - self::errorVips(); + return $this->newFromImage($value); } } @@ -856,32 +646,37 @@ private static function runCmplx(\Closure $func, Image $image): Image } /** - * Create a new Image from a file on disc. + * 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. * - * @param string $filename The file to open. - * @param array $options Any options to pass on to the load operation. + * @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 Image A new Image. + * @return mixed The operation result. + * + * @internal */ - public static function newFromFile( - string $filename, + private function callEnum( + $other, + string $base, + string $op, array $options = [] - ): Image { - Utils::debugLog('newFromFile', [ - 'instance' => null, - 'arguments' => [$filename, $options] - ]); - - $options = self::unwrap($options); - $result = vips_image_new_from_file($filename, $options); - self::errorIsArray($result); - $result = self::wrapResult($result); - - Utils::debugLog('newFromFile', ['result' => $result]); - - return $result; + ) { + if (self::isImageish($other)) { + return VipsOperation::call($base, $this, [$other, $op], $options); + } else { + return VipsOperation::call( + $base . '_const', + $this, + [$op, $other], + $options + ); + } } /** @@ -895,61 +690,40 @@ public static function newFromFile( */ public static function findLoad(string $filename): ?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]]); + $result = Config::ffi()->vips_foreign_find_load($filename); return $result; } /** - * Create a new Image from a compressed image held as a string. + * Create a new Image from a file on disc. * - * @param string $buffer The formatted image to open. - * @param string $option_string Any text-style options to pass to the - * selected loader. - * @param array $options Any options to pass on to the load operation. + * @param string $filename The file to open. + * @param array $options Any options to pass on to the load operation. * * @throws Exception * * @return Image A new Image. */ - public static function newFromBuffer( - string $buffer, - string $option_string = '', + public static function newFromFile( + string $name, array $options = [] ): Image { - Utils::debugLog('newFromBuffer', [ - 'instance' => null, - 'arguments' => [$buffer, $option_string, $options] - ]); + $filename = Config::filenameGetFilename($name); + $string_options = Config::filenameGetOptions($name); - $options = self::unwrap($options); - $result = vips_image_new_from_buffer($buffer, $option_string, $options); - self::errorIsArray($result); - $result = self::wrapResult($result); + $loader = self::findLoad($filename); + if ($loader == null) { + Config::error(); + } + + if (strlen($string_options) != 0) { + $options = array_merge([ + "string_options" => $string_options, + ], $options); + } - Utils::debugLog('newFromBuffer', ['result' => $result]); + $result = VipsOperation::call($loader, null, [$filename], $options); return $result; } @@ -965,30 +739,41 @@ public static function newFromBuffer( */ public static function findLoadBuffer(string $buffer): ?string { - Utils::debugLog('findLoadBuffer', [ - 'instance' => null, - 'arguments' => [$buffer] - ]); + $result = Config::ffi()-> + vips_foreign_find_load_buffer($buffer, strlen($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) { - } + return $result; + } + + /** + * Create a new Image from a compressed image held as a string. + * + * @param string $buffer The formatted image to open. + * @param string $string_options Any text-style options to pass to the + * selected loader. + * @param array $options Options to pass on to the load operation. + * + * @throws Exception + * + * @return Image A new Image. + */ + public static function newFromBuffer( + string $buffer, + string $string_options = '', + array $options = [] + ): Image { + $loader = self::findLoadBuffer($buffer); + if ($loader == null) { + Config::error(); + } + + if (strlen($string_options) != 0) { + $options = array_merge([ + "string_options" => $string_options, + ], $options); } - Utils::debugLog('findLoadBuffer', ['result' => [$result]]); + $result = VipsOperation::call($loader, null, [$buffer], $options); return $result; } @@ -1013,18 +798,31 @@ 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; + $ctype = \FFI::arrayType(\FFI::type("double"), [$n]); + $a = \FFI::new($ctype, true, true); + for ($y = 0; $y < $height; $y++) { + for ($x = 0; $x < $width; $x++) { + $a[$x + $y * $width] = $array[$y][$x]; + } + } + + $pointer = Config::ffi()-> + vips_image_new_matrix_from_array($width, $height, $a, $n); + if ($pointer == null) { + Config::error(); } - $result = self::wrapResult($result); + $result = new Image($pointer); - Utils::debugLog('newFromArray', ['result' => $result]); + $result->setType(Config::gtypes("gdouble"), 'scale', $scale); + $result->setType(Config::gtypes("gdouble"), 'offset', $offset); return $result; } @@ -1032,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. @@ -1043,51 +841,41 @@ public static function newFromArray( * @return Image A new Image. */ public static function newFromMemory( - string $data, + mixed $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 = Config::ffi()->vips_image_new_from_memory_copy( + $data, + strlen($data), + $width, + $height, + $bands, + $format + ); + if ($pointer == null) { + Config::error(); } - $result = self::wrapResult($result); - Utils::debugLog('newFromMemory', ['result' => $result]); + $result = new Image($pointer); return $result; } /** - * 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. + * Deprecated thing to make an interpolator. * - * @return resource|null The interpolator, or null on error. + * See Interpolator::newFromName() for the new thing. */ public static function newInterpolator(string $name) { - 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); } /** @@ -1108,12 +896,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, @@ -1129,8 +912,6 @@ public function newFromImage($value): Image 'yoffset' => $this->yoffset ]); - Utils::debugLog('newFromImage', ['result' => $image]); - return $image; } @@ -1145,17 +926,26 @@ public function newFromImage($value): Image * * @return void */ - public function writeToFile(string $filename, array $options = []): void + public function writeToFile(string $name, array $options = []): void { - Utils::debugLog('writeToFile', [ - 'instance' => $this, - 'arguments' => [$filename, $options] - ]); + $filename = Config::filenameGetFilename($name); + $string_options = Config::filenameGetOptions($name); + + $saver = Config::ffi()->vips_foreign_find_save($filename); + if ($saver == "") { + Config::error(); + } + + 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(); + Config::error(); } } @@ -1172,19 +962,21 @@ public function writeToFile(string $filename, array $options = []): void */ public function writeToBuffer(string $suffix, array $options = []): string { - Utils::debugLog('writeToBuffer', [ - 'instance' => $this, - 'arguments' => [$suffix, $options] - ]); + $filename = Config::filenameGetFilename($suffix); + $string_options = Config::filenameGetOptions($suffix); - $options = self::unwrap($options); - $result = vips_image_write_to_buffer($this->image, $suffix, $options); - if ($result === -1) { - self::errorVips(); + $saver = Config::ffi()->vips_foreign_find_save_buffer($filename); + if ($saver == "") { + Config::error(); + } + + if (strlen($string_options) != 0) { + $options = array_merge([ + "string_options" => $string_options, + ], $options); } - $result = self::wrapResult($result); - Utils::debugLog('writeToBuffer', ['result' => $result]); + $result = VipsOperation::call($saver, $this, [], $options); return $result; } @@ -1198,17 +990,19 @@ public function writeToBuffer(string $suffix, array $options = []): string */ public function writeToMemory(): string { - Utils::debugLog('writeToMemory', [ - 'instance' => $this, - 'arguments' => [] - ]); + $ctype = \FFI::arrayType(\FFI::type("size_t"), [1]); + $p_size = \FFI::new($ctype); - $result = vips_image_write_to_memory($this->image); - if ($result === -1) { - self::errorVips(); + $pointer = Config::ffi()-> + vips_image_write_to_memory($this->pointer, $p_size); + if ($pointer == null) { + Config::error(); } - Utils::debugLog('writeToMemory', ['result' => $result]); + // string() takes a copy + $result = \FFI::string($pointer, $p_size[0]); + + Config::ffi()->g_free($pointer); return $result; } @@ -1238,17 +1032,29 @@ public function writeToMemory(): string */ public function writeToArray(): array { - Utils::debugLog('writeToArray', [ - 'instance' => $this, - 'arguments' => [] - ]); + $ctype = \FFI::arrayType(\FFI::type("size_t"), [1]); + $p_size = \FFI::new($ctype); - $result = vips_image_write_to_array($this->image); - if ($result === -1) { - self::errorVips(); + $pointer = Config::ffi()-> + vips_image_write_to_memory($this->pointer, $p_size); + if ($pointer == null) { + Config::error(); } - Utils::debugLog('writeToArray', ['result' => $result]); + // wrap pointer up as a C array of the right type + $n = $this->width * $this->height * $this->bands; + $type_name = Config::ftypes($this->format); + $ctype = \FFI::arrayType(\FFI::type($type_name), [$n]); + $array = \FFI::cast($ctype, $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 + Config::ffi()->g_free($pointer); return $result; } @@ -1269,18 +1075,11 @@ public function writeToArray(): array */ public function copyMemory(): Image { - Utils::debugLog('copyMemory', [ - 'instance' => $this, - 'arguments' => [] - ]); - - $result = vips_image_copy_memory($this->image); - if ($result === -1) { - self::errorVips(); + $pointer = Config::ffi()->vips_image_copy_memory($this->pointer); + if ($pointer == null) { + Config::error(); } - $result = self::wrapResult($result); - - Utils::debugLog('copyMemory', ['result' => $result]); + $result = new Image($pointer); return $result; } @@ -1296,9 +1095,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); } /** @@ -1311,7 +1108,7 @@ public function __get(string $name) */ public function __set(string $name, $value): void { - vips_image_set($this->image, $name, $value); + $this->set($name, $value); } /** @@ -1323,7 +1120,7 @@ public function __set(string $name, $value): void */ public function __isset(string $name): bool { - return $this->typeof($name) !== 0; + return $this->getType($name) != 0; } /** @@ -1332,7 +1129,7 @@ public function __isset(string $name): bool * 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. @@ -1343,9 +1140,13 @@ public function __isset(string $name): bool */ public function get(string $name) { - $result = vips_image_get($this->image, $name); - self::errorIsArray($result); - return self::wrapResult($result); + $gvalue = new GValue(); + if (Config::ffi()-> + vips_image_get($this->pointer, $name, $gvalue->pointer) != 0) { + Config::error(); + } + + return $gvalue->get(); } /** @@ -1357,9 +1158,21 @@ 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 Config::ffi()->vips_image_get_typeof($this->pointer, $name); + } + + /** + * A deprecated synonym for getType(). + * + * @param string $name The property name. + * + * @return integer + */ + public function typeOf(string $name): int + { + return $this->getType($name); } /** @@ -1377,10 +1190,35 @@ public function typeof(string $name): int */ public function set(string $name, $value): void { - $result = vips_image_set($this->image, $name, $value); - 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 = Config::gtypes("VipsArrayInt"); + } elseif (is_float($value[0])) { + $gtype = Config::gtypes("VipsArrayDouble"); + } else { + $gtype = Config::gtypes("VipsArrayImage"); + } + } elseif (is_int($value)) { + $gtype = Config::gtypes("gint"); + } elseif (is_float($value)) { + $gtype = Config::gtypes("gdouble"); + } elseif (is_string($value)) { + $gtype = Config::gtypes("VipsRefString"); + } else { + $gtype = Config::gtypes("VipsImage"); + } } + + $gvalue->setType($gtype); + $gvalue->set($value); + + Config::ffi()->vips_image_set($this->pointer, $name, $gvalue->pointer); } /** @@ -1402,10 +1240,10 @@ public function set(string $name, $value): void */ public function setType($type, string $name, $value): void { - $result = vips_image_set_type($this->image, $type, $name, $value); - if ($result === -1) { - self::errorVips(); - } + $gvalue = new GValue(); + $gvalue->setType($type); + $gvalue->set($value); + Config::ffi()->vips_image_set($this->pointer, $name, $gvalue->pointer); } /** @@ -1419,9 +1257,8 @@ public function setType($type, string $name, $value): void */ public function remove(string $name): void { - $result = vips_image_remove($this->image, $name); - if ($result === -1) { - self::errorVips(); + if (!Config::ffi()->vips_image_remove($this->pointer, $name)) { + Config::error(); } } @@ -1443,110 +1280,6 @@ public function __toString() return json_encode($array); } - /** - * 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 $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 $name, - ?Image $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; - } - - /** - * 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 = [] - ) { - /* - 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])); - } - - /** - * 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. - * - * @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 mixed The operation result. - * - * @internal - */ - 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); - } - } - /** * Call any vips operation as an instance method. * @@ -1559,7 +1292,7 @@ private function callEnum( */ public function __call(string $name, array $arguments) { - return self::callBase($name, $this, $arguments); + return VipsOperation::callBase($name, $this, $arguments); } /** @@ -1574,7 +1307,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); } /** @@ -1616,7 +1349,8 @@ public function offsetExists($offset): bool */ public function offsetGet($offset): ?Image { - return $this->offsetExists($offset) ? $this->extract_band($offset) : null; + return $this->offsetExists($offset) ? + $this->extract_band($offset) : null; } /** @@ -1651,7 +1385,8 @@ public function offsetSet($offset, $value): void } 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 @@ -1662,7 +1397,7 @@ public function offsetSet($offset, $value): void // 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 = []; @@ -1675,7 +1410,14 @@ public function offsetSet($offset, $value): void } $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; } /** @@ -1693,7 +1435,8 @@ 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 = []; @@ -1709,9 +1452,14 @@ public function offsetUnset($offset): void $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; } } } @@ -1729,7 +1477,7 @@ public function offsetUnset($offset): void 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); } @@ -1748,7 +1496,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; @@ -1770,7 +1518,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); } @@ -1789,7 +1537,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; @@ -1811,9 +1559,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 + ); } } @@ -1829,7 +1582,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); } /** @@ -1844,7 +1597,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); } /** @@ -1859,7 +1612,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); } /** @@ -1874,7 +1627,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 + ); } /** @@ -1891,7 +1649,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); } /** @@ -1907,7 +1665,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); } /** @@ -1922,7 +1680,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 + ); } /** @@ -1937,7 +1700,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 + ); } /** @@ -1952,7 +1720,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 + ); } /** @@ -1967,7 +1740,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 + ); } /** @@ -1982,7 +1760,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 + ); } /** @@ -1997,7 +1780,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 + ); } /** @@ -2012,7 +1800,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 + ); } /** @@ -2048,9 +1841,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)], @@ -2107,7 +1905,7 @@ public function bandrank($other, array $options = []): Image $other = (array) $other; } - return self::call('bandrank', $this, $other, $options); + return VipsOperation::call('bandrank', $this, $other, $options); } /** @@ -2130,16 +1928,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(Config::gtypes("VipsBlendMode"), $x); }, $mode); - return self::call( + return VipsOperation::call( 'composite', null, [array_merge([$this], $other), $mode], @@ -2199,7 +1999,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) { @@ -2209,14 +2008,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 + ); } /** diff --git a/src/ImageAutodoc.php b/src/ImageAutodoc.php index e917baf..f9537ec 100644 --- a/src/ImageAutodoc.php +++ b/src/ImageAutodoc.php @@ -697,6 +697,6 @@ * @property float $yres Vertical resolution in pixels/mm * @property string $filename Image filename */ -abstract class ImageAutodoc +abstract class ImageAutodoc extends VipsObject { } diff --git a/src/Interpolate.php b/src/Interpolate.php new file mode 100644 index 0000000..c70b327 --- /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($pointer) + { + $this->pointer = Config::ffi()-> + cast(Config::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 resource|null The interpolator, or null on error. + */ + public static function newFromName($name) + { + $pointer = Config::ffi()->vips_interpolate_new($name); + if ($pointer == null) { + Config::error(); + } + + 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..b4aca43 --- /dev/null +++ b/src/Introspect.php @@ -0,0 +1,252 @@ + + * @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 (eg. "add two images"). + */ + public string $description; + + /** + * The operation flags (eg. 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; + + public function __construct($name) + { + $this->name = $name; + + $operation = VipsOperation::newFromName($name); + + $this->description = $operation->getDescription(); + $flags = Config::ffi()->vips_operation_get_flags($operation->pointer); + + $p_names = Config::ffi()->new("char**[1]"); + $p_flags = Config::ffi()->new("int*[1]"); + $p_n_args = Config::ffi()->new("int[1]"); + $result = Config::ffi()->vips_object_get_args( + \FFI::cast(Config::ctypes("VipsObject"), $operation->pointer), + $p_names, + $p_flags, + $p_n_args + ); + if ($result != 0) { + error(); + } + $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) { + # libvips uses '-' to separate parts of arg names, but we + # need '_' for php + $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"]; + $blurb = $details["blurb"]; + $type = $details["type"]; + $typeName = Config::ffi()->g_type_name($type); + + 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 == Config::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($name, ['introspect' => strval($this)]); + } + + public function __toString() + { + $result = ""; + + $result .= "$this->name:\n"; + + foreach ($this->arguments as $name => $details) { + $flags = $details["flags"]; + $blurb = $details["blurb"]; + $type = $details["type"]; + $typeName = Config::ffi()->g_type_name($type); + + $result .= " $name:\n"; + + $result .= " flags: $flags\n"; + foreach (ArgumentFlags::NAMES as $name => $flag) { + if ($flags & $flag) { + $result .= " $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/Utils.php b/src/Utils.php index 2f4fe53..d5ee068 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -92,7 +92,7 @@ public static function errorLog(string $message, \Exception $exception): void */ public static function typeFromName(string $name): int { - return vips_type_from_name($name); + return Config::ffi()->g_type_from_name($name); } } diff --git a/src/VipsObject.php b/src/VipsObject.php new file mode 100644 index 0000000..2a027cf --- /dev/null +++ b/src/VipsObject.php @@ -0,0 +1,187 @@ + + * @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($pointer) + { + $this->pointer = Config::ffi()-> + cast(Config::ctypes("VipsObject"), $pointer); + $this->gObject = Config::ffi()-> + cast(Config::ctypes("GObject"), $pointer); + + parent::__construct($pointer); + } + + // print a table of all active vipsobjects ... handy for debugging + public static function printAll() + { + Config::ffi()->vips_object_print_all(); + } + + public function getDescription() + { + return Config::ffi()->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 + public function getPspec(string $name) + { + $pspec = Config::ffi()->new("GParamSpec*[1]"); + $argument_class = Config::ffi()->new("VipsArgumentClass*[1]"); + $argument_instance = Config::ffi()->new("VipsArgumentInstance*[1]"); + $result = Config::ffi()->vips_object_get_argument( + $this->pointer, + $name, + $pspec, + $argument_class, + $argument_instance + ); + + if ($result != 0) { + return null; + } else { + return $pspec[0]; + } + } + + // get the type of a property from a VipsObject + // 0 if no such property + public function getType(string $name) + { + $pspec = $this->getPspec($name); + if (\FFI::isNull($pspec)) { + # need to clear any error, this is horrible + Config::ffi()->vips_error_clear(); + return 0; + } else { + return $pspec->value_type; + } + } + + public function getBlurb(string $name): string + { + $pspec = $this->getPspec($name); + return Config::ffi()->g_param_spec_get_blurb($pspec); + } + + public function getArgumentDescription(string $name): string + { + $pspec = $this->getPspec($name); + return Config::ffi()->g_param_spec_get_description($pspec); + } + + public function get(string $name) + { + $gvalue = new GValue(); + $gvalue->setType($this->getType($name)); + + Config::ffi()-> + g_object_get_property($this->gObject, $name, $gvalue->pointer); + $value = $gvalue->get(); + + Utils::debugLog("get", [$name => var_export($value, true)]); + + return $value; + } + + public function set(string $name, $value) + { + Utils::debugLog("set", [$name => $value]); + + $gvalue = new GValue(); + $gvalue->setType($this->getType($name)); + $gvalue->set($value); + + Config::ffi()-> + g_object_set_property($this->gObject, $name, $gvalue->pointer); + } + + public function setString(string $string_options) + { + $result = Config::ffi()-> + vips_object_set_from_string($this->pointer, $string_options); + + return $result == 0; + } + + public function unrefOutputs() + { + Config::ffi()->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..606792d --- /dev/null +++ b/src/VipsOperation.php @@ -0,0 +1,413 @@ + + * @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($pointer) + { + $this->pointer = Config::ffi()-> + cast(Config::ctypes("VipsOperation"), $pointer); + + parent::__construct($pointer); + } + + public static function newFromName($name) + { + $pointer = Config::ffi()->vips_operation_new($name); + if ($pointer == null) { + Config::error(); + } + + return new VipsOperation($pointer); + } + + public function setMatch($name, $match_image, $value) + { + $flags = $this->introspect->arguments[$name]["flags"]; + $gtype = $this->introspect->arguments[$name]["type"]; + + if ($match_image != null) { + if ($gtype == Config::gtypes("VipsImage")) { + $value = $match_image->imageize($value); + } elseif ($gtype == Config::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; + } + + /** + * Unwrap an array of stuff ready to pass down to the vips_ layer. We + * swap instances of Image for the ffi pointer. + * + * @param array $result Unwrap this. + * + * @return array $result unwrapped, ready for vips. + * + * @internal + */ + private static function unwrap(array $result): array + { + array_walk_recursive($result, function (&$value) { + if ($value instanceof Image) { + $value = $value->image; + } + }); + + return $result; + } + + /** + * 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) == Config::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; + } + + /** + * 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 + * + * @return void + * + * @internal + */ + private static function errorIsArray($result): void + { + if (!is_array($result)) { + Config::error(); + } + } + + /** + * 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); + + /* 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($operation->introspect->required_input); + $n_supplied = count($arguments); + $used_instance = false; + $n_used = 0; + foreach ($operation->introspect->required_input as $name) { + if ($name == $operation->introspect->member_this) { + if (!$instance) { + $operation->unrefOutputs(); + Config::error("instance argument not supplied"); + } + $operation->setMatch($name, $match_image, $instance); + $used_instance = true; + } elseif ($n_used < $n_supplied) { + $operation->setMatch($name, $match_image, $arguments[$n_used]); + $n_used += 1; + } else { + $operation->unrefOutputs(); + Config::error("$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(); + Config::error("$n_required arguments required, " . + "but $n_supplied supplied"); + } + + /* Set optional. + */ + foreach ($options as $name => $value) { + if (!in_array($name, $operation->introspect->optional_input) && + !in_array($name, $operation->introspect->optional_output)) { + $operation->unrefOutputs(); + Config::error("optional argument '$name' does not exist"); + } + + $operation->setMatch($name, $match_image, $value); + } + + /* Build the operation + */ + $pointer = Config::ffi()-> + vips_cache_operation_build($operation->pointer); + if ($pointer == null) { + $operation->unrefOutputs(); + Config::error(); + } + $operation = new VipsOperation($pointer); + $operation->introspect = self::introspect($operation_name); + + # TODO .. need to attach input refs to output, see _find_inside in + # pyvips + + /* Fetch required output args (and modified input args). + */ + $result = []; + foreach ($operation->introspect->required_output as $name) { + $result[$name] = $operation->get($name); + } + + /* Any optional output args. + */ + $option_keys = array_keys($options); + foreach ($operation->introspect->optional_output as $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($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/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 da251a2..a4cc80c 100644 --- a/tests/ShortcutTest.php +++ b/tests/ShortcutTest.php @@ -207,6 +207,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/WriteTest.php b/tests/WriteTest.php index cc27b81..ed592f7 100644 --- a/tests/WriteTest.php +++ b/tests/WriteTest.php @@ -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] + ); + } } /*