Skip to content

Commit

Permalink
Add true streaming
Browse files Browse the repository at this point in the history
This commit allows creating image sources from descriptor, file or memory
and image targets to descriptor, file or memory.

Custom classes are not implemented
  • Loading branch information
rolandlo committed Feb 9, 2025
1 parent 2f345ea commit 25e7aaa
Show file tree
Hide file tree
Showing 12 changed files with 336 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to `lua-vips` will be documented in this file.

# master

- add `vips.Connection`, `vips.Source` and `vips.Target` for true streaming support [rolandlo]

# 1.1-11 - 2024-04-16

- add standard Lua support [rolandlo]
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,28 @@ print("written ", ffi.sizeof(mem), "bytes to", mem)

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).

## True Streaming

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.
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:

```lua test.lua
local vips = require "vips"
local stdin, stdout = 0, 1
local source = vips.Source.new_from_descriptor(stdin)
local target = vips.Target.new_to_descriptor(stdout)
local image = vips.Image.new_from_source(source, '', { access = 'sequential' })
image = image:invert()
image:write_to_target(target, '.jpg')
```

Running this script in a Unix terminal via
```term
curl https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/600px-Cat03.jpg | lua test.lua > out.jpg
```

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.

## Error handling

Most `lua-vips` methods will call `error()` if they detect an error. Use
Expand Down
18 changes: 18 additions & 0 deletions example/target.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
local vips = require "vips"

if #arg ~= 2 then
error("Usage: lua target.lua ~/pics/k2.png .avif > x")
end

local infilename = arg[1]
local fmt = arg[2]

local descriptor = {
stdin = 0,
stdout = 1,
stderr = 2,
}

local image = vips.Image.new_from_file(infilename)
local target = vips.Target.new_to_descriptor(descriptor.stdout)
image:write_to_target(target, fmt)
5 changes: 4 additions & 1 deletion lua-vips-1.1-11.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ build = {
["vips.voperation"] = "src/vips/voperation.lua",
["vips.Image"] = "src/vips/Image.lua",
["vips.Image_methods"] = "src/vips/Image_methods.lua",
["vips.Interpolate"] = "src/vips/Interpolate.lua"
["vips.Interpolate"] = "src/vips/Interpolate.lua",
["vips.Connection"] = "src/vips/Connection.lua",
["vips.Source"] = "src/vips/Source.lua",
["vips.Target"] = "src/vips/Target.lua",
}
}
44 changes: 44 additions & 0 deletions spec/connection_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
local vips = require "vips"
local ffi = require "ffi"

local JPEG_FILE = "./spec/images/Gugg_coloured.jpg"
local TMP_FILE = ffi.os == "Windows" and os.getenv("TMP") .. "\\x.png" or "/tmp/x.png"

