Skip to content

Commit dc46e1c

Browse files
committed
feat: Implement true streaming and signal handling
1 parent c0d4140 commit dc46e1c

10 files changed

+384
-10
lines changed

Diff for: src/Connection.php

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Jcupitt\Vips;
4+
5+
abstract class Connection extends VipsObject
6+
{
7+
/**
8+
* Get the filename associated with a connection. Return null if there is no associated file.
9+
*/
10+
public function filename(): ?string
11+
{
12+
$so = \FFI::cast(FFI::ctypes('VipsConnection'), $this->pointer);
13+
$pointer = FFI::vips()->vips_connection_filename($so);
14+
15+
if (\FFI::isNull($pointer)) {
16+
return null;
17+
}
18+
19+
return \FFI::string($pointer);
20+
}
21+
22+
/**
23+
* Make a human-readable name for a connection suitable for error messages.
24+
*/
25+
public function nick(): ?string
26+
{
27+
$so = \FFI::cast(FFI::ctypes('VipsConnection'), $this->pointer);
28+
$pointer = FFI::vips()->vips_connection_nick($so);
29+
30+
if (\FFI::isNull($pointer)) {
31+
return null;
32+
}
33+
34+
return \FFI::string($pointer);
35+
}
36+
}

Diff for: src/FFI.php

-6
Original file line numberDiff line numberDiff line change
@@ -703,12 +703,6 @@ private static function init(): void
703703
704704
VipsSourceCustom* vips_source_custom_new (void);
705705
706-
// FIXME ... these need porting to php-ffi
707-
// extern "Python" gint64 _marshal_read (VipsSource*,
708-
// void*, gint64, void*);
709-
// extern "Python" gint64 _marshal_seek (VipsSource*,
710-
// gint64, int, void*);
711-
712706
typedef struct _VipsTarget {
713707
VipsConnection parent_object;
714708

Diff for: src/GObject.php

+49-3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838

3939
namespace Jcupitt\Vips;
4040

41+
use Closure;
42+
use FFI\CData;
43+
4144
/**
4245
* This class holds a pointer to a GObject and manages object lifetime.
4346
*
@@ -55,7 +58,7 @@ abstract class GObject
5558
*
5659
* @internal
5760
*/
58-
private \FFI\CData $pointer;
61+
private CData $pointer;
5962

6063
/**
6164
* Wrap a GObject around an underlying vips resource. The GObject takes
@@ -69,7 +72,7 @@ abstract class GObject
6972
*
7073
* @internal
7174
*/
72-
public function __construct(\FFI\CData $pointer)
75+
public function __construct(CData $pointer)
7376
{
7477
$this->pointer = \FFI::cast(FFI::ctypes("GObject"), $pointer);
7578
}
@@ -94,7 +97,50 @@ public function unref(): void
9497
FFI::gobject()->g_object_unref($this->pointer);
9598
}
9699

97-
// TODO signal marshalling to go in
100+
public function signalConnect(string $name, Closure $callback): void
101+
{
102+
static $marshalers = null;
103+
104+
if ($marshalers === null) {
105+
$imageProgressCb = static function (CData $vi, CData $progress, CData $handle) {
106+
FFI::gobject()->g_object_ref($vi);
107+
$image = new Image($vi);
108+
$progress = \FFI::cast(FFI::ctypes('VipsProgress'), $progress);
109+
$handle($image, $progress);
110+
};
111+
$marshalers = ['preeval' => $imageProgressCb, 'eval' => $imageProgressCb, 'posteval' => $imageProgressCb];
112+
113+
if (FFI::atLeast(8, 9)) {
114+
$marshalers['read'] = static function (CData $gObject, CData $pointer, int $length, CData $handle): int {
115+
$buffer = \FFI::string($pointer, $length);
116+
return $handle($buffer);
117+
};
118+
$marshalers['seek'] = static function (CData $gObject, int $offset, int $whence, CData $handle): int {
119+
return $handle($offset, $whence);
120+
};
121+
$marshalers['write'] = static function (CData $gObject, CData $pointer, int $length, CData $handle): int {
122+
$buffer = \FFI::string($pointer, $length);
123+
return $handle($buffer);
124+
};
125+
$marshalers['finish'] = static function (CData $gObject, CData $handle): void {
126+
$handle();
127+
};
128+
}
129+
130+
if (FFI::atLeast(8, 13)) {
131+
$marshalers['end'] = static function (CData $gObject, CData $handle): int {
132+
return $handle();
133+
};
134+
}
135+
}
136+
137+
if (!isset($marshalers[$name])) {
138+
throw new Exception("unsupported signal $name");
139+
}
140+
141+
$go = \FFI::cast(FFI::ctypes('GObject'), $this->pointer);
142+
FFI::gobject()->g_signal_connect_data($go, $name, $marshalers[$name], $callback, null, 0);
143+
}
98144
}
99145

