Skip to content

Commit 780d0dd

Browse files
committed
add extended custom target
now supports read and seek handlers on targets on_finish renamed as on_end
1 parent 0da5d23 commit 780d0dd

File tree

6 files changed

+201
-49
lines changed

6 files changed

+201
-49
lines changed

Diff for: CHANGELOG.rst

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# master
2+
3+
* add seek and end handlers for TargetCustom [jcupitt]
24

35
## Version 2.2.0 (released 18 Apr 2022)
46

Diff for: examples/connection.py

+59-32
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,63 @@
33
import sys
44
import pyvips
55

6-
input_file = open(sys.argv[1], "rb")
7-
8-
9-
def read_handler(size):
10-
return input_file.read(size)
11-
12-
13-
def seek_handler(offset, whence):
14-
input_file.seek(offset, whence)
15-
return input_file.tell()
16-
17-
18-
source = pyvips.SourceCustom()
19-
source.on_read(read_handler)
20-
source.on_seek(seek_handler)
21-
22-
output_file = open(sys.argv[2], "wb")
23-
24-
25-
def write_handler(chunk):
26-
return output_file.write(chunk)
27-
28-
29-
def finish_handler():
30-
output_file.close()
31-
32-
33-
target = pyvips.TargetCustom()
34-
target.on_write(write_handler)
35-
target.on_finish(finish_handler)
36-
6+
if len(sys.argv) != 4:
7+
print(f"usage: {sys.argv[0]} IN-FILE OUT-FILE FORMAT")
8+
print(f" eg.: {sys.argv[0]} ~/pics/k2.jpg x .tif[tile]")
9+
sys.exit(1)
10+
11+
def source_custom(filename):
12+
input_file = open(sys.argv[1], "rb")
13+
14+
def read_handler(size):
15+
return input_file.read(size)
16+
17+
# seek is optional, but may improve performance by reducing buffering
18+
def seek_handler(offset, whence):
19+
input_file.seek(offset, whence)
20+
return input_file.tell()
21+
22+
source = pyvips.SourceCustom()
23+
source.on_read(read_handler)
24+
source.on_seek(seek_handler)
25+
26+
return source
27+
28+
def target_custom(filename):
29+
# w+ means read and write ... we need to be able to read from our output
30+
# stream for TIFF write
31+
output_file = open(sys.argv[2], "w+b")
32+
33+
def write_handler(chunk):
34+
return output_file.write(chunk)
35+
36+
# read and seek are optional and only needed for formats like TIFF
37+
def read_handler(size):
38+
return output_file.read(size)
39+
40+
def seek_handler(offset, whence):
41+
output_file.seek(offset, whence)
42+
return output_file.tell()
43+
44+
def end_handler():
45+
# you can't throw exceptions over on_ handlers, you must return an error
46+
# code
47+
try:
48+
output_file.close()
49+
except IOError:
50+
return -1
51+
else:
52+
return 0
53+
54+
target = pyvips.TargetCustom()
55+
target.on_write(write_handler)
56+
target.on_read(read_handler)
57+
target.on_seek(seek_handler)
58+
target.on_end(end_handler)
59+
60+
return target
61+
62+
source = source_custom(sys.argv[1])
63+
target = target_custom(sys.argv[2])
3764
image = pyvips.Image.new_from_source(source, '', access='sequential')
38-
image.write_to_target(target, '.png')
65+
image.write_to_target(target, sys.argv[3])

Diff for: pyvips/gobject.py

+24-10
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ def _marshal_image_progress(vi, pointer, handle):
4242
if at_least_libvips(8, 9):
4343
if pyvips.API_mode:
4444
@ffi.def_extern()
45-
def _marshal_read(source_custom, pointer, length, handle):
45+
def _marshal_read(gobject, pointer, length, handle):
4646
buf = ffi.buffer(pointer, length)
4747
callback = ffi.from_handle(handle)
4848
return callback(buf)
4949
_marshal_read_cb = ffi.cast('GCallback', gobject_lib._marshal_read)
5050
else:
5151
@ffi.callback('gint64(VipsSourceCustom*, void*, gint64, void*)')
52-
def _marshal_read(source_custom, pointer, length, handle):
52+
def _marshal_read(gobject, pointer, length, handle):
5353
buf = ffi.buffer(pointer, length)
5454
callback = ffi.from_handle(handle)
5555
return callback(buf)
@@ -58,30 +58,29 @@ def _marshal_read(source_custom, pointer, length, handle):
5858

