Skip to content

Commit 25e7aaa

Browse files
committed
Add true streaming
This commit allows creating image sources from descriptor, file or memory and image targets to descriptor, file or memory. Custom classes are not implemented
1 parent 2f345ea commit 25e7aaa

12 files changed

+336
-1
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ All notable changes to `lua-vips` will be documented in this file.
44

55
# master
66

7+
- add `vips.Connection`, `vips.Source` and `vips.Target` for true streaming support [rolandlo]
8+
79
# 1.1-11 - 2024-04-16
810

911
- add standard Lua support [rolandlo]

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,28 @@ print("written ", ffi.sizeof(mem), "bytes to", mem)
547547

548548
An allocated char array pointer (GCd with a `ffi.gc` callback) and the length in bytes of the image data is directly returned from libvips (no intermediate FFI allocation).
549549

550+
## True Streaming
551+
552+
When processing images an image library would usually read an image from a file into memory, decode and process it and finally write the encoded result into a file. The processing can only start when the image is fully read into memory and the writing can only start when the processing step is completed.
553+
Libvips can process images directly from a pipe and write directly to a pipe, without the need to read the whole image to memory before being able to start and without the need to finish processing before being able to start writing. This is achieved using a technique called true streaming. In this context there are sources and targets and the processing step happens from source to target. Sources can be created from files, memory or descriptors (like stdin) and targets can be created to files, memory or descriptors (like stdout). Here is an example:
554+
555+
```lua test.lua
556+
local vips = require "vips"
557+
local stdin, stdout = 0, 1
558+
local source = vips.Source.new_from_descriptor(stdin)
559+
local target = vips.Target.new_to_descriptor(stdout)
560+
local image = vips.Image.new_from_source(source, '', { access = 'sequential' })
561+
image = image:invert()
562+
image:write_to_target(target, '.jpg')
563+
```
564+
565+
Running this script in a Unix terminal via
566+
```term
567+
curl https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/600px-Cat03.jpg | lua test.lua > out.jpg
568+
```
569+
570+
will feed a cat image from the internet into standard input, from which the Lua script reads and inverts it and writes it to standard output, where it is redirected to a file. This all happens simultaneously, so the processing and writing doesn't need to wait until the whole image is downloaded from the internet.
571+
550572
## Error handling
551573

552574
Most `lua-vips` methods will call `error()` if they detect an error. Use

example/target.lua

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
local vips = require "vips"
2+
3+
if #arg ~= 2 then
4+
error("Usage: lua target.lua ~/pics/k2.png .avif > x")
5+
end
6+
7+
local infilename = arg[1]
8+
local fmt = arg[2]
9+
10+
local descriptor = {
11+
stdin = 0,
12+
stdout = 1,
13+
stderr = 2,
14+
}
15+
16+
local image = vips.Image.new_from_file(infilename)
17+
local target = vips.Target.new_to_descriptor(descriptor.stdout)
18+
image:write_to_target(target, fmt)

lua-vips-1.1-11.rockspec

+4-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ build = {
4747
["vips.voperation"] = "src/vips/voperation.lua",
4848
["vips.Image"] = "src/vips/Image.lua",
4949
["vips.Image_methods"] = "src/vips/Image_methods.lua",
50-
["vips.Interpolate"] = "src/vips/Interpolate.lua"
50+
["vips.Interpolate"] = "src/vips/Interpolate.lua",
51+
["vips.Connection"] = "src/vips/Connection.lua",
52+
["vips.Source"] = "src/vips/Source.lua",
53+
["vips.Target"] = "src/vips/Target.lua",
5154
}
5255
}