100146
/*

Diff for: src/VipsObject.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ abstract class VipsObject extends GObject
5656
*
5757
* @internal
5858
*/
59-
private \FFI\CData $pointer;
59+
protected \FFI\CData $pointer;
6060

6161
/**
6262
* A pointer to the underlying GObject. This is the same as the

Diff for: src/VipsSource.php

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace Jcupitt\Vips;
4+
5+
class VipsSource extends Connection
6+
{
7+
/**
8+
* Make a new source from a file descriptor (a small integer).
9+
* Make a new source that is attached to the descriptor. For example:
10+
* source = pyvips.Source.new_from_descriptor(0)
11+
* Makes a descriptor attached to stdin.
12+
* You can pass this source to (for example) :meth:`new_from_source`.
13+
* @throws Exception
14+
*/
15+
public static function newFromDescriptor(int $descriptor): self
16+
{
17+
$pointer = FFI::vips()->vips_source_new_from_descriptor($descriptor);
18+
19+
if (\FFI::isNull($pointer)) {
20+
throw new Exception("can't create source from descriptor $descriptor");
21+
}
22+
23+
return new self($pointer);
24+
}
25+
26+
/**
27+
* Make a new source from a filename.
28+
* Make a new source that is attached to the named file. For example:
29+
* source = pyvips.Source.new_from_file("myfile.jpg")
30+
* You can pass this source to (for example) :meth:`new_from_source`.
31+
* @throws Exception
32+
*/
33+
public static function newFromFile(string $filename): self
34+
{
35+
$pointer = FFI::vips()->vips_source_new_from_file($filename);
36+
37+
if (\FFI::isNull($pointer)) {
38+
throw new Exception("can't create source from filename $filename");
39+
}
40+
41+
return new self($pointer);
42+
}
43+
44+
/**
45+
* @TODO Not sure how best to implement this since PHP does not have buffers like Python
46+
* @throws Exception
47+
*/
48+
public static function newFromMemory(string $data): self
49+
{
50+
$pointer = FFI::vips()->vips_source_new_from_memory($data, strlen($data));
51+
52+
if (\FFI::isNull($pointer)) {
53+
throw new Exception("can't create source from memory");
54+
}
55+
56+
return new self($pointer);
57+
}
58+
}

Diff for: src/VipsSourceCustom.php

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Jcupitt\Vips;
4+
5+
use Closure;
6+
7+
class VipsSourceCustom extends VipsSource
8+
{
9+
public function __construct()
10+
{
11+
$source = \FFI::cast(FFI::ctypes('VipsSource'), FFI::vips()->vips_source_custom_new());
12+
parent::__construct($source);
13+
}
14+
15+
/**
16+
* Attach a read handler.
17+
* The interface is exactly as io.read() in Python. The handler is given a number
18+
* of bytes to fetch, and should return a bytes-like object containing up
19+
* to that number of bytes. If there is no more data available, it should
20+
* return None.
21+
*/
22+
public function onRead(Closure $callback): void
23+
{
24+
$this->signalConnect('read', static function (string &$buffer) use ($callback): int {
25+
$chunk = $callback(strlen($buffer));
26+
27+
if ($chunk === null) {
28+
return 0;
29+
}
30+
31+
$buffer = substr_replace($buffer, $chunk, 0);
32+
return strlen($chunk);
33+
});
34+
}
35+
36+
/**
37+
* Attach a seek handler.
38+
* The interface is the same as fseek, so the handler is passed
39+
* parameters for $offset and $whence with the same meanings.
40+
* However, the handler MUST return the new seek position. A simple way
41+
* to do this is to call ftell() and return that result.
42+
* Seek handlers are optional. If you do not set one, your source will be
43+
* treated as unseekable and libvips will do extra caching.
44+
* $whence in particular:
45+
* 0 => start
46+
* 1 => current position
47+
* 2 => end
48+
*/
49+
public function onSeek(Closure $callback): void
50+
{
51+
$this->signalConnect('seek', $callback);
52+
}
53+
}