5959
if pyvips.API_mode:
6060
@ffi.def_extern()
61-
def _marshal_seek(source_custom, offset, whence, handle):
61+
def _marshal_seek(gobject, offset, whence, handle):
6262
callback = ffi.from_handle(handle)
6363
return callback(offset, whence)
6464
_marshal_seek_cb = \
6565
ffi.cast('GCallback', gobject_lib._marshal_seek)
6666
else:
6767
@ffi.callback('gint64(VipsSourceCustom*, gint64, int, void*)')
68-
def _marshal_seek(source_custom, offset, whence, handle):
68+
def _marshal_seek(gobject, offset, whence, handle):
6969
callback = ffi.from_handle(handle)
7070
return callback(offset, whence)
7171
_marshal_seek_cb = ffi.cast('GCallback', _marshal_seek)
7272
_marshalers['seek'] = _marshal_seek_cb
7373

7474
if pyvips.API_mode:
7575
@ffi.def_extern()
76-
def _marshal_write(source_custom, pointer, length, handle):
76+
def _marshal_write(gobject, pointer, length, handle):
7777
buf = ffi.buffer(pointer, length)
7878
callback = ffi.from_handle(handle)
79-
result = callback(buf)
80-
return result
79+
return callback(buf)
8180
_marshal_write_cb = ffi.cast('GCallback', gobject_lib._marshal_write)
8281
else:
8382
@ffi.callback('gint64(VipsTargetCustom*, void*, gint64, void*)')
84-
def _marshal_write(source_custom, pointer, length, handle):
83+
def _marshal_write(gobject, pointer, length, handle):
8584
buf = ffi.buffer(pointer, length)
8685
callback = ffi.from_handle(handle)
8786
return callback(buf)
@@ -90,18 +89,33 @@ def _marshal_write(source_custom, pointer, length, handle):
9089

9190
if pyvips.API_mode:
9291
@ffi.def_extern()
93-
def _marshal_finish(source_custom, handle):
92+
def _marshal_finish(gobject, handle):
9493
callback = ffi.from_handle(handle)
9594
callback()
9695
_marshal_finish_cb = ffi.cast('GCallback', gobject_lib._marshal_finish)
9796
else:
9897
@ffi.callback('void(VipsTargetCustom*, void*)')
99-
def _marshal_finish(source_custom, handle):
98+
def _marshal_finish(gobject, handle):
10099
callback = ffi.from_handle(handle)
101100
callback()
102101
_marshal_finish_cb = ffi.cast('GCallback', _marshal_finish)
103102
_marshalers['finish'] = _marshal_finish_cb
104103

104+
if at_least_libvips(8, 13):
105+
if pyvips.API_mode:
106+
@ffi.def_extern()
107+
def _marshal_end(gobject, handle):
108+
callback = ffi.from_handle(handle)
109+
return callback()
110+
_marshal_end_cb = ffi.cast('GCallback', gobject_lib._marshal_end)
111+
else:
112+
@ffi.callback('int(VipsTargetCustom*, void*)')
113+
def _marshal_end(gobject, handle):
114+
callback = ffi.from_handle(handle)
115+
return callback()
116+
_marshal_end_cb = ffi.cast('GCallback', _marshal_end)
117+
_marshalers['end'] = _marshal_end_cb
118+
105119