spec/connection_spec.lua

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
local vips = require "vips"
2+
local ffi = require "ffi"
3+
4+
local JPEG_FILE = "./spec/images/Gugg_coloured.jpg"
5+
local TMP_FILE = ffi.os == "Windows" and os.getenv("TMP") .. "\\x.png" or "/tmp/x.png"
6+
7+
describe("test connection", function()
8+
setup(function()
9+
-- vips.log.enable(true)
10+
end)
11+
12+
describe("to file target", function()
13+
local target
14+
15+
setup(function()
16+
target = vips.Target.new_to_file(TMP_FILE)
17+
end)
18+
19+
it("can create image from file source", function()
20+
local source = vips.Source.new_from_file(JPEG_FILE)
21+
local image = vips.Image.new_from_source(source, '', { access = 'sequential' })
22+
image:write_to_target(target, '.png')
23+
24+
local image1 = vips.Image.new_from_file(JPEG_FILE, { access = 'sequential' })
25+
local image2 = vips.Image.new_from_file(TMP_FILE, { access = 'sequential' })
26+
assert.is_true((image1 - image2):abs():max() < 10)
27+
end)
28+
29+
it("can create image from memory source", function()
30+
local file = assert(io.open(JPEG_FILE, "rb"))
31+
local content = file:read("*a")
32+
file:close()
33+
local mem = ffi.new("unsigned char[?]", #content)
34+
ffi.copy(mem, content, #content)
35+
local source = vips.Source.new_from_memory(mem)
36+
local image = vips.Image.new_from_source(source, '', { access = 'sequential' })
37+
image:write_to_target(target, '.png')
38+
39+
local image1 = vips.Image.new_from_file(JPEG_FILE, { access = 'sequential' })
40+
local image2 = vips.Image.new_from_file(TMP_FILE, { access = 'sequential' })
41+
assert.is_true((image1 - image2):abs():max() < 10)
42+
end)
43+
end)
44+
end)

src/vips.lua

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ local vips = {
2323
voperation = require "vips.voperation",
2424
Image = require "vips.Image_methods",
2525
Interpolate = require "vips.Interpolate",
26+
Connection = require "vips.Connection",
27+
Source = require "vips.Source",
28+
Target = require "vips.Target",
2629
}
2730

2831
function vips.leak_set(leak)

src/vips/Connection.lua

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
-- abstract base Connection class
2+
3+
local ffi = require "ffi"
4+
5+
local vobject = require "vips.vobject"
6+
7+
local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips")
8+
9+
local Connection_method = {}
10+
11+
local Connection = {
12+
mt = {
13+
__index = Connection_method,
14+
}
15+
}
16+
17+
function Connection.mt:__tostring()
18+
return self:filename() or self:nick() or "(nil)"
19+
end
20+
21+
Connection.new = function(vconnection)
22+
local connection = {}
23+
connection.vconnection = vobject.new(vconnection)
24+
return setmetatable(connection, Connection.mt)
25+
end
26+
function Connection_method:vobject()
27+
return ffi.cast(vobject.typeof, self.vconnection)
28+
end
29+
30+
function Connection_method:filename()
31+
-- Get the filename asscoiated with a connection. Return nil if there is no associated file.
32+
local so = ffi.cast('VipsConnection *', self.vconnection)
33+
local filename = vips_lib.vips_connection_filename(so)
34+
if filename == ffi.NULL then
35+
return nil
36+
else
37+
return ffi.string(filename)
38+
end
39+
end
40+
41+
function Connection_method:nick()
42+
-- Make a human-readable name for a connection suitable for error messages.
43+
44+
local so = ffi.cast('VipsConnection *', self.vconnection)
45+
local nick = vips_lib.vips_connection_nick(so)
46+
if nick == ffi.NULL then
47+
return nil
48+
else
49+
return ffi.string(nick)
50+
end
51+
end
52+
53+
return ffi.metatype("VipsConnection", {
54+
__index = Connection
55+
})

src/vips/Image_methods.lua

+19
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,14 @@ function Image.new_from_image(base_image, value)
218218
return image
219219
end
220220

221+
function Image.new_from_source(source, options, ...)
222+
local name = vips_lib.vips_foreign_find_load_source(source.vconnection)
223+
if name == ffi.NULL then
224+
error("Unable to load from source")
225+
end
226+
227+
return voperation.call(ffi.string(name), options, source.vconnection, unpack { ... })
228+
end
221229
-- overloads
222230

223231
function Image.mt.__add(a, b)
@@ -413,6 +421,17 @@ function Image_method:write_to_memory_ptr()
413421
return ffi.gc(vips_memory, glib_lib.g_free), tonumber(psize[0])
414422
end
415423

424+
function Image_method:write_to_target(target, format_string, ...)
425+
collectgarbage("stop")
426+
local options = to_string_copy(vips_lib.vips_filename_get_options(format_string))
427+
local name = vips_lib.vips_foreign_find_save_target(format_string)
428+
collectgarbage("restart")
429+
if name == ffi.NULL then
430+
error(verror.get())
431+
end
432+
433+
return voperation.call(ffi.string(name), options, self, target.vconnection, unpack { ... })
434+
end
416435
-- get/set metadata
417436

418437
function Image_method:get_typeof(name)

src/vips/Source.lua

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-- An output connection
2+
3+
local ffi = require "ffi"
4+
5+
local verror = require "vips.verror"
6+
local Connection = require "vips.Connection"
7+
8+
local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips")
9+
10+
local Source = {}
11+
12+
Source.new_from_descriptor = function(descriptor)
13+
local source = vips_lib.vips_source_new_from_descriptor(descriptor)
14+
if source == ffi.NULL then
15+
error("Can't create source from descriptor " .. descriptor .. "\n" .. verror.get())
16+
end
17+
18+
return Connection.new(source)
19+
end
20+
21+
Source.new_from_file = function(filename)
22+
local source = vips_lib.vips_source_new_from_file(filename)
23+
if source == ffi.NULL then
24+
error("Can't create source from filename " .. filename .. "\n" .. verror.get())
25+
end
26+
27+
return Connection.new(source)
28+
end
29+
30+
Source.new_from_memory = function(data) -- data is an FFI memory array containing the image data
31+
local source = vips_lib.vips_source_new_from_memory(data, ffi.sizeof(data))
32+
if source == ffi.NULL then
33+
error("Can't create input source from memory \n" .. verror.get())
34+
end
35+
36+
return Connection.new(source)
37+
end
38+
39+
return ffi.metatype("VipsSource", {
40+
__index = Source
41+
})

src/vips/Target.lua

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
-- An input connection
2+
3+
local ffi = require "ffi"
4+
5+
local Connection = require "vips.Connection"
6+
7+
local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips")
8+
9+
local Target = {}
10+
11+
Target.new_to_descriptor = function(descriptor)
12+
collectgarbage("stop")
13+
local target = vips_lib.vips_target_new_to_descriptor(descriptor)
14+
collectgarbage("restart")
15+
if target == ffi.NULL then
16+
error("can't create output target from descriptor " .. descriptor)
17+
else
18+
return Connection.new(target)
19+
end
20+
end
21+
22+
Target.new_to_file = function(filename)
23+
collectgarbage("stop")
24+
local target = vips_lib.vips_target_new_to_file(filename)
25+
collectgarbage("restart")
26+
if target == ffi.NULL then
27+
error("can't create output target from filename " .. filename)
28+
else
29+
return Connection.new(target)
30+
end
31+
end
32+
33+
Target.new_to_memory = function()
34+
collectgarbage("stop")
35+
local target = vips_lib.vips_target_new_to_memory()
36+
collectgarbage("restart")
37+
if target == ffi.NULL then
38+
error("can't create output target from memory")
39+
else
40+
return Connection.new(target)
41+
end
42+
end
43+
44+
return ffi.metatype("VipsTarget", {
45+
__index = Target
46+
})

src/vips/cdefs.lua

+61
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,71 @@ ffi.cdef [[
166166
// opaque
167167
} VipsImage;
168168

169+
typedef struct _VipsConnection {
170+
VipsObject parent_instance;
171+
172+
// opaque
173+
} VipsConnection;
174+
175+
const char *vips_connection_filename (VipsConnection *connection);
176+
const char *vips_connection_nick (VipsConnection *connection);
177+
178+
typedef struct _VipsSource {
179+
VipsConnection parent_instance;
180+
181+
// opaque
182+
} VipsSource;
183+
184+
typedef struct _VipsTarget {
185+
VipsConnection parent_instance;
186+
187+
// opaque
188+
} VipsTarget;
189+
190+
VipsSource *vips_source_new_from_descriptor (int descriptor);
191+
VipsSource *vips_source_new_from_file (const char *filename);
192+
// VipsSource *vips_source_new_from_blob (VipsBlob *blob);
193+
// VipsSource *vips_source_new_from_target (VipsTarget *target);
194+
VipsSource *vips_source_new_from_memory (const void *data, size_t size);
195+
// VipsSource *vips_source_new_from_options (const char *options);
196+
// void vips_source_minimise (VipsSource *source);
197+
// int vips_source_decode (VipsSource *source);
198+
// gint64 vips_source_read (VipsSource *source, void *data, size_t length);
199+
// gboolean vips_source_is_mappable (VipsSource *source);
200+
// gboolean vips_source_is_file (VipsSource *source);
201+
// const void *vips_source_map (VipsSource *source, size_t *length);
202+
// VipsBlob *vips_source_map_blob (VipsSource *source);
203+
// gint64 vips_source_seek (VipsSource *source, gint64 offset, int whence);
204+
// int vips_source_rewind (VipsSource *source);
205+
// gint64 vips_source_sniff_at_most (VipsSource *source, unsigned char **data, size_t length);
206+
// unsigned char *vips_source_sniff (VipsSource *source, size_t length);
207+
// gint64 vips_source_length (VipsSource *source);
208+
// VipsSourceCustom *vips_source_custom_new (void);
209+
// GInputStream *vips_g_input_stream_new_from_source (VipsSource *source);
210+
// VipsSourceGInputStream *vips_source_g_input_stream_new (GInputStream *stream);
211+
212+
VipsTarget *vips_target_new_to_descriptor (int descriptor);
213+
VipsTarget *vips_target_new_to_file (const char *filename);
214+
VipsTarget *vips_target_new_to_memory (void);
215+
// VipsTarget *vips_target_new_temp (VipsTarget *target);
216+
// int vips_target_write (VipsTarget *target, const void *data, size_t length);
217+
// gint64 vips_target_read (VipsTarget *target, void *buffer, size_t length);
218+
// gint64 vips_target_seek (VipsTarget *target, gint64 offset, int whence);
219+
// int vips_target_end (VipsTarget *target);
220+
// unsigned char *vips_target_steal (VipsTarget *target, size_t *length);
221+
// char *vips_target_steal_text (VipsTarget *target);
222+
// int vips_target_putc (VipsTarget *target, int ch);
223+
// int vips_target_writes (VipsTarget *target, const char *str);
224+
// int vips_target_writef (VipsTarget *target, const char *fmt, ...);
225+
// int vips_target_write_amp (VipsTarget *target, const char *str);
226+
// VipsTargetCustom *vips_target_custom_new (void);
227+
169228
const char *vips_foreign_find_load (const char *name);
170229
const char *vips_foreign_find_load_buffer (const void *data, size_t size);
171230
const char *vips_foreign_find_save (const char *name);
172231
const char *vips_foreign_find_save_buffer (const char *suffix);
232+
const char* vips_foreign_find_load_source (VipsSource *source);
233+
const char* vips_foreign_find_save_target (const char* suffix);
173234

174235
VipsImage *vips_image_new_matrix_from_array (int width, int height,
175236
const double *array, int size);

0 commit comments

Comments
 (0)