diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f4c16..c91d620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] diff --git a/README.md b/README.md index 1f723f2..c0beef6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/example/target.lua b/example/target.lua new file mode 100644 index 0000000..f64e2b7 --- /dev/null +++ b/example/target.lua @@ -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) diff --git a/lua-vips-1.1-11.rockspec b/lua-vips-1.1-11.rockspec index 57a8e5f..e4360ca 100644 --- a/lua-vips-1.1-11.rockspec +++ b/lua-vips-1.1-11.rockspec @@ -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", } } diff --git a/spec/connection_spec.lua b/spec/connection_spec.lua new file mode 100644 index 0000000..44c65be --- /dev/null +++ b/spec/connection_spec.lua @@ -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) diff --git a/src/vips.lua b/src/vips.lua index a42f5fe..ace8231 100644 --- a/src/vips.lua +++ b/src/vips.lua @@ -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) diff --git a/src/vips/Connection.lua b/src/vips/Connection.lua new file mode 100644 index 0000000..66d7605 --- /dev/null +++ b/src/vips/Connection.lua @@ -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 +}) diff --git a/src/vips/Image_methods.lua b/src/vips/Image_methods.lua index ef91dff..3145c6c 100644 --- a/src/vips/Image_methods.lua +++ b/src/vips/Image_methods.lua @@ -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) @@ -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) diff --git a/src/vips/Source.lua b/src/vips/Source.lua new file mode 100644 index 0000000..a642f65 --- /dev/null +++ b/src/vips/Source.lua @@ -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 +}) diff --git a/src/vips/Target.lua b/src/vips/Target.lua new file mode 100644 index 0000000..acb0579 --- /dev/null +++ b/src/vips/Target.lua @@ -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 +}) diff --git a/src/vips/cdefs.lua b/src/vips/cdefs.lua index 3e4363b..30d5bd1 100644 --- a/src/vips/cdefs.lua +++ b/src/vips/cdefs.lua @@ -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); diff --git a/src/vips/gvalue.lua b/src/vips/gvalue.lua index 76a7a09..5e8ba13 100644 --- a/src/vips/gvalue.lua +++ b/src/vips/gvalue.lua @@ -40,6 +40,9 @@ gvalue.double_arr_typeof = ffi.typeof("const double[?]") gvalue.psize_typeof = ffi.typeof("size_t[?]") gvalue.mem_typeof = ffi.typeof("unsigned char[?]") gvalue.interpolate_typeof = ffi.typeof("VipsInterpolate*") +gvalue.connection_typeof = ffi.typeof("VipsConnection*") +gvalue.source_typeof = ffi.typeof("VipsSource*") +gvalue.target_typeof = ffi.typeof("VipsTarget*") -- look up some common gtypes at init for speed gvalue.gbool_type = gobject_lib.g_type_from_name("gboolean") @@ -57,6 +60,9 @@ gvalue.blob_type = gobject_lib.g_type_from_name("VipsBlob") gvalue.band_format_type = gobject_lib.g_type_from_name("VipsBandFormat") gvalue.blend_mode_type = version.at_least(8, 6) and gobject_lib.g_type_from_name("VipsBlendMode") or 0 gvalue.interpolate_type = gobject_lib.g_type_from_name("VipsInterpolate") +gvalue.connection_type = gobject_lib.g_type_from_name("VipsConnection") +gvalue.source_type = gobject_lib.g_type_from_name("VipsSource") +gvalue.target_type = gobject_lib.g_type_from_name("VipsTarget") -- gvalue.*_type can be of type cdata or number depending on the OS and Lua version -- gtypes as returned by vips_lib can also be of type cdata or number @@ -159,6 +165,12 @@ gvalue.set = function(gv, value) end elseif gtype_comp == gvalue.interpolate_type then gobject_lib.g_value_set_object(gv, value) + elseif gtype_comp == gvalue.connection_type then + gobject_lib.g_value_set_object(gv, value) + elseif gtype_comp == gvalue.source_type then + gobject_lib.g_value_set_object(gv, value) + elseif gtype_comp == gvalue.target_type then + gobject_lib.g_value_set_object(gv, value) else error("unsupported gtype for set " .. gvalue.type_name(gtype)) end @@ -256,6 +268,15 @@ gvalue.get = function(gv) elseif gtype_comp == gvalue.interpolate_type then local vo = gobject_lib.g_value_get_object(gv) result = ffi.cast(gvalue.interpolate_typeof, vo) + elseif gtype_comp == gvalue.connection_type then + local vo = gobject_lib.g_value_get_object(gv) + result = ffi.cast(gvalue.connection_typeof, vo) + elseif gtype_comp == gvalue.source_type then + local vo = gobject_lib.g_value_get_object(gv) + result = ffi.cast(gvalue.source_typeof, vo) + elseif gtype_comp == gvalue.target_type then + local vo = gobject_lib.g_value_get_object(gv) + result = ffi.cast(gvalue.target_typeof, vo) else error("unsupported gtype for get " .. gvalue.type_name(gtype)) end