describe("test connection", function()
setup(function()
-- vips.log.enable(true)
end)

describe("to file target", function()
local target

setup(function()
target = vips.Target.new_to_file(TMP_FILE)
end)

it("can create image from file source", function()
local source = vips.Source.new_from_file(JPEG_FILE)
local image = vips.Image.new_from_source(source, '', { access = 'sequential' })
image:write_to_target(target, '.png')

local image1 = vips.Image.new_from_file(JPEG_FILE, { access = 'sequential' })
local image2 = vips.Image.new_from_file(TMP_FILE, { access = 'sequential' })
assert.is_true((image1 - image2):abs():max() < 10)
end)

it("can create image from memory source", function()
local file = assert(io.open(JPEG_FILE, "rb"))
local content = file:read("*a")
file:close()
local mem = ffi.new("unsigned char[?]", #content)
ffi.copy(mem, content, #content)
local source = vips.Source.new_from_memory(mem)
local image = vips.Image.new_from_source(source, '', { access = 'sequential' })
image:write_to_target(target, '.png')

local image1 = vips.Image.new_from_file(JPEG_FILE, { access = 'sequential' })
local image2 = vips.Image.new_from_file(TMP_FILE, { access = 'sequential' })
assert.is_true((image1 - image2):abs():max() < 10)
end)
end)
end)
3 changes: 3 additions & 0 deletions src/vips.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ local vips = {
voperation = require "vips.voperation",
Image = require "vips.Image_methods",
Interpolate = require "vips.Interpolate",
Connection = require "vips.Connection",
Source = require "vips.Source",
Target = require "vips.Target",
}

function vips.leak_set(leak)
Expand Down
55 changes: 55 additions & 0 deletions src/vips/Connection.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-- abstract base Connection class

local ffi = require "ffi"

local vobject = require "vips.vobject"

local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips")

local Connection_method = {}

local Connection = {
mt = {
__index = Connection_method,
}
}

function Connection.mt:__tostring()
return self:filename() or self:nick() or "(nil)"
end

Connection.new = function(vconnection)
local connection = {}
connection.vconnection = vobject.new(vconnection)
return setmetatable(connection, Connection.mt)
end
function Connection_method:vobject()
return ffi.cast(vobject.typeof, self.vconnection)
end

function Connection_method:filename()
-- Get the filename asscoiated with a connection. Return nil if there is no associated file.
local so = ffi.cast('VipsConnection *', self.vconnection)
local filename = vips_lib.vips_connection_filename(so)
if filename == ffi.NULL then
return nil
else
return ffi.string(filename)
end
end

function Connection_method:nick()
-- Make a human-readable name for a connection suitable for error messages.

local so = ffi.cast('VipsConnection *', self.vconnection)
local nick = vips_lib.vips_connection_nick(so)
if nick == ffi.NULL then
return nil
else
return ffi.string(nick)
end
end

return ffi.metatype("VipsConnection", {
__index = Connection
})
19 changes: 19 additions & 0 deletions src/vips/Image_methods.lua
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,14 @@ function Image.new_from_image(base_image, value)
return image
end

function Image.new_from_source(source, options, ...)
local name = vips_lib.vips_foreign_find_load_source(source.vconnection)
if name == ffi.NULL then
error("Unable to load from source")
end

return voperation.call(ffi.string(name), options, source.vconnection, unpack { ... })
end
-- overloads

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

function Image_method:write_to_target(target, format_string, ...)
collectgarbage("stop")
local options = to_string_copy(vips_lib.vips_filename_get_options(format_string))
local name = vips_lib.vips_foreign_find_save_target(format_string)
collectgarbage("restart")
if name == ffi.NULL then
error(verror.get())
end

return voperation.call(ffi.string(name), options, self, target.vconnection, unpack { ... })
end
-- get/set metadata

function Image_method:get_typeof(name)
Expand Down
41 changes: 41 additions & 0 deletions src/vips/Source.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- An output connection

local ffi = require "ffi"

local verror = require "vips.verror"
local Connection = require "vips.Connection"

local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips")

local Source = {}

Source.new_from_descriptor = function(descriptor)
local source = vips_lib.vips_source_new_from_descriptor(descriptor)
if source == ffi.NULL then
error("Can't create source from descriptor " .. descriptor .. "\n" .. verror.get())
end

return Connection.new(source)
end

Source.new_from_file = function(filename)
local source = vips_lib.vips_source_new_from_file(filename)
if source == ffi.NULL then
error("Can't create source from filename " .. filename .. "\n" .. verror.get())
end

return Connection.new(source)
end

Source.new_from_memory = function(data) -- data is an FFI memory array containing the image data
local source = vips_lib.vips_source_new_from_memory(data, ffi.sizeof(data))
if source == ffi.NULL then
error("Can't create input source from memory \n" .. verror.get())
end

return Connection.new(source)
end

return ffi.metatype("VipsSource", {
__index = Source
})
46 changes: 46 additions & 0 deletions src/vips/Target.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
-- An input connection

local ffi = require "ffi"

local Connection = require "vips.Connection"

local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips")

local Target = {}

Target.new_to_descriptor = function(descriptor)
collectgarbage("stop")
local target = vips_lib.vips_target_new_to_descriptor(descriptor)
collectgarbage("restart")
if target == ffi.NULL then
error("can't create output target from descriptor " .. descriptor)
else
return Connection.new(target)
end
end

Target.new_to_file = function(filename)
collectgarbage("stop")
local target = vips_lib.vips_target_new_to_file(filename)
collectgarbage("restart")
if target == ffi.NULL then
error("can't create output target from filename " .. filename)
else
return Connection.new(target)
end
end

Target.new_to_memory = function()
collectgarbage("stop")
local target = vips_lib.vips_target_new_to_memory()
collectgarbage("restart")
if target == ffi.NULL then
error("can't create output target from memory")
else
return Connection.new(target)
end
end

return ffi.metatype("VipsTarget", {
__index = Target
})
61 changes: 61 additions & 0 deletions src/vips/cdefs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,71 @@ ffi.cdef [[
// opaque
} VipsImage;

typedef struct _VipsConnection {
VipsObject parent_instance;

// opaque
} VipsConnection;

const char *vips_connection_filename (VipsConnection *connection);
const char *vips_connection_nick (VipsConnection *connection);

typedef struct _VipsSource {
VipsConnection parent_instance;

// opaque
} VipsSource;

typedef struct _VipsTarget {
VipsConnection parent_instance;

// opaque
} VipsTarget;

VipsSource *vips_source_new_from_descriptor (int descriptor);
VipsSource *vips_source_new_from_file (const char *filename);
// VipsSource *vips_source_new_from_blob (VipsBlob *blob);
// VipsSource *vips_source_new_from_target (VipsTarget *target);
VipsSource *vips_source_new_from_memory (const void *data, size_t size);
// VipsSource *vips_source_new_from_options (const char *options);
// void vips_source_minimise (VipsSource *source);
// int vips_source_decode (VipsSource *source);
// gint64 vips_source_read (VipsSource *source, void *data, size_t length);
// gboolean vips_source_is_mappable (VipsSource *source);
// gboolean vips_source_is_file (VipsSource *source);
// const void *vips_source_map (VipsSource *source, size_t *length);
// VipsBlob *vips_source_map_blob (VipsSource *source);
// gint64 vips_source_seek (VipsSource *source, gint64 offset, int whence);
// int vips_source_rewind (VipsSource *source);
// gint64 vips_source_sniff_at_most (VipsSource *source, unsigned char **data, size_t length);
// unsigned char *vips_source_sniff (VipsSource *source, size_t length);
// gint64 vips_source_length (VipsSource *source);
// VipsSourceCustom *vips_source_custom_new (void);
// GInputStream *vips_g_input_stream_new_from_source (VipsSource *source);
// VipsSourceGInputStream *vips_source_g_input_stream_new (GInputStream *stream);

VipsTarget *vips_target_new_to_descriptor (int descriptor);
VipsTarget *vips_target_new_to_file (const char *filename);
VipsTarget *vips_target_new_to_memory (void);
// VipsTarget *vips_target_new_temp (VipsTarget *target);
// int vips_target_write (VipsTarget *target, const void *data, size_t length);
// gint64 vips_target_read (VipsTarget *target, void *buffer, size_t length);
// gint64 vips_target_seek (VipsTarget *target, gint64 offset, int whence);
// int vips_target_end (VipsTarget *target);
// unsigned char *vips_target_steal (VipsTarget *target, size_t *length);
// char *vips_target_steal_text (VipsTarget *target);
// int vips_target_putc (VipsTarget *target, int ch);
// int vips_target_writes (VipsTarget *target, const char *str);
// int vips_target_writef (VipsTarget *target, const char *fmt, ...);
// int vips_target_write_amp (VipsTarget *target, const char *str);
// VipsTargetCustom *vips_target_custom_new (void);

const char *vips_foreign_find_load (const char *name);
const char *vips_foreign_find_load_buffer (const void *data, size_t size);
const char *vips_foreign_find_save (const char *name);
const char *vips_foreign_find_save_buffer (const char *suffix);
const char* vips_foreign_find_load_source (VipsSource *source);
const char* vips_foreign_find_save_target (const char* suffix);

VipsImage *vips_image_new_matrix_from_array (int width, int height,
const double *array, int size);
Expand Down
Loading

0 comments on commit 25e7aaa

Please sign in to comment.