diff --git a/examples/generate_phpdoc.py b/examples/generate_phpdoc.py index 0f2c60f..632db36 100755 --- a/examples/generate_phpdoc.py +++ b/examples/generate_phpdoc.py @@ -31,7 +31,9 @@ GValue.array_int_type: 'integer[]|integer', GValue.array_double_type: 'float[]|float', GValue.array_image_type: 'Image[]|Image', - GValue.blob_type: 'string' + GValue.blob_type: 'string', + GValue.source_type: 'Source', + GValue.target_type: 'Target' } # php result type names are different, annoyingly, and very restricted @@ -48,7 +50,9 @@ GValue.array_int_type: 'array', GValue.array_double_type: 'array', GValue.array_image_type: 'array', - GValue.blob_type: 'string' + GValue.blob_type: 'string', + GValue.source_type: 'Source', + GValue.target_type: 'Target' } # values for VipsArgumentFlags diff --git a/examples/streaming-bench.php b/examples/streaming-bench.php new file mode 100644 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.php b/examples/streaming.php new file mode 100644 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/src/Connection.php b/src/Connection.php new file mode 100644 index 0000000..ea38166 --- /dev/null +++ b/src/Connection.php @@ -0,0 +1,36 @@ +pointer = \FFI::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/FFI.php b/src/FFI.php index b76c8bf..f6cb23d 100644 --- a/src/FFI.php +++ b/src/FFI.php @@ -178,6 +178,18 @@ 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) { @@ -302,6 +314,7 @@ private static function init(): void typedef int32_t gint32; typedef uint64_t guint64; typedef int64_t gint64; +typedef void* gpointer; typedef $gtype GType; @@ -366,6 +379,7 @@ private static function init(): void 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); @@ -376,6 +390,7 @@ private static function init(): void 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; @@ -429,6 +444,19 @@ private static function init(): void 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); EOS; # the whole libvips API, mostly adapted from pyvips @@ -703,12 +731,6 @@ private static function init(): void VipsSourceCustom* vips_source_custom_new (void); -// FIXME ... these need porting to php-ffi -// extern "Python" gint64 _marshal_read (VipsSource*, -// void*, gint64, void*); -// extern "Python" gint64 _marshal_seek (VipsSource*, -// gint64, int, void*); - typedef struct _VipsTarget { VipsConnection parent_object; @@ -752,11 +774,17 @@ private static function init(): void // 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*"), + "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 = [ @@ -780,6 +808,8 @@ private static function init(): void "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 diff --git a/src/ForeignDzLayout.php b/src/ForeignDzLayout.php index ae36259..021d75f 100644 --- a/src/ForeignDzLayout.php +++ b/src/ForeignDzLayout.php @@ -53,5 +53,4 @@ abstract class ForeignDzLayout const ZOOMIFY = 'zoomify'; const GOOGLE = 'google'; const IIIF = 'iiif'; - const IIIF3 = 'iiif3'; } diff --git a/src/GObject.php b/src/GObject.php index 4ebc500..0047e20 100644 --- a/src/GObject.php +++ b/src/GObject.php @@ -38,6 +38,9 @@ namespace Jcupitt\Vips; +use Closure; +use FFI\CData; + /** * This class holds a pointer to a GObject and manages object lifetime. * @@ -55,7 +58,7 @@ abstract class GObject * * @internal */ - private \FFI\CData $pointer; + private CData $pointer; /** * Wrap a GObject around an underlying vips resource. The GObject takes @@ -64,12 +67,12 @@ abstract class GObject * Don't call this yourself, users should stick to (for example) * Image::newFromFile(). * - * @param FFI\CData $pointer The underlying pointer that this + * @param CData $pointer The underlying pointer that this * object should wrap. * * @internal */ - public function __construct(\FFI\CData $pointer) + public function __construct(CData $pointer) { $this->pointer = \FFI::cast(FFI::ctypes("GObject"), $pointer); } @@ -94,7 +97,164 @@ public function unref(): void FFI::gobject()->g_object_unref($this->pointer); } - // TODO signal marshalling to go in + /** + * 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 + { + $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 === 3); + /** + * 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::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 === 4); + /* + * 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 === 4); + /* + * 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 === 4); + /* + * 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 === 2); + /** + * 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 === 2); + /** + * Signature: int(VipsTargetCustom* target, void* handle) + */ + FFI::gobject()->g_value_set_int($returnValue, $callback()); + }; + } + + return null; + default: + return null; + } + } } /* diff --git a/src/GValue.php b/src/GValue.php index 6478352..c310e6a 100644 --- a/src/GValue.php +++ b/src/GValue.php @@ -188,9 +188,7 @@ public function set($value): void # can own and free $n = strlen($value); $memory = \FFI::new("char[$n]", false, true); - for ($i = 0; $i < $n; $i++) { - $memory[$i] = $value[$i]; - } + \FFI::memcpy($memory, $value, $n); FFI::vips()-> vips_value_set_blob_free($this->pointer, $memory, $n); break; 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 bbddf07..5701de2 100644 --- a/src/Image.php +++ b/src/Image.php @@ -901,6 +901,38 @@ public function newFromImage($value): 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::cast(FFI::ctypes('VipsSource'), $source->pointer)); + } + + /** + * @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. * @@ -1040,6 +1072,28 @@ public function writeToArray(): array 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. * diff --git a/src/ImageAutodoc.php b/src/ImageAutodoc.php index 529c834..36f017b 100644 --- a/src/ImageAutodoc.php +++ b/src/ImageAutodoc.php @@ -171,11 +171,11 @@ * @throws Exception * @method static Image csvload(string $filename, array $options = []) Load csv. * @throws Exception - * @method static Image csvload_source(string $source, array $options = []) Load csv. + * @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(string $target, array $options = []) Save image to csv. + * @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 @@ -203,8 +203,6 @@ * @throws Exception * @method string dzsave_buffer(array $options = []) Save image to dz buffer. * @throws Exception - * @method void dzsave_target(string $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. @@ -229,7 +227,7 @@ * @throws Exception * @method static Image fitsload(string $filename, array $options = []) Load a FITS image. * @throws Exception - * @method static Image fitsload_source(string $source, array $options = []) Load FITS from a source. + * @method static Image fitsload_source(Source $source, array $options = []) Load FITS from a source. * @throws Exception * @method void fitssave(string $filename, array $options = []) Save image to fits file. * @throws Exception @@ -260,13 +258,7 @@ * @throws Exception * @method static Image gifload_buffer(string $buffer, array $options = []) Load GIF with libnsgif. * @throws Exception - * @method static Image gifload_source(string $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(string $target, array $options = []) Save as gif. + * @method static Image gifload_source(Source $source, array $options = []) Load gif from source. * @throws Exception * @method Image globalbalance(array $options = []) Global balance an image mosaic. * @throws Exception @@ -281,13 +273,13 @@ * @throws Exception * @method static Image heifload_buffer(string $buffer, array $options = []) Load a HEIF image. * @throws Exception - * @method static Image heifload_source(string $source, array $options = []) Load a HEIF image. + * @method static Image heifload_source(Source $source, array $options = []) Load a HEIF image. * @throws Exception * @method void heifsave(string $filename, array $options = []) Save image in HEIF format. * @throws Exception * @method string heifsave_buffer(array $options = []) Save image in HEIF format. * @throws Exception - * @method void heifsave_target(string $target, array $options = []) Save image in HEIF format. + * @method void heifsave_target(Target $target, array $options = []) Save image in HEIF format. * @throws Exception * @method Image hist_cum(array $options = []) Form cumulative histogram. * @throws Exception @@ -334,23 +326,11 @@ * @method Image join(Image $in2, string $direction, array $options = []) Join a pair of images. * @see Direction for possible values for $direction * @throws Exception - * @method static Image jp2kload(string $filename, array $options = []) Load JPEG2000 image. - * @throws Exception - * @method static Image jp2kload_buffer(string $buffer, array $options = []) Load JPEG2000 image. - * @throws Exception - * @method static Image jp2kload_source(string $source, array $options = []) Load JPEG2000 image. - * @throws Exception - * @method void jp2ksave(string $filename, array $options = []) Save image in JPEG2000 format. - * @throws Exception - * @method string jp2ksave_buffer(array $options = []) Save image in JPEG2000 format. - * @throws Exception - * @method void jp2ksave_target(string $target, array $options = []) Save image in JPEG2000 format. - * @throws Exception * @method static Image jpegload(string $filename, array $options = []) Load jpeg from file. * @throws Exception * @method static Image jpegload_buffer(string $buffer, array $options = []) Load jpeg from buffer. * @throws Exception - * @method static Image jpegload_source(string $source, array $options = []) Load image from jpeg source. + * @method static Image jpegload_source(Source $source, array $options = []) Load image from jpeg source. * @throws Exception * @method void jpegsave(string $filename, array $options = []) Save image to jpeg file. * @throws Exception @@ -358,19 +338,19 @@ * @throws Exception * @method void jpegsave_mime(array $options = []) Save image to jpeg mime. * @throws Exception - * @method void jpegsave_target(string $target, array $options = []) Save image to jpeg target. + * @method void jpegsave_target(Target $target, array $options = []) Save image to jpeg target. * @throws Exception * @method static Image jxlload(string $filename, array $options = []) Load JPEG-XL image. * @throws Exception * @method static Image jxlload_buffer(string $buffer, array $options = []) Load JPEG-XL image. * @throws Exception - * @method static Image jxlload_source(string $source, array $options = []) Load JPEG-XL image. + * @method static Image jxlload_source(Source $source, array $options = []) Load JPEG-XL image. * @throws Exception * @method void jxlsave(string $filename, array $options = []) Save image in JPEG-XL format. * @throws Exception * @method string jxlsave_buffer(array $options = []) Save image in JPEG-XL format. * @throws Exception - * @method void jxlsave_target(string $target, array $options = []) Save image in JPEG-XL format. + * @method void jxlsave_target(Target $target, array $options = []) Save image in JPEG-XL format. * @throws Exception * @method Image labelregions(array $options = []) Label regions in an image. * @throws Exception @@ -378,7 +358,7 @@ * @throws Exception * @method Image linecache(array $options = []) Cache an image as a set of lines. * @throws Exception - * @method static Image logmat(float $sigma, float $min_ampl, array $options = []) Make a Laplacian of Gaussian image. + * @method static Image logmat(float $sigma, float $min_ampl, array $options = []) Make a laplacian of gaussian image. * @throws Exception * @method static Image magickload(string $filename, array $options = []) Load file with ImageMagick. * @throws Exception @@ -429,13 +409,13 @@ * @throws Exception * @method static Image matrixload(string $filename, array $options = []) Load matrix. * @throws Exception - * @method static Image matrixload_source(string $source, array $options = []) Load matrix. + * @method static Image matrixload_source(Source $source, array $options = []) Load matrix. * @throws Exception * @method void matrixprint(array $options = []) Print matrix. * @throws Exception * @method void matrixsave(string $filename, array $options = []) Save image to matrix. * @throws Exception - * @method void matrixsave_target(string $target, array $options = []) Save image to matrix. + * @method void matrixsave_target(Target $target, array $options = []) Save image to matrix. * @throws Exception * @method float max(array $options = []) Find image maximum. * @throws Exception @@ -457,23 +437,17 @@ * @throws Exception * @method Image msb(array $options = []) Pick most-significant byte from an image. * @throws Exception - * @method static Image niftiload(string $filename, array $options = []) Load NIfTI volume. - * @throws Exception - * @method static Image niftiload_source(string $source, array $options = []) Load NIfTI volumes. - * @throws Exception - * @method void niftisave(string $filename, array $options = []) Save image to nifti file. - * @throws Exception * @method static Image openexrload(string $filename, array $options = []) Load an OpenEXR image. * @throws Exception * @method static Image openslideload(string $filename, array $options = []) Load file with OpenSlide. * @throws Exception - * @method static Image openslideload_source(string $source, array $options = []) Load source with OpenSlide. + * @method static Image openslideload_source(Source $source, array $options = []) Load source with OpenSlide. * @throws Exception * @method static Image pdfload(string $filename, array $options = []) Load PDF from file. * @throws Exception * @method static Image pdfload_buffer(string $buffer, array $options = []) Load PDF from buffer. * @throws Exception - * @method static Image pdfload_source(string $source, array $options = []) Load PDF from source. + * @method static Image pdfload_source(Source $source, array $options = []) Load PDF from source. * @throws Exception * @method integer percent(float $percent, array $options = []) Find threshold for percent of pixels. * @throws Exception @@ -485,21 +459,21 @@ * @throws Exception * @method static Image pngload_buffer(string $buffer, array $options = []) Load png from buffer. * @throws Exception - * @method static Image pngload_source(string $source, array $options = []) Load png from source. + * @method static Image pngload_source(Source $source, array $options = []) Load png from source. * @throws Exception - * @method void pngsave(string $filename, array $options = []) Save image to file as PNG. + * @method void pngsave(string $filename, array $options = []) Save image to png file. * @throws Exception - * @method string pngsave_buffer(array $options = []) Save image to buffer as PNG. + * @method string pngsave_buffer(array $options = []) Save image to png buffer. * @throws Exception - * @method void pngsave_target(string $target, array $options = []) Save image to target as PNG. + * @method void pngsave_target(Target $target, array $options = []) Save image to target as PNG. * @throws Exception * @method static Image ppmload(string $filename, array $options = []) Load ppm from file. * @throws Exception - * @method static Image ppmload_source(string $source, array $options = []) Load ppm base class. + * @method static Image ppmload_source(Source $source, array $options = []) Load ppm base class. * @throws Exception * @method void ppmsave(string $filename, array $options = []) Save image to ppm file. * @throws Exception - * @method void ppmsave_target(string $target, array $options = []) Save to ppm. + * @method void ppmsave_target(Target $target, array $options = []) Save to ppm. * @throws Exception * @method Image premultiply(array $options = []) Premultiply image alpha. * @throws Exception @@ -525,13 +499,13 @@ * @throws Exception * @method static Image radload_buffer(string $buffer, array $options = []) Load rad from buffer. * @throws Exception - * @method static Image radload_source(string $source, array $options = []) Load rad from source. + * @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 string radsave_buffer(array $options = []) Save image to Radiance buffer. * @throws Exception - * @method void radsave_target(string $target, array $options = []) Save image to Radiance target. + * @method void radsave_target(Target $target, array $options = []) Save image to Radiance target. * @throws Exception * @method Image rank(integer $width, integer $height, integer $index, array $options = []) Rank filter. * @throws Exception @@ -619,7 +593,7 @@ * @throws Exception * @method static Image svgload_buffer(string $buffer, array $options = []) Load SVG with rsvg. * @throws Exception - * @method static Image svgload_source(string $source, array $options = []) Load svg from source. + * @method static Image svgload_source(Source $source, array $options = []) Load svg from source. * @throws Exception * @method static Image switch(Image[]|Image $tests, array $options = []) Find the index of the first non-zero pixel in tests. * @throws Exception @@ -633,20 +607,18 @@ * @throws Exception * @method Image thumbnail_image(integer $width, array $options = []) Generate thumbnail from image. * @throws Exception - * @method static Image thumbnail_source(string $source, integer $width, array $options = []) Generate thumbnail from source. + * @method static Image thumbnail_source(Source $source, integer $width, array $options = []) Generate thumbnail from source. * @throws Exception * @method static Image tiffload(string $filename, array $options = []) Load tiff from file. * @throws Exception * @method static Image tiffload_buffer(string $buffer, array $options = []) Load tiff from buffer. * @throws Exception - * @method static Image tiffload_source(string $source, array $options = []) Load tiff from source. + * @method static Image tiffload_source(Source $source, array $options = []) Load tiff from source. * @throws Exception * @method void tiffsave(string $filename, array $options = []) Save image to tiff file. * @throws Exception * @method string tiffsave_buffer(array $options = []) Save image to tiff buffer. * @throws Exception - * @method void tiffsave_target(string $target, array $options = []) Save image to tiff target. - * @throws Exception * @method Image tilecache(array $options = []) Cache an image as a set of tiles. * @throws Exception * @method static Image tonelut(array $options = []) Build a look-up table. @@ -657,23 +629,23 @@ * @throws Exception * @method static Image vipsload(string $filename, array $options = []) Load vips from file. * @throws Exception - * @method static Image vipsload_source(string $source, array $options = []) Load vips from source. + * @method static Image vipsload_source(Source $source, array $options = []) Load vips from source. * @throws Exception * @method void vipssave(string $filename, array $options = []) Save image to file in vips format. * @throws Exception - * @method void vipssave_target(string $target, array $options = []) Save image to target in vips format. + * @method void vipssave_target(Target $target, array $options = []) Save image to target in vips format. * @throws Exception * @method static Image webpload(string $filename, array $options = []) Load webp from file. * @throws Exception * @method static Image webpload_buffer(string $buffer, array $options = []) Load webp from buffer. * @throws Exception - * @method static Image webpload_source(string $source, array $options = []) Load webp from source. + * @method static Image webpload_source(Source $source, array $options = []) Load webp from source. * @throws Exception * @method void webpsave(string $filename, array $options = []) Save image to webp file. * @throws Exception * @method string webpsave_buffer(array $options = []) Save image to webp buffer. * @throws Exception - * @method void webpsave_target(string $target, array $options = []) Save image to webp target. + * @method void webpsave_target(Target $target, array $options = []) Save image to webp target. * @throws Exception * @method static Image worley(integer $width, integer $height, array $options = []) Make a worley noise image. * @throws Exception diff --git a/src/OperationMath.php b/src/OperationMath.php index 857b488..52ffef1 100644 --- a/src/OperationMath.php +++ b/src/OperationMath.php @@ -59,10 +59,4 @@ 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 b1fc69e..9e86093 100644 --- a/src/OperationMath2.php +++ b/src/OperationMath2.php @@ -51,5 +51,4 @@ abstract class OperationMath2 { const POW = 'pow'; const WOP = 'wop'; - const ATAN2 = 'atan2'; } diff --git a/src/Source.php b/src/Source.php new file mode 100644 index 0000000..60975fe --- /dev/null +++ b/src/Source.php @@ -0,0 +1,80 @@ +pointer = \FFI::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::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..47d9bb4 --- /dev/null +++ b/src/SourceCustom.php @@ -0,0 +1,50 @@ +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..9b31e61 --- /dev/null +++ b/src/Target.php @@ -0,0 +1,74 @@ +pointer = \FFI::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..dedb149 --- /dev/null +++ b/src/TargetCustom.php @@ -0,0 +1,94 @@ +pointer = FFI::vips()->vips_target_custom_new(); + parent::__construct($this->pointer); + } + + /** + * Attach a write handler. + * The interface is exactly as fwrite. The handler is given a bytes-like object to write, + * and should return the number of bytes written. + * @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 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 + { + 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 @throws Exception + * @see TargetCustom::onEnd() + */ + 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/tests/StreamingTest.php b/tests/StreamingTest.php new file mode 100644 index 0000000..9d07184 --- /dev/null +++ b/tests/StreamingTest.php @@ -0,0 +1,99 @@ + 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. + } +}