Diff for: src/VipsSourceResource.php

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Jcupitt\Vips;
4+
5+
use Closure;
6+
7+
class VipsSourceResource extends VipsSourceCustom
8+
{
9+
/**
10+
* @var resource
11+
*/
12+
private $resource;
13+
14+
/**
15+
* The resource passed in will become "owned" by this class. On destruction of this class, the resource will be closed.
16+
*
17+
* @param resource $resource
18+
*/
19+
public function __construct($resource)
20+
{
21+
$this->resource = $resource;
22+
parent::__construct();
23+
24+
$this->onRead(static function (int $length) use ($resource): ?string {
25+
return fread($resource, $length) ?: null;
26+
});
27+
28+
if (stream_get_meta_data($resource)['seekable']) {
29+
$this->onSeek(static function (int $offset, int $whence) use ($resource): int {
30+
fseek($resource, $offset, $whence);
31+
return ftell($resource);
32+
});
33+
}
34+
}
35+
36+
public function __destruct()
37+
{
38+
fclose($this->resource);
39+
parent::__destruct(); // TODO: Change the autogenerated stub
40+
}
41+
}

Diff for: src/VipsTarget.php

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Jcupitt\Vips;
4+
5+
class VipsTarget extends Connection
6+
{
7+
public static function newToDescriptor(int $descriptor): self
8+
{
9+
$pointer = FFI::vips()->vips_target_new_to_descriptor($descriptor);
10+
if (\FFI::isNull($pointer)) {
11+
throw new Exception("can't create output target from descriptor $descriptor");
12+
}
13+
14+
return new self($pointer);
15+
}
16+
17+
public static function newToFile(string $filename): self
18+
{
19+
$pointer = FFI::vips()->vips_target_new_to_file($filename);
20+
21+
if (\FFI::isNull($pointer)) {
22+
throw new Exception("can't create output target from filename $filename");
23+
}
24+
25+
return new self($pointer);
26+
}
27+
28+
public static function newToMemory(): self
29+
{
30+
$pointer = FFI::vips()->vips_target_new_to_memory();
31+
32+
if (\FFI::isNull($pointer)) {
33+
throw new Exception("can't create output target from memory");
34+
}
35+
36+
return new self($pointer);
37+
}
38+
}

Diff for: src/VipsTargetCustom.php

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace Jcupitt\Vips;
4+
5+
use Closure;
6+
7+
class VipsTargetCustom extends VipsTarget
8+
{
9+
public function __construct()
10+
{
11+
$pointer = \FFI::cast(FFI::ctypes('VipsTarget'), FFI::vips()->vips_target_custom_new());
12+
parent::__construct($pointer);
13+
}
14+
15+
public function onWrite(Closure $callback): void
16+
{
17+
$this->signalConnect('write', $callback);
18+
}
19+
20+
public function onRead(Closure $callback): void
21+
{
22+
if (FFI::atLeast(8, 13)) {
23+
$this->signalConnect('read', static function (string &$buffer) use ($callback): int {
24+
$chunk = $callback(strlen($buffer));
25+
26+
if ($chunk === null) {
27+
return 0;
28+
}
29+
$buffer = substr_replace($buffer, $chunk, 0);
30+
return strlen($chunk);
31+
});
32+
}
33+
}
34+
35+
public function onSeek(Closure $callback): void
36+
{
37+
if (FFI::atLeast(8, 13)) {
38+
$this->signalConnect('seek', $callback);
39+
}
40+
}
41+
42+
public function onEnd(Closure $callback): void
43+
{
44+
if (FFI::atLeast(8, 13)) {
45+
$this->signalConnect('end', $callback);
46+
} else {
47+
$this->onFinish($callback);
48+
}
49+
}
50+
51+
public function onFinish(Closure $callback): void
52+
{
53+
$this->signalConnect('finish', $callback);
54+
}
55+
}

0 commit comments

Comments
 (0)