106120
class GObject(object):
107121
"""Manage GObject lifetime.

Diff for: pyvips/vdecls.py

+7
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,13 @@ def cdefs(features):
504504
505505
'''
506506

507+
if _at_least(features, 8, 13):
508+
code += '''
509+
extern "Python" int _marshal_end (VipsTarget*,
510+
void*);
511+
512+
'''
513+
507514
# we must only define these in API mode ... in ABI mode we need to call
508515
# these things earlier
509516
if features['api']:

Diff for: pyvips/vtargetcustom.py

+60-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44

55
import pyvips
6-
from pyvips import ffi, vips_lib
6+
from pyvips import ffi, vips_lib, at_least_libvips
77

88
logger = logging.getLogger(__name__)
99

@@ -44,11 +44,68 @@ def interface_handler(buf):
4444

4545
self.signal_connect("write", interface_handler)
4646

47+
def on_read(self, handler):
48+
"""Attach a read handler.
49+
50+
The interface is exactly as io.read(). The handler is given a number
51+
of bytes to fetch, and should return a bytes-like object containing up
52+
to that number of bytes. If there is no more data available, it should
53+
return None.
54+
55+
Read handlers are optional for targets. If you do not set one, your
56+
target will be treated as unreadable and libvips will be unable to
57+
write some file types (just TIFF, as of the time of writing).
58+
59+
"""
60+
61+
def interface_handler(buf):
62+
chunk = handler(len(buf))
63+
if chunk is None:
64+
return 0
65+
66+
bytes_read = len(chunk)
67+
buf[:bytes_read] = chunk
68+
69+
return bytes_read
70+
71+
self.signal_connect("read", interface_handler)
72+
73+
def on_seek(self, handler):
74+
"""Attach a seek handler.
75+
76+
The interface is the same as io.seek(), so the handler is passed
77+
parameters for offset and whence with the same meanings.
78+
79+
However, the handler MUST return the new seek position. A simple way
80+
to do this is to call io.tell() and return that result.
81+
82+
Seek handlers are optional. If you do not set one, your target will be
83+
treated as unseekable and libvips will be unable to write some file
84+
types (just TIFF, as of the time of writing).
85+
86+
"""
87+
88+
self.signal_connect("seek", handler)
89+
90+
def on_end(self, handler):
91+
"""Attach an end handler.
92+
93+
This optional handler is called at the end of write. It should do any
94+
cleaning up necessary, and return 0 on success and -1 on error.
95+
96+
"""
97+
98+
if not at_least_libvips(8, 13):
99+
# fall back for older libvips
100+
self.on_finish(handler)
101+
else:
102+
self.signal_connect("end", handler)
103+
47104
def on_finish(self, handler):
48105
"""Attach a finish handler.
49106
50-
This optional handler is called at the end of write. It should do any
51-
cleaning up necessary.
107+
For libvips 8.13 and later, this method is deprecated in favour of
108+
:meth:`on_end`.
52109
53110
"""
54111

Diff for: tests/test_connections.py

+49-4
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,22 @@ def seek_handler(offset, whence):
7272
reason="requires libvips >= 8.9")
7373
def test_target_custom(self):
7474
filename = temp_filename(self.tempdir, '.png')
75-
output_file = open(filename, "wb")
75+
output_file = open(filename, "w+b")
7676

7777
def write_handler(chunk):
7878
return output_file.write(chunk)
7979

80-
def finish_handler():
81-
output_file.close()
80+
def end_handler():
81+
try:
82+
output_file.close()
83+
except IOError:
84+
return -1
85+
else:
86+
return 0
8287

8388
target = pyvips.TargetCustom()
8489
target.on_write(write_handler)
85-
target.on_finish(finish_handler)
90+
target.on_end(end_handler)
8691

8792
image = pyvips.Image.new_from_file(JPEG_FILE, access='sequential')
8893
image.write_to_target(target, '.png')
@@ -92,6 +97,46 @@ def finish_handler():
9297

9398
assert (image - image2).abs().max() == 0
9499

100+
@skip_if_no('jpegload')
101+
@skip_if_no('tiffsave')
102+
@pytest.mark.skipif(not pyvips.at_least_libvips(8, 13),
103+
reason="requires libvips >= 8.13")
104+
def test_target_custom_seek(self):
105+
filename = temp_filename(self.tempdir, '.png')
106+
output_file = open(filename, "w+b")
107+
108+
def write_handler(chunk):
109+
return output_file.write(chunk)
110+
111+
def read_handler(size):
112+
return output_file.read(size)
113+
114+
def seek_handler(offset, whence):
115+
output_file.seek(offset, whence)
116+
return output_file.tell()
117+
118+
def end_handler():
119+
try:
120+
output_file.close()
121+
except IOError:
122+
return -1
123+
else:
124+
return 0
125+
126+
target = pyvips.TargetCustom()
127+
target.on_write(write_handler)
128+
target.on_read(read_handler)
129+
target.on_seek(seek_handler)
130+
target.on_end(end_handler)
131+
132+
image = pyvips.Image.new_from_file(JPEG_FILE, access='sequential')
133+
image.write_to_target(target, '.tif')
134+
135+
image = pyvips.Image.new_from_file(JPEG_FILE, access='sequential')
136+
image2 = pyvips.Image.new_from_file(filename, access='sequential')
137+
138+
assert (image - image2).abs().max() == 0
139+
95140
# test webp as well, since that maps the stream rather than using read
96141

97142
@skip_if_no('webpload')

0 commit comments

Comments
 (0)