├── spec ├── images │ ├── uhdr.jpg │ ├── watermark.png │ └── Gugg_coloured.jpg ├── helpers.lua ├── cache_spec.lua ├── interpolate_spec.lua ├── enum_spec.lua ├── connection_spec.lua ├── gvalue_spec.lua ├── meta_spec.lua ├── convenience_spec.lua ├── write_spec.lua ├── overloads_spec.lua └── new_spec.lua ├── .gitignore ├── example ├── images │ ├── Gugg_coloured.jpg │ └── PNG_transparency_demonstration_1.png ├── hello-world.lua ├── target.lua ├── buffer.lua ├── combine.lua ├── soak.lua ├── array.lua ├── watermark.lua └── noise.lua ├── .luacheckrc ├── .busted ├── src ├── vips │ ├── Image.lua │ ├── verror.lua │ ├── version.lua │ ├── Interpolate.lua │ ├── Source.lua │ ├── Target.lua │ ├── Connection.lua │ ├── log.lua │ ├── vobject.lua │ ├── voperation.lua │ ├── gvalue.lua │ ├── cdefs.lua │ └── Image_methods.lua └── vips.lua ├── LICENSE ├── lua-vips-1.1-12.rockspec ├── CHANGELOG.md ├── .github └── workflows │ └── ci.yml └── README.md /spec/images/uhdr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libvips/lua-vips/HEAD/spec/images/uhdr.jpg -------------------------------------------------------------------------------- /spec/images/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libvips/lua-vips/HEAD/spec/images/watermark.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | lua-vips-*.src.rock 3 | 4 | # LuaCov 5 | luacov.stats.out 6 | luacov.report.out 7 | -------------------------------------------------------------------------------- /spec/images/Gugg_coloured.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libvips/lua-vips/HEAD/spec/images/Gugg_coloured.jpg -------------------------------------------------------------------------------- /example/images/Gugg_coloured.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libvips/lua-vips/HEAD/example/images/Gugg_coloured.jpg -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | files["spec/**/*.lua"] = { 2 | std = "+busted", 3 | } 4 | exclude_files = { ".install", ".luarocks", ".lua" } 5 | -------------------------------------------------------------------------------- /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | default = { 3 | helper = "./spec/helpers.lua", 4 | coverage = os.getenv("TEST_COVERAGE") == "1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/images/PNG_transparency_demonstration_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libvips/lua-vips/HEAD/example/images/PNG_transparency_demonstration_1.png -------------------------------------------------------------------------------- /src/vips/Image.lua: -------------------------------------------------------------------------------- 1 | -- the definition of the image class 2 | -- 3 | -- we have to split the definition of Image from the methods to avoid 4 | -- recursive requires between the methods and voperation 5 | 6 | local Image = { 7 | mt = {} 8 | } 9 | 10 | return Image 11 | -------------------------------------------------------------------------------- /example/hello-world.lua: -------------------------------------------------------------------------------- 1 | local vips = require "vips" 2 | 3 | -- uncomment for very chatty output 4 | -- vips.log.enable(true) 5 | 6 | local image1 = vips.Image.text("Hello World!", { dpi = 300 }) 7 | print("writing to x.png ...") 8 | image1:write_to_file("x.png") 9 | 10 | -------------------------------------------------------------------------------- /src/vips/verror.lua: -------------------------------------------------------------------------------- 1 | -- handle the libvips error buffer 2 | 3 | local ffi = require "ffi" 4 | 5 | local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") 6 | 7 | local verror = { 8 | -- get and clear the error buffer 9 | get = function() 10 | local errstr = ffi.string(vips_lib.vips_error_buffer()) 11 | vips_lib.vips_error_clear() 12 | 13 | return errstr 14 | end 15 | } 16 | 17 | return verror 18 | -------------------------------------------------------------------------------- /example/target.lua: -------------------------------------------------------------------------------- 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) 19 | -------------------------------------------------------------------------------- /src/vips/version.lua: -------------------------------------------------------------------------------- 1 | -- detect and test libvips version 2 | 3 | local ffi = require "ffi" 4 | 5 | local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") 6 | 7 | local version = {} 8 | 9 | version = { 10 | major = vips_lib.vips_version(0), 11 | minor = vips_lib.vips_version(1), 12 | micro = vips_lib.vips_version(2), 13 | 14 | -- test for libvips version is better than x.y .. we use this to turn on 15 | -- various workarounds for older libvips 16 | at_least = function(x, y) 17 | return version.major > x or (version.major == x and version.minor >= y) 18 | end 19 | } 20 | 21 | return version 22 | -------------------------------------------------------------------------------- /example/buffer.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/luajit 2 | 3 | -- load and save images to and from memory buffers 4 | 5 | local vips = require "vips" 6 | 7 | if #arg ~= 1 then 8 | print("usage: luajit buffer.lua image-file") 9 | error() 10 | end 11 | local f = assert(io.open(arg[1], "rb")) 12 | local content = f:read("*all") 13 | 14 | local im = vips.Image.new_from_buffer(content, "", {access = "sequential"}) 15 | 16 | -- brighten 20% 17 | im = (im * 1.2):cast("uchar") 18 | 19 | -- print as mime jpg 20 | local buffer = im:write_to_buffer(".jpg", {Q = 90}) 21 | print("Content-length: " .. #buffer) 22 | print("Content-type: image/jpeg") 23 | print("") 24 | print(buffer) 25 | -------------------------------------------------------------------------------- /example/combine.lua: -------------------------------------------------------------------------------- 1 | local vips = require "vips" 2 | 3 | -- uncomment for very chatty output 4 | -- vips.log.enable(true) 5 | 6 | local main_filename = "images/Gugg_coloured.jpg" 7 | local watermark_filename = "images/PNG_transparency_demonstration_1.png" 8 | 9 | local main = vips.Image.new_from_file(main_filename) 10 | local watermark = vips.Image.new_from_file(watermark_filename) 11 | 12 | -- scale the alpha down to 30% transparency 13 | watermark = watermark * { 1, 1, 1, 0.3 } 14 | 15 | -- composite onto the base image at the top left 16 | local result = main:composite(watermark, "over", { x = 10, y = 10 }) 17 | 18 | print("writing x.jpg ...") 19 | result:write_to_file("x.jpg") 20 | 21 | -------------------------------------------------------------------------------- /example/soak.lua: -------------------------------------------------------------------------------- 1 | -- a lua version of 2 | -- https://github.com/libvips/pyvips/blob/master/examples/soak-test.py 3 | -- this should run in a steady amount of memory 4 | 5 | local vips = require "vips" 6 | 7 | vips.leak_set(true) 8 | vips.cache_set_max(0) 9 | 10 | if #arg ~= 2 then 11 | print("usage: luajit soak.lua image-file iterations") 12 | error() 13 | end 14 | 15 | local im 16 | 17 | for i = 0, tonumber(arg[2]) do 18 | print("loop ", i) 19 | 20 | im = vips.Image.new_from_file(arg[1]) 21 | im = im:embed(100, 100, 3000, 3000, { extend = "mirror" }) 22 | -- local buf = im:write_to_buffer(".jpg") 23 | -- im:write_to_file("x.jpg") 24 | im:write_to_file("x.v") 25 | im = nil -- luacheck: ignore 26 | 27 | collectgarbage() 28 | end 29 | -------------------------------------------------------------------------------- /spec/helpers.lua: -------------------------------------------------------------------------------- 1 | -- Pre-load the vips module 2 | require "vips" 3 | 4 | local assert = require "luassert.assert" 5 | local say = require "say" 6 | 7 | local function almost_equal(_, arguments) 8 | local threshold = arguments[3] or 0.001 9 | 10 | if type(arguments[1]) ~= "number" or type(arguments[2]) ~= "number" then 11 | return false 12 | end 13 | 14 | return math.abs(arguments[1] - arguments[2]) < threshold 15 | end 16 | 17 | say:set("assertion.almost_equal.positive", 18 | "Expected %s to almost equal %s") 19 | say:set("assertion.almost_equal.negative", 20 | "Expected %s to not almost equal %s") 21 | assert:register("assertion", "almost_equal", almost_equal, 22 | "assertion.almost_equal.positive", 23 | "assertion.almost_equal.negative") -------------------------------------------------------------------------------- /src/vips/Interpolate.lua: -------------------------------------------------------------------------------- 1 | -- make image interpolators, see affine 2 | 3 | local ffi = require "ffi" 4 | 5 | local verror = require "vips.verror" 6 | local vobject = require "vips.vobject" 7 | 8 | local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") 9 | 10 | local Interpolate = {} 11 | 12 | Interpolate.vobject = function(self) 13 | return ffi.cast(vobject.typeof, self) 14 | end 15 | 16 | Interpolate.new = function(name) 17 | -- there could potentially be other params here, but ignore that for now 18 | local interpolate = vips_lib.vips_interpolate_new(name) 19 | if interpolate == nil then 20 | error("no such interpolator\n" .. verror.get()) 21 | end 22 | 23 | return vobject.new(interpolate) 24 | end 25 | 26 | return ffi.metatype("VipsInterpolate", { 27 | __index = Interpolate 28 | }) 29 | 30 | 31 | -------------------------------------------------------------------------------- /spec/cache_spec.lua: -------------------------------------------------------------------------------- 1 | local vips = require "vips" 2 | 3 | -- test cache control 4 | describe("cache control", function() 5 | 6 | setup(function() 7 | -- vips.log.enable(true) 8 | end) 9 | 10 | it("can set number of operations to cache", function() 11 | local max = vips.get_max() 12 | 13 | vips.set_max(10) 14 | assert.are.equal(vips.get_max(), 10) 15 | vips.set_max(max) 16 | end) 17 | 18 | it("can limit the number of operations to cache by open files", function() 19 | local max = vips.get_max_files() 20 | 21 | vips.set_max_files(10) 22 | assert.are.equal(vips.get_max_files(), 10) 23 | vips.set_max_files(max) 24 | end) 25 | 26 | it("can limit the number of operations to cache by memory", function() 27 | local max = vips.get_max_mem() 28 | 29 | vips.set_max_mem(10) 30 | assert.are.equal(vips.get_max_mem(), 10) 31 | vips.set_max_mem(max) 32 | end) 33 | end) 34 | -------------------------------------------------------------------------------- /example/array.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/luajit 2 | 3 | -- turn a vips image into a lua array 4 | 5 | local vips = require "vips" 6 | local ffi = require "ffi" 7 | 8 | -- make a tiny two band u16 image whose pixels are their coordinates 9 | local im = vips.Image.xyz(3, 2):cast("ushort") 10 | 11 | -- write as a C-style memory array, so band-interleaved, a series of scanlines 12 | -- 13 | -- "data" is a pointer of type uchar*, though the underlying memory is really 14 | -- pairs of int16s, see above 15 | local data = im:write_to_memory() 16 | 17 | -- the type of each pixel ... a pair of shorts 18 | ffi.cdef [[ 19 | typedef struct { 20 | unsigned short x; 21 | unsigned short y; 22 | } pixel; 23 | ]] 24 | -- and cast the image pointer to a 1D array of pixel structs 25 | local ptype = ffi.typeof("pixel*") 26 | local array = ffi.cast(ptype, data) 27 | 28 | -- and print! ffi arrays number from 0 29 | for y = 0, im:height() - 1 do 30 | for x = 0, im:width() - 1 do 31 | local i = x + y * im:width() 32 | 33 | print("x = ", x, "y = ", y, "value = ", array[i].x, array[i].y) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/interpolate_spec.lua: -------------------------------------------------------------------------------- 1 | local vips = require "vips" 2 | 3 | -- test image interpolation 4 | describe("image interpolation", function() 5 | setup(function() 6 | -- vips.log.enable(true) 7 | end) 8 | 9 | it("can rotate an image using nearest interpolator", function() 10 | local interpolate = vips.Interpolate.new("nearest") 11 | local original = { 12 | { 1, 2, 3 }, 13 | { 4, 5, 6 }, 14 | { 7, 8, 9 }, 15 | } 16 | local rotated = { 17 | { 0.0, 0.0, 1.0, 0.0 }, 18 | { 0.0, 0.0, 1.0, 2.0 }, 19 | { 0.0, 7.0, 5.0, 3.0 }, 20 | { 0.0, 8.0, 9.0, 6.0 } 21 | } 22 | local im = vips.Image.new_from_array(original) 23 | local rot = im:rotate(45, { interpolate = interpolate }) 24 | assert.are.equal(rot:width(), 4) 25 | assert.are.equal(rot:height(), 4) 26 | assert.are.equal(rot:bands(), 1) 27 | for x = 1, 4 do 28 | for y = 1, 4 do 29 | assert.are_equal(rot(x - 1, y - 1), rotated[y][x]) 30 | end 31 | end 32 | end) 33 | end) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 John Cupitt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/watermark.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/luajit 2 | 3 | -- add a simple text watermark to an image 4 | -- ./watermark.lua ~/pics/IMG_0073.JPG x.jpg "Hello world!" 5 | 6 | local vips = require "vips" 7 | 8 | -- uncomment for very chatty output 9 | -- vips.log.enable(true) 10 | 11 | if #arg ~= 3 then 12 | print("usage: luajit watermark.lua input-image-file output-image-file text") 13 | error() 14 | end 15 | local im = vips.Image.new_from_file(arg[1], {access = "sequential"}) 16 | 17 | -- make the text mask 18 | local text = vips.Image.text(arg[3], 19 | {width = 200, dpi = 200, align = "centre", font = "sans bold"}) 20 | text = text:rotate(-45) 21 | -- make the text transparent 22 | text = (text * 0.3):cast("uchar") 23 | text = text:gravity("centre", 200, 200) 24 | -- this block of pixels will be reused many times ... make a copy 25 | text = text:copy_memory() 26 | text = text:replicate(1 + math.floor(im:width() / text:width()), 27 | 1 + math.floor(im:height() / text:height())) 28 | text = text:crop(0, 0, im:width(), im:height()) 29 | 30 | -- we make a constant colour image and attach the text mask as the alpha 31 | local overlay = 32 | text:new_from_image({255, 128, 128}):copy{interpretation = "srgb"} 33 | overlay = overlay:bandjoin(text) 34 | 35 | -- overlay the text 36 | im = im:composite(overlay, "over") 37 | 38 | im:write_to_file(arg[2]) 39 | -------------------------------------------------------------------------------- /src/vips/Source.lua: -------------------------------------------------------------------------------- 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 | }) 42 | -------------------------------------------------------------------------------- /src/vips/Target.lua: -------------------------------------------------------------------------------- 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 | }) 47 | -------------------------------------------------------------------------------- /example/noise.lua: -------------------------------------------------------------------------------- 1 | local vips = require "vips" 2 | 3 | local size = 1024 4 | 5 | -- perlin's "turbulence" image 6 | local function turbulence(turb_size) 7 | local image 8 | local iterations = math.log(turb_size, 2) - 2 9 | for i = 0, iterations do 10 | -- make perlin noise at this scale 11 | local layer = vips.Image.perlin(turb_size, turb_size, { 12 | cell_size = turb_size / math.pow(2, i) 13 | }) 14 | layer = layer:abs() * (1.0 / (i + 1)) 15 | 16 | -- and sum 17 | if image then 18 | image = image + layer 19 | else 20 | image = layer 21 | end 22 | end 23 | 24 | return image 25 | end 26 | 27 | -- make a gradient colour map ... a smooth fade from start to stop, with 28 | -- start and stop as CIELAB colours, then map as sRGB 29 | local function gradient(start, stop) 30 | local lut = vips.Image.identity() / 255 31 | lut = lut * start + (lut * -1 + 1) * stop 32 | return lut:colourspace("srgb", { source_space = "lab" }) 33 | end 34 | 35 | -- make a turbulent stripe pattern 36 | local stripe = vips.Image.xyz(size, size):extract_band(0) 37 | stripe = (stripe * 360 * 4 / size + turbulence(size) * 700):sin() 38 | 39 | -- make a colour map ... we want a smooth gradient from white to dark brown 40 | -- colours here in CIELAB 41 | local dark_brown = { 7.45, 4.3, 8 } 42 | local white = { 100, 0, 0 } 43 | local lut = gradient(dark_brown, white) 44 | 45 | -- rescale to 0 - 255 and colour with our lut 46 | stripe = ((stripe + 1) * 128):maplut(lut) 47 | 48 | print("writing x.png ...") 49 | stripe:write_to_file("x.png") 50 | -------------------------------------------------------------------------------- /spec/enum_spec.lua: -------------------------------------------------------------------------------- 1 | local vips = require "vips" 2 | 3 | -- test metadata read/write 4 | describe("enum expansions", function() 5 | local array, im 6 | 7 | setup(function() 8 | array = { 1, 2, 3, 4 } 9 | im = vips.Image.new_from_array(array) 10 | -- vips.log.enable(true) 11 | end) 12 | 13 | -- there are loads of expansions, just test one of each type 14 | 15 | it("can call pow() with a constant arg", function() 16 | local im2 = im:pow(2) 17 | 18 | assert.are.equal(im2:width(), 4) 19 | assert.are.equal(im2:height(), 1) 20 | assert.are.equal(im2:bands(), 1) 21 | assert.are.equal(im2:avg(), (1 + 4 + 9 + 16) / 4) 22 | end) 23 | 24 | it("can call pow() with an image arg", function() 25 | local im2 = im:pow(im) 26 | 27 | assert.are.equal(im2:width(), 4) 28 | assert.are.equal(im2:height(), 1) 29 | assert.are.equal(im2:bands(), 1) 30 | assert.are.equal(im2:avg(), (1 ^ 1 + 2 ^ 2 + 3 ^ 3 + 4 ^ 4) / 4) 31 | end) 32 | 33 | it("can call lshift()", function() 34 | local im2 = im:lshift(1) 35 | 36 | assert.are.equal(im2:width(), 4) 37 | assert.are.equal(im2:height(), 1) 38 | assert.are.equal(im2:bands(), 1) 39 | assert.are.equal(im2:avg(), (2 + 4 + 6 + 8) / 4) 40 | end) 41 | 42 | it("can call less()", function() 43 | local im2 = im:less(2) 44 | 45 | assert.are.equal(im2:width(), 4) 46 | assert.are.equal(im2:height(), 1) 47 | assert.are.equal(im2:bands(), 1) 48 | assert.are.equal(im2:avg(), (255 + 0 + 0 + 0) / 4) 49 | end) 50 | end) 51 | -------------------------------------------------------------------------------- /src/vips/Connection.lua: -------------------------------------------------------------------------------- 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 | }) 56 | -------------------------------------------------------------------------------- /lua-vips-1.1-12.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-vips" 2 | version = "1.1-12" 3 | rockspec_format = "3.0" 4 | 5 | source = { 6 | url = "git://github.com/libvips/lua-vips.git", 7 | tag = "v1.1-12", 8 | } 9 | 10 | description = { 11 | summary = "A fast image processing library with low memory needs.", 12 | detailed = [[ 13 | This rock implements a binding for the libvips image processing library. 14 | It is usually faster and needs less memory than similar libraries. 15 | 16 | For use with standard Lua, the dependency luaffi-tkl is used as a drop-in 17 | replacement for LuaJIT's ffi module. 18 | ]], 19 | homepage = "https://github.com/libvips/lua-vips", 20 | license = "MIT", 21 | labels = { "image" } 22 | } 23 | 24 | dependencies = { 25 | "lua >= 5.1, < 5.5", -- standard Lua or LuaJIT >= 2.0 26 | "luaffi-tkl >= 1.0" -- provided by VM with LuaJIT, use `luarocks config rocks_provided.luaffi-tkl 2.1-1` in that case 27 | } 28 | 29 | test_dependencies = { 30 | "busted" 31 | } 32 | 33 | test = { 34 | type = "busted" 35 | } 36 | 37 | build = { 38 | type = "builtin", 39 | modules = { 40 | vips = "src/vips.lua", 41 | ["vips.cdefs"] = "src/vips/cdefs.lua", 42 | ["vips.verror"] = "src/vips/verror.lua", 43 | ["vips.version"] = "src/vips/version.lua", 44 | ["vips.log"] = "src/vips/log.lua", 45 | ["vips.gvalue"] = "src/vips/gvalue.lua", 46 | ["vips.vobject"] = "src/vips/vobject.lua", 47 | ["vips.voperation"] = "src/vips/voperation.lua", 48 | ["vips.Image"] = "src/vips/Image.lua", 49 | ["vips.Image_methods"] = "src/vips/Image_methods.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", 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /spec/connection_spec.lua: -------------------------------------------------------------------------------- 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) 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `lua-vips` will be documented in this file. 4 | 5 | # master 6 | 7 | # 1.1-12 - 2025-02-09 8 | 9 | - add `vips.Connection`, `vips.Source` and `vips.Target` for true streaming support [rolandlo] 10 | 11 | # 1.1-11 - 2024-04-16 12 | 13 | - add standard Lua support [rolandlo] 14 | - add `vips.Interpolate` [jcupitt, rolandlo] 15 | - add `vips.concurrency_get()` and `set()` [kamyabzad] 16 | - add `hasalpha`/`addalpha` [RiskoZoSlovenska] 17 | 18 | # 1.1-10 - 2021-04-18 19 | 20 | - fix NYI warnings [kleisauke] 21 | 22 | # 1.1-9 - 2018-08-03 23 | 24 | - add `vips.leak_set()` [jcupitt] 25 | - add `soak.lua` example [jcupitt] 26 | - fix five minor memleaks [kleisauke] 27 | - update links for new home [jcupitt] 28 | 29 | # 1.1-8 - 2018-07-25 30 | 31 | - cleanups and some reorganisation [kleisauke] 32 | - fix regressions from 1.1-7 [kleisauke] 33 | - add `find_load` [kleisauke] 34 | - add `find_load_buffer` [kleisauke] 35 | 36 | # 1.1-7 - 2018-03-23 37 | 38 | - cleanups and some reorganisation 39 | - renamed cache control funcs, the names were missing the `cache_` prefix 40 | - fix `image:remove()` [kleisauke] 41 | 42 | # 1.1-6 - 2018-03-23 43 | 44 | - add operation cache control 45 | 46 | # 1.1-5 - 2017-10-09 47 | 48 | - add verror: handle libvips error buffer 49 | - add version: handle libvips version numbers 50 | - add `gvalue.to_enum`: wrap up enum encoding 51 | - add `composite` 52 | - add `new_from_memory` 53 | - add `write_to_memory` 54 | - remove `[]` and `#` overloads -- too confusing, and they broke debuggers 55 | 56 | # 1.1-4 - 2017-08-30 57 | 58 | - small doc fixes 59 | - fix get() on gobject enum properties with older libvips 60 | - test for gobject enum properties as strings 61 | 62 | # 1.1-3 - 2017-08-08 63 | 64 | - more Windows fixes 65 | - fix a callback leak with buffers, thanks wuyachao 66 | 67 | # 1.1-2 - 2017-07-21 68 | 69 | - fix "-" characters in arg names 70 | 71 | # 1.1-1 - 2017-06-19 72 | 73 | - tweaks to help LuaJIT on Windows 74 | 75 | # 1.0-1 - 2017-06-04 76 | 77 | - first API stable release 78 | -------------------------------------------------------------------------------- /src/vips.lua: -------------------------------------------------------------------------------- 1 | -- top include for lua-vips 2 | 3 | local ffi = require "ffi" 4 | 5 | local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") 6 | 7 | require "vips.cdefs" 8 | 9 | local result = vips_lib.vips_init("lua-vips") 10 | if result ~= 0 then 11 | local errstr = ffi.string(vips_lib.vips_error_buffer()) 12 | vips_lib.vips_error_clear() 13 | 14 | error("unable to start up libvips: " .. errstr) 15 | end 16 | 17 | local vips = { 18 | verror = require "vips.verror", 19 | version = require "vips.version", 20 | log = require "vips.log", 21 | gvalue = require "vips.gvalue", 22 | vobject = require "vips.vobject", 23 | voperation = require "vips.voperation", 24 | Image = require "vips.Image_methods", 25 | Interpolate = require "vips.Interpolate", 26 | Connection = require "vips.Connection", 27 | Source = require "vips.Source", 28 | Target = require "vips.Target", 29 | } 30 | 31 | function vips.leak_set(leak) 32 | vips_lib.vips_leak_set(leak) 33 | end 34 | 35 | function vips.cache_set_max(max) 36 | vips_lib.vips_cache_set_max(max) 37 | end 38 | 39 | function vips.cache_get_max() 40 | return vips_lib.vips_cache_get_max() 41 | end 42 | 43 | function vips.cache_set_max_files(max) 44 | vips_lib.vips_cache_set_max_files(max) 45 | end 46 | 47 | function vips.cache_get_max_files() 48 | return vips_lib.vips_cache_get_max_files() 49 | end 50 | 51 | function vips.cache_set_max_mem(max) 52 | vips_lib.vips_cache_set_max_mem(max) 53 | end 54 | 55 | function vips.cache_get_max_mem() 56 | return vips_lib.vips_cache_get_max_mem() 57 | end 58 | 59 | function vips.concurrency_set(concurrency) 60 | return vips_lib.vips_concurrency_set(concurrency) 61 | end 62 | 63 | function vips.concurrency_get() 64 | return vips_lib.vips_concurrency_get() 65 | end 66 | 67 | function vips.type_find(basename, nickname) 68 | return vips_lib.vips_type_find(basename, nickname) 69 | end 70 | 71 | function vips.has(name) 72 | return vips.type_find("VipsOperation", name) ~= 0 73 | end 74 | -- for compat with 1.1-6, when these were misnamed 75 | vips.set_max = vips.cache_set_max 76 | vips.get_max = vips.cache_get_max 77 | vips.set_max_files = vips.cache_set_max_files 78 | vips.get_max_files = vips.cache_get_max_files 79 | vips.set_max_mem = vips.cache_set_max_mem 80 | vips.get_max_mem = vips.cache_get_max_mem 81 | 82 | return vips 83 | -------------------------------------------------------------------------------- /src/vips/log.lua: -------------------------------------------------------------------------------- 1 | -- simple logging 2 | 3 | local logging_enabled = false 4 | 5 | local type = type 6 | local print = print 7 | local pairs = pairs 8 | local unpack = unpack or table.unpack 9 | local tostring = tostring 10 | local str_rep = string.rep 11 | 12 | local log = {} 13 | log = { 14 | enable = function(on) 15 | logging_enabled = on 16 | end, 17 | 18 | msg = function(...) 19 | if logging_enabled then 20 | print(unpack { ... }) 21 | end 22 | end, 23 | 24 | prettyprint_table = function(p, table) 25 | local p_r_cache = {} 26 | local function sub_p_r(t, indent) 27 | if (p_r_cache[tostring(t)]) then 28 | p(indent .. "*" .. tostring(t)) 29 | else 30 | p_r_cache[tostring(t)] = true 31 | if type(t) == "table" then 32 | for pos, val in pairs(t) do 33 | if type(val) == "table" then 34 | p(indent .. 35 | "[" .. pos .. "] => " .. tostring(t) .. " {") 36 | local length = type(pos) == "string" and #pos or pos 37 | sub_p_r(val, indent .. str_rep(" ", length + 8)) 38 | p(indent .. str_rep(" ", length + 6) .. "}") 39 | elseif type(val) == "string" then 40 | p(indent .. "[" .. pos .. '] => "' .. 41 | val .. '"') 42 | else 43 | p(indent .. "[" .. pos .. "] => " .. 44 | tostring(val)) 45 | end 46 | end 47 | else 48 | p(indent .. tostring(t)) 49 | end 50 | end 51 | end 52 | 53 | if type(table) == "table" then 54 | p(tostring(table) .. " {") 55 | sub_p_r(table, " ") 56 | p("}") 57 | else 58 | sub_p_r(table, " ") 59 | end 60 | p() 61 | end, 62 | 63 | msg_r = function(t) 64 | if logging_enabled then 65 | log.prettyprint_table(log.msg, t) 66 | end 67 | end, 68 | 69 | print_r = function(t) 70 | if logging_enabled then 71 | log.prettyprint_table(print, t) 72 | end 73 | end 74 | } 75 | 76 | return log 77 | -------------------------------------------------------------------------------- /spec/gvalue_spec.lua: -------------------------------------------------------------------------------- 1 | local vips = require "vips" 2 | 3 | -- test gvalue 4 | describe("test gvalue", function() 5 | local im, values 6 | 7 | setup(function() 8 | im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 9 | values = im:bandsplit() 10 | -- vips.log.enable(true) 11 | end) 12 | 13 | it("can set/get an int-valued gvalue", function() 14 | local value = vips.gvalue() 15 | value:init(vips.gvalue.gint_type) 16 | value:set(12) 17 | assert.are.equal(12, value:get()) 18 | end) 19 | 20 | it("can set/get a string-valued gvalue", function() 21 | local value = vips.gvalue() 22 | value:init(vips.gvalue.gstr_type) 23 | value:set("banana") 24 | assert.are.equal("banana", value:get()) 25 | end) 26 | 27 | it("can set/get a bool-valued gvalue", function() 28 | local value = vips.gvalue() 29 | value:init(vips.gvalue.gbool_type) 30 | value:set(true) 31 | assert.are.equal(1, value:get()) 32 | end) 33 | 34 | it("can set/get a double-valued gvalue", function() 35 | local value = vips.gvalue() 36 | value:init(vips.gvalue.gdouble_type) 37 | value:set(3.1415) 38 | assert.are.equal(3.1415, value:get()) 39 | end) 40 | 41 | it("can set/get a enum-valued gvalue", function() 42 | if vips.version.at_least(8, 6) then 43 | local value = vips.gvalue() 44 | value:init(vips.gvalue.blend_mode_type) 45 | -- need to map str -> int by hand, since the mode arg is actually 46 | -- arrayint 47 | value:set(vips.gvalue.to_enum(vips.gvalue.blend_mode_type, 'over')) 48 | assert.are.equal('over', value:get()) 49 | end 50 | end) 51 | 52 | it("can set/get a array-int-valued gvalue", function() 53 | local value = vips.gvalue() 54 | value:init(vips.gvalue.array_int_type) 55 | value:set({ 1, 2, 3 }) 56 | assert.are.same({ 1, 2, 3 }, value:get()) 57 | end) 58 | 59 | it("can set/get a array-double-valued gvalue", function() 60 | local value = vips.gvalue() 61 | value:init(vips.gvalue.array_double_type) 62 | value:set({ 1.1, 2.1, 3.1 }) 63 | assert.are.same({ 1.1, 2.1, 3.1 }, value:get()) 64 | end) 65 | 66 | it("can set/get a image-valued gvalue", function() 67 | local value = vips.gvalue() 68 | value:init(vips.gvalue.image_type) 69 | value:set(im) 70 | assert.are.same(im, value:get()) 71 | end) 72 | 73 | it("can set/get a array-image-valued gvalue", function() 74 | local value = vips.gvalue() 75 | value:init(vips.gvalue.array_image_type) 76 | value:set(values) 77 | assert.are.same(values, value:get()) 78 | end) 79 | end) 80 | -------------------------------------------------------------------------------- /spec/meta_spec.lua: -------------------------------------------------------------------------------- 1 | local vips = require "vips" 2 | local ffi = require "ffi" 3 | 4 | local UHDR_FILE = "./spec/images/uhdr.jpg" 5 | -- test metadata read/write 6 | describe("metadata", function() 7 | local array, im 8 | local tmp_vips_filename = ffi.os == "Windows" and os.getenv("TMP") .. "\\x.v" or "/tmp/x.v" 9 | 10 | setup(function() 11 | array = { 1, 2, 3, 4 } 12 | im = vips.Image.new_from_array(array) 13 | -- vips.log.enable(true) 14 | end) 15 | 16 | teardown(function() 17 | os.remove(tmp_vips_filename) 18 | end) 19 | 20 | it("can set/get int", function() 21 | local im2 = im:copy() 22 | 23 | im2:set_type(vips.gvalue.gint_type, "banana", 12) 24 | assert.are.equal(im2:get("banana"), 12) 25 | end) 26 | 27 | it("can remove metadata", function() 28 | local im2 = im:copy() 29 | 30 | im2:set_type(vips.gvalue.gint_type, "banana", 12) 31 | im2:remove("banana") 32 | assert.are.equal(im2:get_typeof("banana"), 0) 33 | end) 34 | 35 | it("can set/get double", function() 36 | local im2 = im:copy() 37 | 38 | im2:set_type(vips.gvalue.gdouble_type, "banana", 3.1415) 39 | assert.are.equal(im2:get("banana"), 3.1415) 40 | end) 41 | 42 | it("can set/get string", function() 43 | local im2 = im:copy() 44 | 45 | im2:set_type(vips.gvalue.gstr_type, "banana", "tasty one") 46 | assert.are.same(im2:get("banana"), "tasty one") 47 | end) 48 | 49 | it("can set/get through vips file save/load", function() 50 | local im2 = im:copy() 51 | 52 | im2:set_type(vips.gvalue.gint_type, "banana", 12) 53 | im2:write_to_file(tmp_vips_filename) 54 | local im3 = vips.Image.new_from_file(tmp_vips_filename) 55 | assert.are.same(im3:get("banana"), im2:get("banana")) 56 | end) 57 | 58 | it("can get property enums as strings", function() 59 | local im2 = im:copy() 60 | 61 | assert.are.same(im2:format(), "double") 62 | end) 63 | 64 | it("can set gainmap", function() 65 | if vips.version.at_least(8, 18) and vips.has("uhdrload") then 66 | local function crop_gainmap(image) 67 | local gainmap = image:get_gainmap() 68 | 69 | if gainmap then 70 | local new_gainmap = gainmap:crop(0, 0, 10, 10) 71 | image = image:copy() 72 | image:set_type(vips.gvalue.image_type, "gainmap", new_gainmap) 73 | end 74 | 75 | return image 76 | end 77 | 78 | local image = vips.Image.new_from_file(UHDR_FILE) 79 | image = crop_gainmap(image) 80 | local buf = image:write_to_buffer(".jpg") 81 | local new_image = vips.Image.new_from_buffer(buf, "") 82 | local new_gainmap = new_image:get_gainmap() 83 | 84 | assert.are.same(new_gainmap:width(), 10) 85 | end 86 | end) 87 | end) 88 | -------------------------------------------------------------------------------- /src/vips/vobject.lua: -------------------------------------------------------------------------------- 1 | -- manage VipsObject 2 | -- abstract base class for voperation and vimage 3 | 4 | local ffi = require "ffi" 5 | 6 | local verror = require "vips.verror" 7 | local log = require "vips.log" 8 | local gvalue = require "vips.gvalue" 9 | 10 | local print = print 11 | local error = error 12 | local collectgarbage = collectgarbage 13 | 14 | local vips_lib 15 | local gobject_lib 16 | if ffi.os == "Windows" then 17 | vips_lib = ffi.load("libvips-42.dll") 18 | gobject_lib = ffi.load("libgobject-2.0-0.dll") 19 | else 20 | vips_lib = ffi.load("vips") 21 | gobject_lib = vips_lib 22 | end 23 | 24 | local vobject = {} 25 | 26 | -- types to get ref back from vips_object_get_argument() 27 | vobject.typeof = ffi.typeof("VipsObject*") 28 | vobject.pspec_typeof = ffi.typeof("GParamSpec*[1]") 29 | vobject.argument_class_typeof = ffi.typeof("VipsArgumentClass*[1]") 30 | vobject.argument_instance_typeof = ffi.typeof("VipsArgumentInstance*[1]") 31 | 32 | vobject.print_all = function(msg) 33 | collectgarbage() 34 | print(msg) 35 | vips_lib.vips_object_print_all() 36 | print() 37 | end 38 | 39 | vobject.new = function(pt) 40 | return ffi.gc(pt, gobject_lib.g_object_unref) 41 | end 42 | 43 | -- return 0 for not found and leave the error in the error log 44 | vobject.get_typeof = function(self, name) 45 | local pspec = vobject.pspec_typeof() 46 | local argument_class = vobject.argument_class_typeof() 47 | local argument_instance = vobject.argument_instance_typeof() 48 | local result = vips_lib.vips_object_get_argument(self, name, 49 | pspec, argument_class, argument_instance) 50 | 51 | if result ~= 0 then 52 | return 0 53 | end 54 | 55 | return pspec[0].value_type 56 | end 57 | 58 | vobject.get_type = function(self, name, gtype) 59 | log.msg("vobject.get_type") 60 | log.msg(" name =", name) 61 | 62 | if gtype == 0 then 63 | return false 64 | end 65 | 66 | local pgv = gvalue(true) 67 | pgv[0]:init(gtype) 68 | 69 | -- this will add a ref for GObject properties, that ref will be 70 | -- unreffed when the gvalue is finalized 71 | gobject_lib.g_object_get_property(self, name, pgv) 72 | 73 | local result = pgv[0]:get() 74 | gobject_lib.g_value_unset(pgv[0]) 75 | 76 | return result 77 | end 78 | 79 | vobject.set_type = function(self, name, value, gtype) 80 | log.msg("vobject.set_type") 81 | log.msg(" name =", name) 82 | log.msg(" value =", value) 83 | 84 | if gtype == 0 then 85 | return false 86 | end 87 | 88 | local pgv = gvalue(true) 89 | pgv[0]:init(gtype) 90 | pgv[0]:set(value) 91 | gobject_lib.g_object_set_property(self, name, pgv) 92 | gobject_lib.g_value_unset(pgv[0]) 93 | 94 | return true 95 | end 96 | 97 | vobject.get = function(self, name) 98 | log.msg("vobject.get") 99 | log.msg(" name =", name) 100 | 101 | local gtype = self:get_typeof(name) 102 | if gtype == 0 then 103 | error(verror.get()) 104 | end 105 | 106 | return vobject.get_type(self, name, gtype) 107 | end 108 | 109 | vobject.set = function(self, name, value) 110 | log.msg("vobject.set") 111 | log.msg(" name =", name) 112 | log.msg(" value =", value) 113 | 114 | local gtype = self:get_typeof(name) 115 | if gtype == 0 then 116 | error(verror.get()) 117 | end 118 | 119 | vobject.set_type(self, name, value, gtype) 120 | 121 | return true 122 | end 123 | 124 | return ffi.metatype("VipsObject", { 125 | 126 | -- no __gc method, we don't build these things ourselves, just wrap the 127 | -- pointer, so we use ffi.gc() instead 128 | 129 | __index = vobject 130 | }) 131 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | unix: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | luaVersion: ["5.1", "5.2", "5.3", "5.4", "luajit-openresty"] 11 | os: ["ubuntu-latest", "macos-latest"] 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | steps: 16 | - uses: actions/checkout@v6 17 | 18 | - uses: leafo/gh-actions-lua@v12 19 | with: 20 | luaVersion: ${{ matrix.luaVersion }} 21 | 22 | - uses: leafo/gh-actions-luarocks@v6 23 | 24 | - name: Install Ubuntu dependencies 25 | if: runner.os == 'Linux' 26 | run: | 27 | sudo apt-get update 28 | sudo apt-get install --no-install-recommends libvips-dev 29 | 30 | - name: Install macOS dependencies 31 | if: runner.os == 'macOS' 32 | run: | 33 | brew install vips 34 | echo "DYLD_LIBRARY_PATH=$(brew --prefix vips)/lib:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV 35 | 36 | - name: Prepare LuaJIT environment 37 | if: startsWith(matrix.luaVersion, 'luajit') 38 | run: luarocks config --scope system rocks_provided.luaffi-tkl 2.1-1 39 | 40 | - name: Install lua-vips 41 | run: luarocks make 42 | 43 | - name: Lint with luacheck 44 | run: | 45 | luarocks install luacheck 46 | luacheck -q . 47 | 48 | - name: Busted tests 49 | run: luarocks test spec/ -- -o gtest -v spec 50 | 51 | windows: 52 | strategy: 53 | fail-fast: false 54 | matrix: 55 | include: 56 | # Two tests are failing 57 | #- lua: { version: "5.1", msys2-name: "lua51", exe: "lua5.1" } 58 | - lua: { version: "5.3", msys2-name: "lua53", exe: "lua5.3" } 59 | - lua: { version: "5.4", msys2-name: "lua", exe: "lua" } 60 | - lua: { version: "5.1", msys2-name: "luajit", exe: "luajit" } 61 | 62 | runs-on: windows-latest 63 | defaults: 64 | run: 65 | shell: msys2 {0} 66 | 67 | steps: 68 | - uses: actions/checkout@v6 69 | 70 | - uses: msys2/setup-msys2@v2 71 | with: 72 | msystem: mingw64 73 | update: true 74 | install: >- 75 | git 76 | make 77 | mingw-w64-x86_64-toolchain 78 | mingw-w64-x86_64-libvips 79 | mingw-w64-x86_64-openslide 80 | mingw-w64-x86_64-libheif 81 | mingw-w64-x86_64-libjxl 82 | mingw-w64-x86_64-imagemagick 83 | mingw-w64-x86_64-poppler 84 | mingw-w64-x86_64-lua-luarocks 85 | mingw-w64-x86_64-${{ matrix.lua.msys2-name }} 86 | 87 | - name: Prepare LuaRocks config for Lua 5.3 88 | if: matrix.lua.exe == 'lua5.3' 89 | run: cp /mingw64/etc/luarocks/config-5.{4,3}.lua 90 | 91 | - name: Prepare LuaRocks 92 | run: | 93 | luarocks config --scope system lua_version ${{ matrix.lua.version }} 94 | luarocks config --scope system lua_interpreter ${{ matrix.lua.exe }}.exe 95 | luarocks config --scope system lua_dir /mingw64 96 | 97 | - name: Prepare LuaJIT environment 98 | if: matrix.lua.exe == 'luajit' 99 | run: luarocks config --scope system rocks_provided.luaffi-tkl 2.1-1 100 | 101 | - name: Add local LuaRocks bin to PATH 102 | run: echo "$HOME/.luarocks/bin" >> $GITHUB_PATH 103 | 104 | - name: Install lua-vips 105 | run: luarocks make 106 | 107 | - name: Lint with luacheck 108 | run: | 109 | luarocks install luacheck 110 | luacheck.bat -q . 111 | 112 | - name: Busted tests 113 | run: luarocks test spec -- --lua=${{ matrix.lua.exe }} -o gtest -v 114 | -------------------------------------------------------------------------------- /spec/convenience_spec.lua: -------------------------------------------------------------------------------- 1 | local vips = require "vips" 2 | 3 | -- test convenience functions 4 | describe("test convenience functions", function() 5 | local array, im 6 | 7 | setup(function() 8 | array = { 1, 2, 3, 4 } 9 | im = vips.Image.new_from_array(array) 10 | -- vips.log.enable(true) 11 | end) 12 | 13 | it("can join one image bandwise", function() 14 | local im2 = im:bandjoin(im) 15 | 16 | assert.are.equal(im2:width(), 4) 17 | assert.are.equal(im2:height(), 1) 18 | assert.are.equal(im2:bands(), 2) 19 | assert.are.equal(im2:extract_band(0):avg(), 2.5) 20 | assert.are.equal(im2:extract_band(1):avg(), 2.5) 21 | end) 22 | 23 | it("can join images bandwise", function() 24 | local im2 = im:bandjoin { im + 1, im + 2 } 25 | 26 | assert.are.equal(im2:width(), 4) 27 | assert.are.equal(im2:height(), 1) 28 | assert.are.equal(im2:bands(), 3) 29 | assert.are.equal(im2:extract_band(0):avg(), 2.5) 30 | assert.are.equal(im2:extract_band(1):avg(), 3.5) 31 | assert.are.equal(im2:extract_band(2):avg(), 4.5) 32 | end) 33 | 34 | it("can join constants to images bandwise", function() 35 | local im2 = im:bandjoin(255) 36 | 37 | assert.are.equal(im2:width(), 4) 38 | assert.are.equal(im2:height(), 1) 39 | assert.are.equal(im2:bands(), 2) 40 | assert.are.equal(im2:extract_band(0):avg(), 2.5) 41 | assert.are.equal(im2:extract_band(1):avg(), 255) 42 | end) 43 | 44 | it("can join images and constants bandwise", function() 45 | local im2 = im:bandjoin { im + 1, 255, im + 2 } 46 | 47 | assert.are.equal(im2:width(), 4) 48 | assert.are.equal(im2:height(), 1) 49 | assert.are.equal(im2:bands(), 4) 50 | assert.are.equal(im2:extract_band(0):avg(), 2.5) 51 | assert.are.equal(im2:extract_band(1):avg(), 3.5) 52 | assert.are.equal(im2:extract_band(2):avg(), 255) 53 | assert.are.equal(im2:extract_band(3):avg(), 4.5) 54 | end) 55 | 56 | it("can join images and array constants bandwise", function() 57 | local im2 = im:bandjoin { im + 1, { 255, 128 } } 58 | 59 | assert.are.equal(im2:width(), 4) 60 | assert.are.equal(im2:height(), 1) 61 | assert.are.equal(im2:bands(), 4) 62 | assert.are.equal(im2:extract_band(0):avg(), 2.5) 63 | assert.are.equal(im2:extract_band(1):avg(), 3.5) 64 | assert.are.equal(im2:extract_band(2):avg(), 255) 65 | assert.are.equal(im2:extract_band(3):avg(), 128) 66 | end) 67 | 68 | it("can call composite", function() 69 | if vips.version.at_least(8, 6) then 70 | local base = (im + { 10, 11, 12 }):copy { interpretation = "srgb" } 71 | local overlay = (base + 10):bandjoin(128) 72 | local comp = base:composite(overlay, "over") 73 | local pixel = comp:getpoint(0, 0) 74 | 75 | assert.are.equal(comp:width(), 4) 76 | assert.are.equal(comp:height(), 1) 77 | assert.are.equal(comp:bands(), 4) 78 | assert.is_true(math.abs(pixel[1] - 16) < 0.1) 79 | assert.is_true(math.abs(pixel[2] - 17) < 0.1) 80 | assert.is_true(math.abs(pixel[3] - 18) < 0.1) 81 | assert.are.equal(pixel[4], 255) 82 | end 83 | end) 84 | 85 | it("can call bandrank", function() 86 | local im2 = im:bandrank(im + 1, { index = 0 }) 87 | 88 | assert.are.equal(im2:width(), 4) 89 | assert.are.equal(im2:height(), 1) 90 | assert.are.equal(im2:bands(), 1) 91 | assert.are.equal(im2:extract_band(0):avg(), 2.5) 92 | end) 93 | 94 | it("can call bandsplit", function() 95 | local bands = im:bandjoin { im + 1, { 255, 128 } }:bandsplit() 96 | 97 | assert.are.equal(#bands, 4) 98 | assert.are.equal(bands[1]:width(), 4) 99 | assert.are.equal(bands[1]:height(), 1) 100 | assert.are.equal(bands[1]:bands(), 1) 101 | end) 102 | 103 | it("can call ifthenelse with an image and two constants", function() 104 | local result = im:more(2):ifthenelse(1, 2) 105 | 106 | assert.are.equal(result:width(), 4) 107 | assert.are.equal(result:height(), 1) 108 | assert.are.equal(result:bands(), 1) 109 | assert.are.equal(result:avg(), 6 / 4) 110 | end) 111 | 112 | it("can call ifthenelse with two images and one constant", function() 113 | local result = im:more(2):ifthenelse(im + 3, 2) 114 | 115 | assert.are.equal(result:width(), 4) 116 | assert.are.equal(result:height(), 1) 117 | assert.are.equal(result:bands(), 1) 118 | assert.are.equal(result:avg(), 17 / 4) 119 | end) 120 | 121 | it("can call hasalpha", function() 122 | local im1 = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 123 | local im2 = vips.Image.new_from_file("./spec/images/watermark.png") 124 | 125 | assert.is_false(im1:hasalpha()) 126 | assert.is_true(im2:hasalpha()) 127 | end) 128 | 129 | it("can call addalpha", function () 130 | assert.are.equal(im:addalpha():avg(), 128.75) 131 | end) 132 | end) 133 | -------------------------------------------------------------------------------- /spec/write_spec.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local vips = require "vips" 3 | 4 | -- test image writers 5 | describe("test image write", function() 6 | 7 | setup(function() 8 | -- vips.log.enable(true) 9 | end) 10 | 11 | describe("to file", function() 12 | local im = vips.Image.black(100, 100) 13 | local tmp_png_filename = ffi.os == "Windows" and os.getenv("TMP") .. "\\x.png" or "/tmp/x.png" 14 | local tmp_jpg_filename = ffi.os == "Windows" and os.getenv("TMP") .. "\\x.jpg" or "/tmp/x.jpg" 15 | 16 | teardown(function() 17 | os.remove(tmp_png_filename) 18 | os.remove(tmp_jpg_filename) 19 | end) 20 | 21 | it("can save and then load a png", function() 22 | im:write_to_file(tmp_png_filename) 23 | local im2 = vips.Image.new_from_file(tmp_png_filename) 24 | 25 | assert.are.equal(im:width(), im2:width()) 26 | assert.are.equal(im:height(), im2:height()) 27 | assert.are.equal(im:avg(), im2:avg()) 28 | end) 29 | 30 | it("can save and then load a jpg with an option", function() 31 | im:write_to_file(tmp_jpg_filename, { Q = 90 }) 32 | local im2 = vips.Image.new_from_file(tmp_jpg_filename) 33 | 34 | assert.are.equal(im:width(), im2:width()) 35 | assert.are.equal(im:height(), im2:height()) 36 | assert.are.almost_equal(im:avg(), im2:avg()) 37 | end) 38 | end) 39 | 40 | describe("to buffer", function() 41 | it("can write a jpeg to buffer", function() 42 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 43 | local buf = im:write_to_buffer(".jpg") 44 | local f = io.open("x.jpg", "w+b") 45 | f:write(buf) 46 | f:close() 47 | local im2 = vips.Image.new_from_file("x.jpg") 48 | 49 | assert.are.equal(im:width(), im2:width()) 50 | assert.are.equal(im:height(), im2:height()) 51 | assert.are.equal(im:format(), im2:format()) 52 | assert.are.equal(im:xres(), im2:xres()) 53 | assert.are.equal(im:yres(), im2:yres()) 54 | -- remove test file 55 | os.remove("x.jpg") 56 | end) 57 | 58 | it("can write a jpeg to buffer with an option", function() 59 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 60 | local buf = im:write_to_buffer(".jpg") 61 | local buf2 = im:write_to_buffer(".jpg", { Q = 100 }) 62 | 63 | assert.is.True(#buf2 > #buf) 64 | end) 65 | 66 | it("can write an image to a memory area", function() 67 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 68 | local mem = im:write_to_memory() 69 | 70 | assert.are.equal(im:width() * im:height() * 3, ffi.sizeof(mem)) 71 | end) 72 | 73 | it("can read an image back from a memory area", function() 74 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 75 | local mem = im:write_to_memory() 76 | assert.are.equal(im:width() * im:height() * 3, ffi.sizeof(mem)) 77 | local im2 = vips.Image.new_from_memory(mem, 78 | im:width(), im:height(), im:bands(), im:format()) 79 | 80 | assert.are.equal(im:avg(), im2:avg()) 81 | end) 82 | 83 | it("can write an image to a memory area (no copy)", function() 84 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 85 | local _, size = im:write_to_memory_ptr() 86 | 87 | assert.are.equal(im:width() * im:height() * 3, size) 88 | end) 89 | 90 | it("can read an image back from a memory area (no copy)", function() 91 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 92 | local ptr, size = im:write_to_memory_ptr() 93 | assert.are.equal(im:width() * im:height() * 3, size) 94 | local im2 = vips.Image.new_from_memory_ptr(ptr, 95 | size, im:width(), im:height(), im:bands(), im:format()) 96 | 97 | assert.are.equal(im:avg(), im2:avg()) 98 | end) 99 | end) 100 | 101 | describe("MODIFY args", function() 102 | it("can draw a circle on an image", function() 103 | local im = vips.Image.black(101, 101) 104 | local im2 = im:draw_circle(255, 50, 50, 50, { fill = true }) 105 | 106 | assert.are.equal(im2:width(), 101) 107 | assert.are.equal(im2:height(), 101) 108 | assert.are.almost_equal(im2:avg(), 255 * 3.1415927 / 4, 0.2) 109 | end) 110 | 111 | it("each draw op makes a new image", function() 112 | local im = vips.Image.black(101, 101) 113 | local im2 = im:draw_circle(255, 50, 50, 50, { fill = true }) 114 | local im3 = im2:draw_circle(0, 50, 50, 40, { fill = true }) 115 | 116 | assert.are.equal(im2:width(), 101) 117 | assert.are.equal(im2:height(), 101) 118 | assert.are.almost_equal(im2:avg(), 255 * 3.1415927 / 4, 0.2) 119 | assert.is.True(im3:avg() < im2:avg()) 120 | end) 121 | end) 122 | end) 123 | -------------------------------------------------------------------------------- /spec/overloads_spec.lua: -------------------------------------------------------------------------------- 1 | local vips = require "vips" 2 | 3 | -- test all operator overloads 4 | 5 | -- make a table of x repeated n times 6 | local function replicate(x, n) 7 | local result = {} 8 | 9 | for i = 1, n do 10 | result[i] = x 11 | end 12 | 13 | return result 14 | end 15 | 16 | -- apply an operation to a nested table, or to a number 17 | local function map(op, a) 18 | local result 19 | 20 | if type(a) == "table" then 21 | result = {} 22 | for i = 1, #a do 23 | result[i] = map(op, a[i]) 24 | end 25 | else 26 | result = op(a) 27 | end 28 | 29 | return result 30 | end 31 | 32 | -- apply an operation pairwise to two nested tables, or to a table and a number, 33 | -- or to two numbers 34 | local function map2(op, a, b) 35 | if type(a) == "table" and type(b) == "table" then 36 | assert.are.equal(#a, #b) 37 | end 38 | 39 | local result 40 | if type(a) == "table" or type(b) == "table" then 41 | if type(a) ~= "table" then 42 | a = replicate(a, #b) 43 | end 44 | 45 | if type(b) ~= "table" then 46 | b = replicate(b, #a) 47 | end 48 | 49 | result = {} 50 | for i = 1, #a do 51 | result[i] = map2(op, a[i], b[i]) 52 | end 53 | else 54 | result = op(a, b) 55 | end 56 | 57 | return result 58 | end 59 | 60 | -- find the sum and number of elements in a nested table 61 | local function sum(a) 62 | local total = 0 63 | local n = 0 64 | 65 | if type(a) == "table" then 66 | for i = 1, #a do 67 | local new_total 68 | local new_n 69 | 70 | new_total, new_n = sum(a[i]) 71 | 72 | total = total + new_total 73 | n = n + new_n 74 | end 75 | else 76 | total = a 77 | n = 1 78 | end 79 | 80 | return total, n 81 | end 82 | 83 | -- find the average of a nested table 84 | local function avg(a) 85 | local total 86 | local n 87 | 88 | total, n = sum(a) 89 | 90 | return total / n 91 | end 92 | 93 | local function test_binary(name, vop, lop) 94 | local array, im 95 | 96 | setup(function() 97 | array = { 1, 2, 3, 4 } 98 | im = vips.Image.new_from_array(array) 99 | end) 100 | 101 | describe(name, function() 102 | 103 | it("can " .. name .. " image and single constant", function() 104 | local im2 = vop(im, 12) 105 | local a2 = map2(lop, array, 12) 106 | 107 | assert.are.equal(im2:width(), 4) 108 | assert.are.equal(im2:height(), 1) 109 | assert.are.equal(im2:bands(), 1) 110 | assert.are.almost_equal(im2:avg(), avg(a2)) 111 | end) 112 | 113 | it("can " .. name .. " image and single constant, reversed", function() 114 | local im2 = vop(12, im) 115 | local a2 = map2(lop, 12, array) 116 | 117 | assert.are.equal(im2:width(), 4) 118 | assert.are.equal(im2:height(), 1) 119 | assert.are.equal(im2:bands(), 1) 120 | assert.are.almost_equal(im2:avg(), avg(a2)) 121 | end) 122 | 123 | it("can " .. name .. " an image and an array", function() 124 | local array_constant = { 12, 13, 14 } 125 | local im2 = vop(im, array_constant) 126 | local a2 = map2(lop, array, replicate(array_constant, #array)) 127 | 128 | assert.are.equal(im2:width(), 4) 129 | assert.are.equal(im2:height(), 1) 130 | assert.are.equal(im2:bands(), 3) 131 | assert.are.almost_equal(im2:avg(), avg(a2)) 132 | end) 133 | 134 | it("can " .. name .. " an image and an array, reversed", function() 135 | local array_constant = { 12, 13, 14 } 136 | local im2 = vop(array_constant, im) 137 | local a2 = map2(lop, replicate(array_constant, #array), array) 138 | 139 | assert.are.equal(im2:width(), 4) 140 | assert.are.equal(im2:height(), 1) 141 | assert.are.equal(im2:bands(), 3) 142 | assert.are.almost_equal(im2:avg(), avg(a2)) 143 | end) 144 | 145 | it("can " .. name .. " two images", function() 146 | local im2 = vop(im, im) 147 | local a2 = map2(lop, array, array) 148 | 149 | assert.are.equal(im2:width(), 4) 150 | assert.are.equal(im2:height(), 1) 151 | assert.are.equal(im2:bands(), 1) 152 | assert.are.almost_equal(im2:avg(), avg(a2)) 153 | end) 154 | end) 155 | end 156 | 157 | local function test_binary_noreverse(name, vop, lop) 158 | local array, im 159 | 160 | setup(function() 161 | array = { 1, 2, 3, 4 } 162 | im = vips.Image.new_from_array(array) 163 | end) 164 | 165 | describe(name, function() 166 | it("can " .. name .. " image and single constant", function() 167 | local im2 = vop(im, 12) 168 | local a2 = map2(lop, array, 12) 169 | 170 | assert.are.equal(im2:width(), 4) 171 | assert.are.equal(im2:height(), 1) 172 | assert.are.equal(im2:bands(), 1) 173 | assert.are.almost_equal(im2:avg(), avg(a2)) 174 | end) 175 | 176 | it("can " .. name .. " an image and an array", function() 177 | local im2 = vop(im, { 12, 13, 14 }) 178 | local a2 = map2(lop, array, 13) 179 | 180 | assert.are.equal(im2:width(), 4) 181 | assert.are.equal(im2:height(), 1) 182 | assert.are.equal(im2:bands(), 3) 183 | assert.are.almost_equal(im2:avg(), avg(a2)) 184 | end) 185 | 186 | it("can " .. name .. " two images", function() 187 | local im2 = vop(im, im) 188 | local a2 = map2(lop, array, array) 189 | 190 | assert.are.equal(im2:width(), 4) 191 | assert.are.equal(im2:height(), 1) 192 | assert.are.equal(im2:bands(), 1) 193 | assert.are.almost_equal(im2:avg(), avg(a2)) 194 | end) 195 | end) 196 | end 197 | 198 | local function test_unary(name, vop, lop) 199 | local array, im 200 | 201 | setup(function() 202 | array = { 1, 2, 3, 4 } 203 | im = vips.Image.new_from_array(array) 204 | end) 205 | 206 | describe(name, function() 207 | it("can " .. name .. " an image", function() 208 | local im2 = vop(im) 209 | local a2 = map(lop, array) 210 | 211 | assert.are.equal(im2:width(), 4) 212 | assert.are.equal(im2:height(), 1) 213 | assert.are.equal(im2:bands(), 1) 214 | assert.are.almost_equal(im2:avg(), avg(a2)) 215 | end) 216 | end) 217 | end 218 | 219 | describe("test overload", function() 220 | test_binary("add", 221 | function(a, b) 222 | return vips.Image.mt.__add(a, b) 223 | end, 224 | function(a, b) 225 | return a + b 226 | end) 227 | 228 | test_binary("sub", 229 | function(a, b) 230 | return vips.Image.mt.__sub(a, b) 231 | end, 232 | function(a, b) 233 | return a - b 234 | end) 235 | 236 | test_binary("mul", 237 | function(a, b) 238 | return vips.Image.mt.__mul(a, b) 239 | end, 240 | function(a, b) 241 | return a * b 242 | end) 243 | 244 | test_binary("div", 245 | function(a, b) 246 | return vips.Image.mt.__div(a, b) 247 | end, 248 | function(a, b) 249 | return a / b 250 | end) 251 | 252 | test_binary_noreverse("mod", 253 | function(a, b) 254 | return vips.Image.mt.__mod(a, b) 255 | end, 256 | function(a, b) 257 | return a % b 258 | end) 259 | 260 | test_binary("pow", 261 | function(a, b) 262 | return vips.Image.mt.__pow(a, b) 263 | end, 264 | function(a, b) 265 | return a ^ b 266 | end) 267 | 268 | test_unary("unm", 269 | function(a) 270 | return vips.Image.mt.__unm(a) 271 | end, 272 | function(a) 273 | return -a 274 | end) 275 | 276 | describe("band overloads", function() 277 | local array, im, im2 278 | 279 | setup(function() 280 | array = { 1, 2, 3, 4 } 281 | im = vips.Image.new_from_array(array) 282 | im2 = im:bandjoin({ im + 1, im + 2 }) 283 | end) 284 | 285 | it("can bandjoin with '..'", function() 286 | local b = im .. im2 287 | 288 | assert.are.equal(b:width(), 4) 289 | assert.are.equal(b:height(), 1) 290 | assert.are.equal(b:bands(), 4) 291 | assert.are.equal(b:extract_band(0):avg(), 2.5) 292 | end) 293 | end) 294 | 295 | describe("call overload", function() 296 | local array, im, im2 297 | 298 | setup(function() 299 | array = { 1, 2, 3, 4 } 300 | im = vips.Image.new_from_array(array) 301 | im2 = im:bandjoin({ im + 1, im + 2 }) 302 | end) 303 | 304 | it("can extract a pixel with '()'", function() 305 | local a, b, c = im2(1, 0) 306 | 307 | assert.are.equal(a, 2) 308 | assert.are.equal(b, 3) 309 | assert.are.equal(c, 4) 310 | end) 311 | end) 312 | end) 313 | -------------------------------------------------------------------------------- /spec/new_spec.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | ffi.cdef[[ 3 | void* malloc(size_t size); 4 | void free(void *ptr); 5 | ]] 6 | local vips = require "vips" 7 | 8 | -- test image new/load/etc. 9 | describe("test image creation", function() 10 | setup(function() 11 | -- vips.log.enable(true) 12 | end) 13 | 14 | describe("test image from array", function() 15 | it("can make an image from a 1D array", function() 16 | local array = { 1, 2, 3, 4 } 17 | local im = vips.Image.new_from_array(array) 18 | 19 | assert.are.equal(im:width(), 4) 20 | assert.are.equal(im:height(), 1) 21 | assert.are.equal(im:get("scale"), 1) 22 | assert.are.equal(im:get("offset"), 0) 23 | assert.are.equal(im:avg(), 2.5) 24 | end) 25 | 26 | it("can make an image from a 2D array", function() 27 | local array = { { 1, 2 }, { 3, 4 } } 28 | local im = vips.Image.new_from_array(array) 29 | 30 | assert.are.equal(im:width(), 2) 31 | assert.are.equal(im:height(), 2) 32 | assert.are.equal(im:get("scale"), 1) 33 | assert.are.equal(im:get("offset"), 0) 34 | assert.are.equal(im:avg(), 2.5) 35 | end) 36 | 37 | it("can set scale and offset on an array", function() 38 | local array = { { 1, 2 }, { 3, 4 } } 39 | local im = vips.Image.new_from_array(array, 12, 3) 40 | 41 | assert.are.equal(im:width(), 2) 42 | assert.are.equal(im:height(), 2) 43 | assert.are.equal(im:get("scale"), 12) 44 | assert.are.equal(im:get("offset"), 3) 45 | assert.are.equal(im:avg(), 2.5) 46 | end) 47 | end) 48 | 49 | describe("test image from file", function() 50 | it("can load a jpeg from a file", function() 51 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 52 | 53 | assert.are.equal(im:width(), 972) 54 | assert.are.equal(im:height(), 1296) 55 | assert.are.almost_equal(im:avg(), 113.96) 56 | end) 57 | 58 | it("throws error when file does not exits", function() 59 | assert.has_error(function() 60 | vips.Image.new_from_file("/path/does/not/exist/unknown.jpg") 61 | end, "VipsForeignLoad: file \"/path/does/not/exist/unknown.jpg\" does not exist\n") 62 | end) 63 | 64 | it("can subsample a jpeg from a file", function() 65 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg", 66 | { shrink = 2 }) 67 | 68 | assert.are.equal(im:width(), 486) 69 | assert.are.equal(im:height(), 648) 70 | assert.are.almost_equal(im:avg(), 113.979) 71 | end) 72 | 73 | it("can subsample a jpeg from a file, shrink in filename", function() 74 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg[shrink=2]") 75 | 76 | assert.are.equal(im:width(), 486) 77 | assert.are.equal(im:height(), 648) 78 | assert.are.almost_equal(im:avg(), 113.979) 79 | end) 80 | end) 81 | 82 | describe("test image from buffer", function() 83 | it("can write a jpeg to buffer", function() 84 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 85 | local buf = im:write_to_buffer(".jpg") 86 | local f = io.open("x.jpg", "w+b") 87 | f:write(buf) 88 | f:close() 89 | local im2 = vips.Image.new_from_file("x.jpg") 90 | 91 | assert.are.equal(im:width(), im2:width()) 92 | assert.are.equal(im:height(), im2:height()) 93 | assert.are.equal(im:format(), im2:format()) 94 | assert.are.equal(im:xres(), im2:xres()) 95 | assert.are.equal(im:yres(), im2:yres()) 96 | -- remove test file 97 | os.remove("x.jpg") 98 | end) 99 | 100 | it("can load a jpeg from a buffer", function() 101 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 102 | local f = io.open("./spec/images/Gugg_coloured.jpg", "rb") 103 | local buf = f:read("*all") 104 | f:close() 105 | local im2 = vips.Image.new_from_buffer(buf) 106 | 107 | assert.are.equal(im:width(), im2:width()) 108 | assert.are.equal(im:height(), im2:height()) 109 | assert.are.equal(im:format(), im2:format()) 110 | assert.are.equal(im:xres(), im2:xres()) 111 | assert.are.equal(im:yres(), im2:yres()) 112 | end) 113 | 114 | it("throws error when loading from unknown buffer", function() 115 | local buf = "GIF89a" 116 | 117 | assert.error_matches(function() 118 | vips.Image.new_from_buffer(buf) 119 | end, "unable to call VipsForeignLoadNsgifBuffer\ngifload_buffer: .+") 120 | end) 121 | 122 | it("can load a jpeg from a buffer, options in a table", function() 123 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg", 124 | { shrink = 2 }) 125 | local f = io.open("./spec/images/Gugg_coloured.jpg", "rb") 126 | local buf = f:read("*all") 127 | f:close() 128 | local im2 = vips.Image.new_from_buffer(buf, "", { shrink = 2 }) 129 | 130 | assert.are.equal(im:width(), im2:width()) 131 | assert.are.equal(im:height(), im2:height()) 132 | assert.are.equal(im:format(), im2:format()) 133 | assert.are.equal(im:xres(), im2:xres()) 134 | assert.are.equal(im:yres(), im2:yres()) 135 | end) 136 | 137 | it("can load a jpeg from a buffer, options in a table", function() 138 | local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg", 139 | { shrink = 2 }) 140 | local f = io.open("./spec/images/Gugg_coloured.jpg", "rb") 141 | local buf = f:read("*all") 142 | f:close() 143 | local im2 = vips.Image.new_from_buffer(buf, "shrink=2") 144 | 145 | assert.are.equal(im:width(), im2:width()) 146 | assert.are.equal(im:height(), im2:height()) 147 | assert.are.equal(im:format(), im2:format()) 148 | assert.are.equal(im:xres(), im2:xres()) 149 | assert.are.equal(im:yres(), im2:yres()) 150 | end) 151 | end) 152 | 153 | describe("test image from memory", function() 154 | it("can make an image from a memory area", function() 155 | local width = 64 156 | local height = 32 157 | local size = width * height 158 | local data = ffi.new("unsigned char[?]", size) 159 | 160 | for y = 0, height - 1 do 161 | for x = 0, width - 1 do 162 | data[x + y * width] = x + y 163 | end 164 | end 165 | 166 | local im = vips.Image.new_from_memory(data, 167 | width, height, 1, "uchar") 168 | 169 | assert.are.equal(im:width(), width) 170 | assert.are.equal(im:height(), height) 171 | assert.are.equal(im:bands(), 1) 172 | assert.are.equal(im:format(), "uchar") 173 | assert.are.equal(im:avg(), 47) 174 | end) 175 | 176 | it("can make an image from a memory area (pointer)", function() 177 | local width = 64 178 | local height = 32 179 | local size = width * height 180 | local data = ffi.gc(ffi.cast("unsigned char*", ffi.C.malloc(size)), ffi.C.free) 181 | 182 | for y = 0, height - 1 do 183 | for x = 0, width - 1 do 184 | data[x + y * width] = x + y 185 | end 186 | end 187 | 188 | local im = vips.Image.new_from_memory_ptr(data, 189 | size, width, height, 1, "uchar") 190 | 191 | assert.are.equal(im:width(), width) 192 | assert.are.equal(im:height(), height) 193 | assert.are.equal(im:bands(), 1) 194 | assert.are.equal(im:format(), "uchar") 195 | assert.are.equal(im:avg(), 47) 196 | end) 197 | end) 198 | 199 | describe("test vips creators", function() 200 | it("can call vips_black()", function() 201 | local im = vips.Image.black(1, 1) 202 | 203 | assert.are.equal(im:width(), 1) 204 | assert.are.equal(im:height(), 1) 205 | assert.are.equal(im:bands(), 1) 206 | assert.are.equal(im:avg(), 0) 207 | end) 208 | 209 | it("can call operations with - in option names", function() 210 | local im = vips.Image.perlin(100, 100, { cell_size = 10 }) 211 | 212 | assert.are.equal(im:width(), 100) 213 | assert.are.equal(im:height(), 100) 214 | assert.are.equal(im:bands(), 1) 215 | end) 216 | end) 217 | 218 | describe("test image from image", function() 219 | local im 220 | 221 | setup(function() 222 | im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") 223 | end) 224 | 225 | it("can make a one-band constant image", function() 226 | local im2 = im:new_from_image(12) 227 | 228 | assert.are.equal(im:width(), im2:width()) 229 | assert.are.equal(im:height(), im2:height()) 230 | assert.are.equal(im:format(), im2:format()) 231 | assert.are.equal(im:interpretation(), im2:interpretation()) 232 | assert.are.equal(im:xres(), im2:xres()) 233 | assert.are.equal(im:yres(), im2:yres()) 234 | assert.are.equal(im2:bands(), 1) 235 | assert.are.equal(im2:avg(), 12) 236 | end) 237 | 238 | it("can make a many-band constant image", function() 239 | local im2 = im:new_from_image({ 1, 2, 3, 4 }) 240 | 241 | assert.are.equal(im:width(), im2:width()) 242 | assert.are.equal(im:height(), im2:height()) 243 | assert.are.equal(im:format(), im2:format()) 244 | assert.are.equal(im:interpretation(), im2:interpretation()) 245 | assert.are.equal(im:xres(), im2:xres()) 246 | assert.are.equal(im:yres(), im2:yres()) 247 | assert.are.equal(im2:bands(), 4) 248 | assert.are.equal(im2:avg(), 2.5) 249 | end) 250 | end) 251 | end) 252 | -------------------------------------------------------------------------------- /src/vips/voperation.lua: -------------------------------------------------------------------------------- 1 | -- manage VipsOperation 2 | -- lookup and call operations 3 | 4 | local ffi = require "ffi" 5 | 6 | local verror = require "vips.verror" 7 | local version = require "vips.version" 8 | local log = require "vips.log" 9 | local gvalue = require "vips.gvalue" 10 | local vobject = require "vips.vobject" 11 | local Image = require "vips.Image" 12 | 13 | local type = type 14 | local error = error 15 | local pairs = pairs 16 | local unpack = unpack or table.unpack 17 | local tonumber = tonumber 18 | local str_gsub = string.gsub 19 | 20 | local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") 21 | 22 | local REQUIRED = 1 23 | local CONSTRUCT = 2 -- luacheck: ignore 24 | local SET_ONCE = 4 -- luacheck: ignore 25 | local SET_ALWAYS = 8 -- luacheck: ignore 26 | local INPUT = 16 27 | local OUTPUT = 32 28 | local DEPRECATED = 64 29 | local MODIFY = 128 30 | 31 | local function isbitset(a, b) 32 | return ( (a - (a % b)) / b ) % 2 == 1 33 | end 34 | 35 | -- find the first image, and recurse 36 | local function find_first_image(array, length) 37 | length = length or #array 38 | 39 | for i = 1, length do 40 | if Image.is_Image(array[i]) then 41 | return array[i] 42 | elseif type(array[i]) == "table" then 43 | local result = find_first_image(array[i]) 44 | 45 | if result then 46 | return result 47 | end 48 | end 49 | end 50 | 51 | return nil 52 | end 53 | 54 | local voperation = {} 55 | 56 | voperation.argumentmap_typeof = ffi.typeof("VipsArgumentMapFn") 57 | voperation.pstring_array_typeof = ffi.typeof("const char**[1]") 58 | voperation.pint_array_typeof = ffi.typeof("int*[1]") 59 | voperation.pint_typeof = ffi.typeof("int[1]") 60 | 61 | -- cast to a vobject ... this will create a new cdata object, but won't 62 | -- change any VipsObject reference counts, nor add a finalizer 63 | -- TODO: Could we use `self.parent_instance` here? 64 | voperation.vobject = function(self) 65 | return ffi.cast(vobject.typeof, self) 66 | end 67 | 68 | -- but for new() we can't do self:vobject():new() since that would 69 | -- attach the unref callback to the cdata object made by the vobject() 70 | -- cast, not to this voperation 71 | voperation.new = function(self) 72 | return vobject.new(self) 73 | end 74 | 75 | voperation.set = function(self, name, flags, match_image, value) 76 | local vob = self:vobject() 77 | local gtype = vob:get_typeof(name) 78 | local gtype_comp = gvalue.comparable_type(gtype) 79 | 80 | -- if the object wants an image and we have a constant, imageize it 81 | -- 82 | -- if the object wants an image array, imageize any constants in the 83 | -- array 84 | if match_image then 85 | if gtype_comp == gvalue.image_type then 86 | value = match_image:imageize(value) 87 | elseif gtype_comp == gvalue.array_image_type then 88 | for i = 1, #value do 89 | value[i] = match_image:imageize(value[i]) 90 | end 91 | end 92 | end 93 | 94 | -- MODIFY args need to be copied before they are set 95 | if isbitset(flags, MODIFY) then 96 | log.msg("copying MODIFY arg", name) 97 | -- make sure we have a unique copy 98 | value = value:copy():copy_memory() 99 | end 100 | 101 | return vob:set_type(name, value, gtype) 102 | end 103 | 104 | -- this is slow ... call as little as possible 105 | voperation.getargs = function(self) 106 | local names = {} 107 | local flags = {} 108 | local n_args = 0 109 | 110 | if version.at_least(8, 7) then 111 | local p_names = ffi.new(voperation.pstring_array_typeof) 112 | local p_flags = ffi.new(voperation.pint_array_typeof) 113 | local p_n_args = ffi.new(voperation.pint_typeof) 114 | 115 | vips_lib.vips_object_get_args(self, p_names, p_flags, p_n_args) 116 | 117 | p_names = p_names[0] 118 | p_flags = p_flags[0] 119 | n_args = p_n_args[0] 120 | 121 | -- C-array is numbered from zero 122 | for i = 0, n_args - 1 do 123 | names[i + 1] = str_gsub(ffi.string(p_names[i]), "-", "_") 124 | flags[i + 1] = p_flags[i] 125 | end 126 | else 127 | local cb = ffi.cast(voperation.argumentmap_typeof, 128 | function(_, pspec, argument_class, _, _, _) 129 | n_args = n_args + 1 130 | 131 | -- libvips uses "-" to separate parts of arg names, but we 132 | -- need "_" for lua 133 | names[n_args] = str_gsub(ffi.string(pspec.name), "-", "_") 134 | flags[n_args] = tonumber(argument_class.flags) 135 | end) 136 | vips_lib.vips_argument_map(self, cb, nil, nil) 137 | cb:free() 138 | end 139 | 140 | return names, flags, n_args 141 | end 142 | 143 | -- string_options is any optional args coded as a string, perhaps 144 | -- "[strip,tile=true]" 145 | voperation.call = function(name, string_options, ...) 146 | local call_args = { ... } 147 | 148 | local vop = vips_lib.vips_operation_new(name) 149 | if vop == ffi.NULL then 150 | error("no such operation\n" .. verror.get()) 151 | end 152 | vop = vop:new() 153 | 154 | local names, flags, arguments_length = vop:getargs() 155 | 156 | -- cache the call args length 157 | local call_args_length = #call_args 158 | 159 | log.msg("calling operation:", name) 160 | log.msg("passed:") 161 | log.msg_r(call_args) 162 | 163 | -- make a thing to quickly get flags from an arg name 164 | local flags_from_name = {} 165 | 166 | -- count required input args 167 | local n_required = 0 168 | for i = 1, arguments_length do 169 | local flag = flags[i] 170 | flags_from_name[names[i]] = flag 171 | 172 | if isbitset(flag, INPUT) and 173 | isbitset(flag, REQUIRED) and 174 | not isbitset(flag, DEPRECATED) then 175 | n_required = n_required + 1 176 | end 177 | end 178 | 179 | -- so we should have been passed n_required, or n_required + 1 if 180 | -- there's a table of options at the end 181 | local last_arg 182 | if call_args_length == n_required then 183 | last_arg = nil 184 | elseif call_args_length == n_required + 1 then 185 | last_arg = call_args[#call_args] 186 | if type(last_arg) ~= "table" then 187 | error("unable to call " .. name .. ": " .. call_args_length .. 188 | " arguments given, " .. n_required .. 189 | ", but final argument is not a table") 190 | end 191 | else 192 | error("unable to call " .. name .. ": " .. call_args_length .. 193 | " arguments given, but " .. n_required .. " required") 194 | end 195 | 196 | -- the first image argument is the thing we expand constants to 197 | -- match ... look inside tables for images, since we may be passing 198 | -- an array of image as a single param 199 | local match_image = find_first_image(call_args, call_args_length) 200 | 201 | -- set any string options before any args so they can't be 202 | -- overridden 203 | if vips_lib.vips_object_set_from_string(vop:vobject(), 204 | string_options) ~= 0 then 205 | error("unable to call " .. name .. "\n" .. verror.get()) 206 | end 207 | 208 | local n = 0 209 | for i = 1, arguments_length do 210 | local flag = flags[i] 211 | 212 | if isbitset(flag, INPUT) and 213 | isbitset(flag, REQUIRED) and 214 | not isbitset(flag, DEPRECATED) then 215 | n = n + 1 216 | 217 | if not vop:set(names[i], flag, 218 | match_image, call_args[n]) then 219 | error("unable to call " .. name .. "\n" .. verror.get()) 220 | end 221 | end 222 | end 223 | 224 | if last_arg then 225 | for k, v in pairs(last_arg) do 226 | local flag = flags_from_name[k] 227 | if not flag then 228 | error("unable to call " .. name .. ": invalid flag '" .. 229 | tostring(k) .. "'") 230 | end 231 | 232 | if not vop:set(k, flag, match_image, v) then 233 | error("unable to call " .. name .. "\n" .. verror.get()) 234 | end 235 | end 236 | end 237 | 238 | local vop2 = vips_lib.vips_cache_operation_build(vop) 239 | if vop2 == ffi.NULL then 240 | error("unable to call " .. name .. "\n" .. verror.get()) 241 | end 242 | vop = vop2:new() 243 | 244 | local result = {} 245 | local vob = vop:vobject() 246 | 247 | -- fetch required output args, plus modified input images 248 | n = 1 249 | for i = 1, arguments_length do 250 | local flag = flags[i] 251 | 252 | if isbitset(flag, OUTPUT) and 253 | isbitset(flag, REQUIRED) and 254 | not isbitset(flag, DEPRECATED) then 255 | result[n] = vob:get(names[i]) 256 | n = n + 1 257 | end 258 | 259 | -- MODIFY input args are returned .. this will get the copy we 260 | -- made above 261 | if isbitset(flag, INPUT) and 262 | isbitset(flag, MODIFY) then 263 | result[n] = vob:get(names[i]) 264 | n = n + 1 265 | end 266 | end 267 | 268 | -- fetch optional output args 269 | for i = 1, arguments_length do 270 | local flag = flags[i] 271 | 272 | if isbitset(flag, OUTPUT) and 273 | not isbitset(flag, REQUIRED) and 274 | not isbitset(flag, DEPRECATED) then 275 | result[n] = vob:get(names[i]) 276 | n = n + 1 277 | end 278 | end 279 | 280 | -- garbage collection during vips_object_unref_outputs leads to crashes on Lua 5.3 281 | collectgarbage("stop") 282 | vips_lib.vips_object_unref_outputs(vop) 283 | collectgarbage("restart") 284 | 285 | -- this strange if expression is because unpack 286 | -- has not yet been implemented in the JIT compiler 287 | -- of LuaJIT, see: http://wiki.luajit.org/NYI. 288 | if n == 1 then 289 | return nil 290 | elseif n == 2 then 291 | return result[1] 292 | else 293 | -- we could extend this if expression even more, 294 | -- but usually one item is returned. 295 | return unpack(result) 296 | end 297 | end 298 | 299 | return ffi.metatype("VipsOperation", { 300 | __index = voperation 301 | }) 302 | -------------------------------------------------------------------------------- /src/vips/gvalue.lua: -------------------------------------------------------------------------------- 1 | -- manipulate GValue objects from lua 2 | -- pull in gobject via the vips library 3 | 4 | local ffi = require "ffi" 5 | 6 | local verror = require "vips.verror" 7 | local version = require "vips.version" 8 | local Image = require "vips.Image" 9 | 10 | local type = type 11 | local error = error 12 | 13 | local vips_lib 14 | local gobject_lib 15 | local glib_lib 16 | if ffi.os == "Windows" then 17 | vips_lib = ffi.load("libvips-42.dll") 18 | gobject_lib = ffi.load("libgobject-2.0-0.dll") 19 | glib_lib = ffi.load("libglib-2.0-0.dll") 20 | else 21 | vips_lib = ffi.load("vips") 22 | gobject_lib = vips_lib 23 | glib_lib = vips_lib 24 | end 25 | 26 | if version.at_least(8, 6) then 27 | vips_lib.vips_blend_mode_get_type() 28 | end 29 | vips_lib.vips_band_format_get_type() 30 | 31 | local gvalue = {} 32 | 33 | -- make ffi constructors we can reuse 34 | gvalue.gv_typeof = ffi.typeof("GValue") 35 | gvalue.pgv_typeof = ffi.typeof("GValue[1]") 36 | gvalue.image_typeof = ffi.typeof("VipsImage*") 37 | gvalue.pint_typeof = ffi.typeof("int[?]") 38 | gvalue.int_arr_typeof = ffi.typeof("const int[?]") 39 | gvalue.double_arr_typeof = ffi.typeof("const double[?]") 40 | gvalue.psize_typeof = ffi.typeof("size_t[?]") 41 | gvalue.mem_typeof = ffi.typeof("unsigned char[?]") 42 | gvalue.interpolate_typeof = ffi.typeof("VipsInterpolate*") 43 | gvalue.connection_typeof = ffi.typeof("VipsConnection*") 44 | gvalue.source_typeof = ffi.typeof("VipsSource*") 45 | gvalue.target_typeof = ffi.typeof("VipsTarget*") 46 | 47 | -- look up some common gtypes at init for speed 48 | gvalue.gbool_type = gobject_lib.g_type_from_name("gboolean") 49 | gvalue.gint_type = gobject_lib.g_type_from_name("gint") 50 | gvalue.gdouble_type = gobject_lib.g_type_from_name("gdouble") 51 | gvalue.gstr_type = gobject_lib.g_type_from_name("gchararray") 52 | gvalue.genum_type = gobject_lib.g_type_from_name("GEnum") 53 | gvalue.gflags_type = gobject_lib.g_type_from_name("GFlags") 54 | gvalue.image_type = gobject_lib.g_type_from_name("VipsImage") 55 | gvalue.array_int_type = gobject_lib.g_type_from_name("VipsArrayInt") 56 | gvalue.array_double_type = gobject_lib.g_type_from_name("VipsArrayDouble") 57 | gvalue.array_image_type = gobject_lib.g_type_from_name("VipsArrayImage") 58 | gvalue.refstr_type = gobject_lib.g_type_from_name("VipsRefString") 59 | gvalue.blob_type = gobject_lib.g_type_from_name("VipsBlob") 60 | gvalue.band_format_type = gobject_lib.g_type_from_name("VipsBandFormat") 61 | gvalue.blend_mode_type = version.at_least(8, 6) and gobject_lib.g_type_from_name("VipsBlendMode") or 0 62 | gvalue.interpolate_type = gobject_lib.g_type_from_name("VipsInterpolate") 63 | gvalue.connection_type = gobject_lib.g_type_from_name("VipsConnection") 64 | gvalue.source_type = gobject_lib.g_type_from_name("VipsSource") 65 | gvalue.target_type = gobject_lib.g_type_from_name("VipsTarget") 66 | 67 | -- gvalue.*_type can be of type cdata or number depending on the OS and Lua version 68 | -- gtypes as returned by vips_lib can also be of type cdata or number 69 | -- cdata and number are not comparable with Standard Lua (using luaffi-tkl) 70 | gvalue.comparable_type = type(gvalue.gdouble_type) == "number" and 71 | function(gtype) return tonumber(gtype) end or 72 | function(gtype) return gtype end 73 | gvalue.to_enum = function(gtype, value) 74 | -- turn a string into an int, ready to be passed into libvips 75 | local enum_value 76 | 77 | if type(value) == "string" then 78 | enum_value = vips_lib.vips_enum_from_nick("lua-vips", 79 | gtype, value) 80 | 81 | if enum_value < 0 then 82 | error("no such enum " .. value .. "\n" .. verror.get()) 83 | end 84 | else 85 | enum_value = value 86 | end 87 | 88 | return enum_value 89 | end 90 | 91 | gvalue.type_name = function(gtype) 92 | return ffi.string(gobject_lib.g_type_name(gtype)) 93 | end 94 | 95 | gvalue.init = function(gv, gtype) 96 | gobject_lib.g_value_init(gv, gtype) 97 | end 98 | 99 | gvalue.set = function(gv, value) 100 | local gtype = gv.gtype 101 | local gtype_comp = gvalue.comparable_type(gtype) 102 | local fundamental = gobject_lib.g_type_fundamental(gtype) 103 | 104 | if gtype_comp == gvalue.gbool_type then 105 | gobject_lib.g_value_set_boolean(gv, value) 106 | elseif gtype_comp == gvalue.gint_type then 107 | gobject_lib.g_value_set_int(gv, value) 108 | elseif gtype_comp == gvalue.gdouble_type then 109 | gobject_lib.g_value_set_double(gv, value) 110 | elseif fundamental == gvalue.genum_type then 111 | gobject_lib.g_value_set_enum(gv, gvalue.to_enum(gtype, value)) 112 | elseif fundamental == gvalue.gflags_type then 113 | gobject_lib.g_value_set_flags(gv, value) 114 | elseif gtype_comp == gvalue.gstr_type then 115 | gobject_lib.g_value_set_string(gv, value) 116 | elseif gtype_comp == gvalue.refstr_type then 117 | gobject_lib.vips_value_set_ref_string(gv, value) 118 | elseif gtype_comp == gvalue.image_type then 119 | gobject_lib.g_value_set_object(gv, value.vimage) 120 | elseif gtype_comp == gvalue.array_int_type then 121 | if type(value) == "number" then 122 | value = { value } 123 | end 124 | 125 | local n = #value 126 | local a = ffi.new(gvalue.int_arr_typeof, n, value) 127 | 128 | vips_lib.vips_value_set_array_int(gv, a, n) 129 | elseif gtype_comp == gvalue.array_double_type then 130 | if type(value) == "number" then 131 | value = { value } 132 | end 133 | 134 | local n = #value 135 | local a = ffi.new(gvalue.double_arr_typeof, n, value) 136 | 137 | vips_lib.vips_value_set_array_double(gv, a, n) 138 | elseif gtype_comp == gvalue.array_image_type then 139 | if Image.is_Image(value) then 140 | value = { value } 141 | end 142 | 143 | local n = #value 144 | 145 | vips_lib.vips_value_set_array_image(gv, n) 146 | local a = vips_lib.vips_value_get_array_image(gv, nil) 147 | 148 | for i = 0, n - 1 do 149 | a[i] = value[i + 1].vimage 150 | -- the gvalue needs a set of refs to own 151 | gobject_lib.g_object_ref(a[i]) 152 | end 153 | elseif gtype_comp == gvalue.blob_type then 154 | -- we need to set the blob to a copy of the lua string that vips 155 | -- can own 156 | local n = #value 157 | 158 | local buf = glib_lib.g_malloc(n) 159 | ffi.copy(buf, value, n) 160 | 161 | if version.at_least(8, 6) then 162 | vips_lib.vips_value_set_blob_free(gv, buf, n) 163 | else 164 | vips_lib.vips_value_set_blob(gv, glib_lib.g_free, buf, n) 165 | end 166 | elseif gtype_comp == gvalue.interpolate_type then 167 | gobject_lib.g_value_set_object(gv, value) 168 | elseif gtype_comp == gvalue.connection_type then 169 | gobject_lib.g_value_set_object(gv, value) 170 | elseif gtype_comp == gvalue.source_type then 171 | gobject_lib.g_value_set_object(gv, value) 172 | elseif gtype_comp == gvalue.target_type then 173 | gobject_lib.g_value_set_object(gv, value) 174 | else 175 | error("unsupported gtype for set " .. gvalue.type_name(gtype)) 176 | end 177 | end 178 | 179 | gvalue.get = function(gv) 180 | local gtype = gv.gtype 181 | local gtype_comp = gvalue.comparable_type(gtype) 182 | local fundamental = gobject_lib.g_type_fundamental(gtype) 183 | 184 | local result 185 | 186 | if gtype_comp == gvalue.gbool_type then 187 | result = gobject_lib.g_value_get_boolean(gv) 188 | elseif gtype_comp == gvalue.gint_type then 189 | result = gobject_lib.g_value_get_int(gv) 190 | elseif gtype_comp == gvalue.gdouble_type then 191 | result = gobject_lib.g_value_get_double(gv) 192 | elseif fundamental == gvalue.genum_type then 193 | local enum_value = gobject_lib.g_value_get_enum(gv) 194 | 195 | local cstr = vips_lib.vips_enum_nick(gtype, enum_value) 196 | 197 | if cstr == ffi.NULL then 198 | error("value not in enum") 199 | end 200 | 201 | result = ffi.string(cstr) 202 | elseif fundamental == gvalue.gflags_type then 203 | result = gobject_lib.g_value_get_flags(gv) 204 | elseif gtype_comp == gvalue.gstr_type then 205 | local cstr = gobject_lib.g_value_get_string(gv) 206 | 207 | if cstr ~= ffi.NULL then 208 | result = ffi.string(cstr) 209 | else 210 | result = nil 211 | end 212 | elseif gtype_comp == gvalue.refstr_type then 213 | local psize = ffi.new(gvalue.psize_typeof, 1) 214 | 215 | local cstr = vips_lib.vips_value_get_ref_string(gv, psize) 216 | 217 | result = ffi.string(cstr, tonumber(psize[0])) 218 | elseif gtype_comp == gvalue.image_type then 219 | -- g_value_get_object() will not add a ref ... that is 220 | -- held by the gvalue 221 | local vo = gobject_lib.g_value_get_object(gv) 222 | local vimage = ffi.cast(gvalue.image_typeof, vo) 223 | 224 | -- we want a ref that will last with the life of the vimage: 225 | -- this ref is matched by the unref that's attached to finalize 226 | -- by Image.new() 227 | gobject_lib.g_object_ref(vimage) 228 | 229 | result = Image.new(vimage) 230 | elseif gtype_comp == gvalue.array_int_type then 231 | local pint = ffi.new(gvalue.pint_typeof, 1) 232 | 233 | local array = vips_lib.vips_value_get_array_int(gv, pint) 234 | result = {} 235 | for i = 0, pint[0] - 1 do 236 | result[i + 1] = array[i] 237 | end 238 | 239 | elseif gtype_comp == gvalue.array_double_type then 240 | local pint = ffi.new(gvalue.pint_typeof, 1) 241 | 242 | local array = vips_lib.vips_value_get_array_double(gv, pint) 243 | result = {} 244 | for i = 0, pint[0] - 1 do 245 | result[i + 1] = array[i] 246 | end 247 | elseif gtype_comp == gvalue.array_image_type then 248 | local pint = ffi.new(gvalue.pint_typeof, 1) 249 | 250 | local array = vips_lib.vips_value_get_array_image(gv, pint) 251 | result = {} 252 | for i = 0, pint[0] - 1 do 253 | -- this will make a new cdata object 254 | local vimage = array[i] 255 | 256 | -- vips_value_get_array_image() adds a ref for each image in 257 | -- the array ... we must remember to drop them 258 | gobject_lib.g_object_ref(vimage) 259 | 260 | result[i + 1] = Image.new(vimage) 261 | end 262 | elseif gtype_comp == gvalue.blob_type then 263 | local psize = ffi.new(gvalue.psize_typeof, 1) 264 | 265 | local array = vips_lib.vips_value_get_blob(gv, psize) 266 | 267 | result = ffi.string(array, tonumber(psize[0])) 268 | elseif gtype_comp == gvalue.interpolate_type then 269 | local vo = gobject_lib.g_value_get_object(gv) 270 | result = ffi.cast(gvalue.interpolate_typeof, vo) 271 | elseif gtype_comp == gvalue.connection_type then 272 | local vo = gobject_lib.g_value_get_object(gv) 273 | result = ffi.cast(gvalue.connection_typeof, vo) 274 | elseif gtype_comp == gvalue.source_type then 275 | local vo = gobject_lib.g_value_get_object(gv) 276 | result = ffi.cast(gvalue.source_typeof, vo) 277 | elseif gtype_comp == gvalue.target_type then 278 | local vo = gobject_lib.g_value_get_object(gv) 279 | result = ffi.cast(gvalue.target_typeof, vo) 280 | else 281 | error("unsupported gtype for get " .. gvalue.type_name(gtype)) 282 | end 283 | 284 | return result 285 | end 286 | 287 | return ffi.metatype("GValue", { 288 | __new = function(ct, pt) 289 | -- if pt equals to true you'll need to 290 | -- g_value_unset() yourself after calling it, 291 | -- it won't unset() automatically! 292 | return pt and ffi.new(gvalue.pgv_typeof) or ffi.new(ct) 293 | end, 294 | 295 | __gc = function(gv) 296 | gobject_lib.g_value_unset(gv) 297 | end, 298 | 299 | __index = gvalue 300 | }) 301 | -------------------------------------------------------------------------------- /src/vips/cdefs.lua: -------------------------------------------------------------------------------- 1 | -- all the C declarations for lua-vips 2 | 3 | local ffi = require "ffi" 4 | 5 | -- GType is an int the size of a pointer ... I don't think we can just use 6 | -- size_t, sadly 7 | if ffi.arch == "x64" or ffi.arch == "arm64" then 8 | ffi.cdef [[ 9 | typedef uint64_t GType; 10 | ]] 11 | else 12 | ffi.cdef [[ 13 | typedef uint32_t GType; 14 | ]] 15 | end 16 | 17 | ffi.cdef [[ 18 | typedef struct _GValue { 19 | GType gtype; 20 | uint64_t data[2]; 21 | } GValue; 22 | 23 | void *g_malloc (size_t size); 24 | void g_free (void *data); 25 | 26 | void g_object_ref (void *object); 27 | void g_object_unref (void *object); 28 | 29 | void g_value_init (GValue *value, GType gtype); 30 | void g_value_unset (GValue *value); 31 | const char *g_type_name (GType gtype); 32 | GType g_type_from_name (const char *name); 33 | GType g_type_fundamental (GType gtype); 34 | 35 | GType vips_blend_mode_get_type (void); 36 | GType vips_band_format_get_type (void); 37 | 38 | GType vips_type_find (const char *basename, const char *nickname); 39 | 40 | int vips_enum_from_nick (const char *domain, 41 | GType gtype, const char *str); 42 | const char *vips_enum_nick (GType gtype, int value); 43 | 44 | void g_value_set_boolean (GValue *value, int v_boolean); 45 | void g_value_set_int (GValue *value, int i); 46 | void g_value_set_double (GValue *value, double d); 47 | void g_value_set_enum (GValue *value, int e); 48 | void g_value_set_flags (GValue *value, unsigned int f); 49 | void g_value_set_string (GValue *value, const char *str); 50 | void vips_value_set_ref_string (GValue *value, const char *str); 51 | void g_value_set_object (GValue *value, void *object); 52 | void vips_value_set_array_double (GValue *value, 53 | const double *array, int n); 54 | void vips_value_set_array_int (GValue *value, 55 | const int *array, int n); 56 | void vips_value_set_array_image (GValue *value, int n); 57 | void vips_value_set_blob (GValue *value, 58 | void (*free_fn)(void *data), void *data, size_t length); 59 | void vips_value_set_blob_free (GValue *value, 60 | void *data, size_t length); 61 | 62 | int g_value_get_boolean (const GValue *value); 63 | int g_value_get_int (const GValue *value); 64 | double g_value_get_double (const GValue *value); 65 | int g_value_get_enum (const GValue *value); 66 | unsigned int g_value_get_flags (const GValue *value); 67 | const char *g_value_get_string (const GValue *value); 68 | const char *vips_value_get_ref_string (const GValue *value, size_t *length); 69 | void *g_value_get_object (const GValue *value); 70 | double *vips_value_get_array_double (const GValue *value, int *n); 71 | int *vips_value_get_array_int (const GValue *value, int *n); 72 | void *vips_value_get_blob (const GValue *value, size_t *length); 73 | 74 | typedef struct _GObject { 75 | void *g_type_instance; 76 | unsigned int ref_count; 77 | void *qdata; 78 | } GObject; 79 | 80 | typedef struct _VipsObject { 81 | GObject parent_instance; 82 | bool constructed; 83 | bool static_object; 84 | void *argument_table; 85 | char *nickname; 86 | char *description; 87 | bool preclose; 88 | bool close; 89 | bool postclose; 90 | size_t local_memory; 91 | } VipsObject; 92 | 93 | typedef struct _VipsObjectClass { 94 | // opaque 95 | } VipsObjectClass; 96 | 97 | typedef enum { 98 | G_PARAM_READABLE = 1, 99 | G_PARAM_WRITABLE = 2, 100 | G_PARAM_CONSTRUCT = 4, 101 | G_PARAM_CONSTRUCT_ONLY = 8, 102 | G_PARAM_LAX_VALIDATION = 16, 103 | G_PARAM_STATIC_NAME = 32, 104 | G_PARAM_PRIVATE = G_PARAM_STATIC_NAME, 105 | G_PARAM_STATIC_NICK = 64, 106 | G_PARAM_STATIC_BLURB = 128 107 | } GParamFlags; 108 | 109 | typedef struct _GParamSpec { 110 | void *g_type_instance; 111 | 112 | const char *name; 113 | GParamFlags flags; 114 | GType value_type; 115 | GType owner_type; 116 | 117 | // rest opaque 118 | } GParamSpec; 119 | 120 | typedef struct _VipsArgument { 121 | GParamSpec *pspec; 122 | } VipsArgument; 123 | 124 | typedef struct _VipsArgumentInstance { 125 | VipsArgument parent; 126 | 127 | // opaque 128 | } VipsArgumentInstance; 129 | 130 | typedef enum _VipsArgumentFlags { 131 | VIPS_ARGUMENT_NONE = 0, 132 | VIPS_ARGUMENT_REQUIRED = 1, 133 | VIPS_ARGUMENT_CONSTRUCT = 2, 134 | VIPS_ARGUMENT_SET_ONCE = 4, 135 | VIPS_ARGUMENT_SET_ALWAYS = 8, 136 | VIPS_ARGUMENT_INPUT = 16, 137 | VIPS_ARGUMENT_OUTPUT = 32, 138 | VIPS_ARGUMENT_DEPRECATED = 64, 139 | VIPS_ARGUMENT_MODIFY = 128 140 | } VipsArgumentFlags; 141 | 142 | typedef struct _VipsArgumentClass { 143 | VipsArgument parent; 144 | 145 | VipsObjectClass *object_class; 146 | VipsArgumentFlags flags; 147 | int priority; 148 | uint64_t offset; 149 | } VipsArgumentClass; 150 | 151 | int vips_object_get_argument (VipsObject *object, 152 | const char *name, GParamSpec **pspec, 153 | VipsArgumentClass **argument_class, 154 | VipsArgumentInstance **argument_instance); 155 | 156 | void g_object_set_property (VipsObject *object, 157 | const char *name, const GValue *value); 158 | void g_object_get_property (VipsObject *object, 159 | const char *name, GValue *value); 160 | 161 | void vips_object_print_all (void); 162 | 163 | int vips_object_set_from_string (VipsObject *object, const char *options); 164 | 165 | typedef struct _VipsImage { 166 | VipsObject parent_instance; 167 | 168 | // opaque 169 | } VipsImage; 170 | 171 | VipsImage *vips_image_get_gainmap(VipsImage *image); 172 | 173 | typedef struct _VipsConnection { 174 | VipsObject parent_instance; 175 | 176 | // opaque 177 | } VipsConnection; 178 | 179 | const char *vips_connection_filename (VipsConnection *connection); 180 | const char *vips_connection_nick (VipsConnection *connection); 181 | 182 | typedef struct _VipsSource { 183 | VipsConnection parent_instance; 184 | 185 | // opaque 186 | } VipsSource; 187 | 188 | typedef struct _VipsTarget { 189 | VipsConnection parent_instance; 190 | 191 | // opaque 192 | } VipsTarget; 193 | 194 | VipsSource *vips_source_new_from_descriptor (int descriptor); 195 | VipsSource *vips_source_new_from_file (const char *filename); 196 | // VipsSource *vips_source_new_from_blob (VipsBlob *blob); 197 | // VipsSource *vips_source_new_from_target (VipsTarget *target); 198 | VipsSource *vips_source_new_from_memory (const void *data, size_t size); 199 | // VipsSource *vips_source_new_from_options (const char *options); 200 | // void vips_source_minimise (VipsSource *source); 201 | // int vips_source_decode (VipsSource *source); 202 | // gint64 vips_source_read (VipsSource *source, void *data, size_t length); 203 | // gboolean vips_source_is_mappable (VipsSource *source); 204 | // gboolean vips_source_is_file (VipsSource *source); 205 | // const void *vips_source_map (VipsSource *source, size_t *length); 206 | // VipsBlob *vips_source_map_blob (VipsSource *source); 207 | // gint64 vips_source_seek (VipsSource *source, gint64 offset, int whence); 208 | // int vips_source_rewind (VipsSource *source); 209 | // gint64 vips_source_sniff_at_most (VipsSource *source, unsigned char **data, size_t length); 210 | // unsigned char *vips_source_sniff (VipsSource *source, size_t length); 211 | // gint64 vips_source_length (VipsSource *source); 212 | // VipsSourceCustom *vips_source_custom_new (void); 213 | // GInputStream *vips_g_input_stream_new_from_source (VipsSource *source); 214 | // VipsSourceGInputStream *vips_source_g_input_stream_new (GInputStream *stream); 215 | 216 | VipsTarget *vips_target_new_to_descriptor (int descriptor); 217 | VipsTarget *vips_target_new_to_file (const char *filename); 218 | VipsTarget *vips_target_new_to_memory (void); 219 | // VipsTarget *vips_target_new_temp (VipsTarget *target); 220 | // int vips_target_write (VipsTarget *target, const void *data, size_t length); 221 | // gint64 vips_target_read (VipsTarget *target, void *buffer, size_t length); 222 | // gint64 vips_target_seek (VipsTarget *target, gint64 offset, int whence); 223 | // int vips_target_end (VipsTarget *target); 224 | // unsigned char *vips_target_steal (VipsTarget *target, size_t *length); 225 | // char *vips_target_steal_text (VipsTarget *target); 226 | // int vips_target_putc (VipsTarget *target, int ch); 227 | // int vips_target_writes (VipsTarget *target, const char *str); 228 | // int vips_target_writef (VipsTarget *target, const char *fmt, ...); 229 | // int vips_target_write_amp (VipsTarget *target, const char *str); 230 | // VipsTargetCustom *vips_target_custom_new (void); 231 | 232 | const char *vips_foreign_find_load (const char *name); 233 | const char *vips_foreign_find_load_buffer (const void *data, size_t size); 234 | const char *vips_foreign_find_save (const char *name); 235 | const char *vips_foreign_find_save_buffer (const char *suffix); 236 | const char* vips_foreign_find_load_source (VipsSource *source); 237 | const char* vips_foreign_find_save_target (const char* suffix); 238 | 239 | VipsImage *vips_image_new_matrix_from_array (int width, int height, 240 | const double *array, int size); 241 | 242 | VipsImage *vips_image_new_from_memory (const void *data, size_t size, 243 | int width, int height, int bands, int format); 244 | unsigned char *vips_image_write_to_memory (VipsImage *image, 245 | size_t *size_out); 246 | 247 | VipsImage *vips_image_copy_memory (VipsImage *image); 248 | 249 | VipsImage **vips_value_get_array_image (const GValue *value, int *n); 250 | 251 | GType vips_image_get_typeof (const VipsImage *image, const char *name); 252 | int vips_image_get (const VipsImage *image, const char *name, 253 | GValue *value_copy); 254 | void vips_image_set (VipsImage *image, const char *name, GValue *value); 255 | int vips_image_remove (VipsImage *image, const char *name); 256 | 257 | int vips_image_hasalpha(VipsImage *image); 258 | 259 | char *vips_filename_get_filename (const char *vips_filename); 260 | char *vips_filename_get_options (const char *vips_filename); 261 | 262 | typedef struct _VipsOperation { 263 | VipsObject parent_instance; 264 | 265 | // opaque 266 | } VipsOperation; 267 | 268 | typedef struct _VipsInterpolate { 269 | VipsObject parent_instance; 270 | 271 | // opaque 272 | } VipsInterpolate; 273 | 274 | VipsInterpolate *vips_interpolate_new (const char *name); 275 | 276 | VipsOperation *vips_operation_new (const char *name); 277 | 278 | typedef void * (*VipsArgumentMapFn) (VipsOperation *object, 279 | GParamSpec *pspec, 280 | VipsArgumentClass *argument_class, 281 | VipsArgumentInstance *argument_instance, 282 | void *a, void *b); 283 | 284 | void *vips_argument_map (VipsOperation *object, 285 | VipsArgumentMapFn fn, void *a, void *b); 286 | 287 | void vips_object_get_args (VipsOperation *object, 288 | const char ***names, int **flags, int *n_args); 289 | 290 | VipsOperation *vips_cache_operation_build (VipsOperation *operation); 291 | void vips_object_unref_outputs (VipsOperation *operation); 292 | 293 | void vips_leak_set (int leak); 294 | void vips_cache_set_max (int max); 295 | void vips_cache_set_max (int max); 296 | int vips_cache_get_max (void); 297 | void vips_cache_set_max_mem (size_t max_mem); 298 | size_t vips_cache_get_max_mem (void); 299 | void vips_cache_set_max_files (int max_files); 300 | int vips_cache_get_max_files (void); 301 | 302 | void vips_concurrency_set (int concurrency); 303 | int vips_concurrency_get (); 304 | 305 | int vips_init (const char *argv0); 306 | int vips_version (int flag); 307 | 308 | const char *vips_error_buffer (void); 309 | void vips_error_clear (void); 310 | 311 | ]] 312 | -------------------------------------------------------------------------------- /src/vips/Image_methods.lua: -------------------------------------------------------------------------------- 1 | -- an Image class with overloads 2 | 3 | local ffi = require "ffi" 4 | 5 | local verror = require "vips.verror" 6 | local version = require "vips.version" 7 | local gvalue = require "vips.gvalue" 8 | local vobject = require "vips.vobject" 9 | local voperation = require "vips.voperation" 10 | local Image = require "vips.Image" 11 | 12 | local type = type 13 | local error = error 14 | local pairs = pairs 15 | local ipairs = ipairs 16 | local unpack = unpack or table.unpack 17 | local rawget = rawget 18 | local setmetatable = setmetatable 19 | local getmetatable = getmetatable 20 | 21 | local vips_lib 22 | local gobject_lib 23 | local glib_lib 24 | if ffi.os == "Windows" then 25 | vips_lib = ffi.load("libvips-42.dll") 26 | gobject_lib = ffi.load("libgobject-2.0-0.dll") 27 | glib_lib = ffi.load("libglib-2.0-0.dll") 28 | else 29 | vips_lib = ffi.load("vips") 30 | gobject_lib = vips_lib 31 | glib_lib = vips_lib 32 | end 33 | 34 | -- test for rectangular array of something 35 | local function is_2D(table) 36 | if type(table) ~= "table" then 37 | return false 38 | end 39 | 40 | for i = 1, #table do 41 | if type(table[i]) ~= "table" then 42 | return false 43 | end 44 | if #table[i] ~= #table[1] then 45 | return false 46 | end 47 | end 48 | 49 | return true 50 | end 51 | 52 | local function map(fn, array) 53 | local new_array = {} 54 | 55 | for i, v in ipairs(array) do 56 | new_array[i] = fn(v) 57 | end 58 | 59 | return new_array 60 | end 61 | 62 | local function swap_Image_left(left, right) 63 | if Image.is_Image(left) then 64 | return left, right 65 | elseif Image.is_Image(right) then 66 | return right, left 67 | else 68 | error("must have one image argument") 69 | end 70 | end 71 | 72 | -- either a single number, or a table of numbers 73 | local function is_pixel(value) 74 | return type(value) == "number" or 75 | (type(value) == "table" and not Image.is_Image(value)) 76 | end 77 | 78 | local function call_enum(image, other, base, operation) 79 | if type(other) == "number" then 80 | return image[base .. "_const"](image, operation, { other }) 81 | elseif is_pixel(other) then 82 | return image[base .. "_const"](image, operation, other) 83 | else 84 | return image[base](image, other, operation) 85 | end 86 | end 87 | 88 | -- turn a string from libvips that must be g_free()d into a lua string 89 | local function to_string_copy(vips_string) 90 | local lua_string = ffi.string(vips_string) 91 | glib_lib.g_free(vips_string) 92 | return lua_string 93 | end 94 | 95 | -- class methods 96 | 97 | function Image.is_Image(thing) 98 | return type(thing) == "table" and getmetatable(thing) == Image.mt 99 | end 100 | 101 | function Image.imageize(self, value) 102 | -- careful! self can be nil if value is a 2D array 103 | if Image.is_Image(value) then 104 | return value 105 | elseif is_2D(value) then 106 | return Image.new_from_array(value) 107 | else 108 | return self:new_from_image(value) 109 | end 110 | end 111 | 112 | -- constructors 113 | 114 | -- we add an unref finalizer too! be careful 115 | function Image.new(vimage) 116 | local image = {} 117 | 118 | image.vimage = vobject.new(vimage) 119 | 120 | return setmetatable(image, Image.mt) 121 | end 122 | 123 | function Image.find_load(filename) 124 | local name = vips_lib.vips_foreign_find_load(filename) 125 | if name == ffi.NULL then 126 | return nil 127 | else 128 | return ffi.string(name) 129 | end 130 | end 131 | 132 | function Image.new_from_file(vips_filename, ...) 133 | local filename = to_string_copy(vips_lib.vips_filename_get_filename(vips_filename)) 134 | local options = to_string_copy(vips_lib.vips_filename_get_options(vips_filename)) 135 | 136 | local name = Image.find_load(filename) 137 | if name == nil then 138 | error(verror.get()) 139 | end 140 | 141 | return voperation.call(name, options, filename, unpack { ... }) 142 | end 143 | 144 | function Image.find_load_buffer(data) 145 | local name = vips_lib.vips_foreign_find_load_buffer(data, #data) 146 | if name == ffi.NULL then 147 | return nil 148 | else 149 | return ffi.string(name) 150 | end 151 | end 152 | 153 | function Image.new_from_buffer(data, options, ...) 154 | local name = Image.find_load_buffer(data) 155 | if name == nil then 156 | error(verror.get()) 157 | end 158 | 159 | return voperation.call(name, options or "", data, unpack { ... }) 160 | end 161 | 162 | function Image.new_from_memory_ptr(data, size, width, height, bands, format) 163 | local format_value = gvalue.to_enum(gvalue.band_format_type, format) 164 | local vimage = vips_lib.vips_image_new_from_memory(data, size, 165 | width, height, bands, format_value) 166 | if vimage == ffi.NULL then 167 | error(verror.get()) 168 | end 169 | return Image.new(vimage) 170 | end 171 | 172 | function Image.new_from_memory(data, width, height, bands, format) 173 | local image = Image.new_from_memory_ptr(data, ffi.sizeof(data), width, height, bands, format) 174 | -- libvips is using the memory we passed in: save a pointer to the memory 175 | -- block to try to stop it being GCd 176 | image._data = data 177 | return image 178 | end 179 | 180 | function Image.new_from_array(array, scale, offset) 181 | if not is_2D(array) then 182 | array = { array } 183 | end 184 | local width = #array[1] 185 | local height = #array 186 | 187 | local n = width * height 188 | local arr = {} 189 | for y = 0, height - 1 do 190 | for x = 0, width - 1 do 191 | arr[x + y * width + 1] = array[y + 1][x + 1] 192 | end 193 | end 194 | local a = ffi.new(gvalue.double_arr_typeof, n, arr) 195 | 196 | local vimage = vips_lib.vips_image_new_matrix_from_array(width, 197 | height, a, n) 198 | local image = Image.new(vimage) 199 | 200 | image:set_type(gvalue.gdouble_type, "scale", scale or 1.0) 201 | image:set_type(gvalue.gdouble_type, "offset", offset or 0.0) 202 | 203 | return image 204 | end 205 | 206 | function Image.new_from_image(base_image, value) 207 | local pixel = (Image.black(1, 1) + value):cast(base_image:format()) 208 | local image = pixel:embed(0, 0, base_image:width(), base_image:height(), 209 | { extend = "copy" }) 210 | image = image:copy { 211 | interpretation = base_image:interpretation(), 212 | xres = base_image:xres(), 213 | yres = base_image:yres(), 214 | xoffset = base_image:xoffset(), 215 | yoffset = base_image:yoffset() 216 | } 217 | 218 | return image 219 | end 220 | 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 229 | -- overloads 230 | 231 | function Image.mt.__add(a, b) 232 | a, b = swap_Image_left(a, b) 233 | 234 | if type(b) == "number" then 235 | return a:linear({ 1 }, { b }) 236 | elseif is_pixel(b) then 237 | return a:linear({ 1 }, b) 238 | else 239 | return a:add(b) 240 | end 241 | end 242 | 243 | function Image.mt.__sub(a, b) 244 | if Image.is_Image(a) then 245 | if type(b) == "number" then 246 | return a:linear({ 1 }, { -b }) 247 | elseif is_pixel(b) then 248 | return a:linear({ 1 }, map(function(x) 249 | return -x 250 | end, b)) 251 | else 252 | return a:subtract(b) 253 | end 254 | else 255 | -- therefore a is a constant and b is an image 256 | if type(a) == "number" then 257 | return (b * -1):linear({ 1 }, { a }) 258 | else 259 | -- assume a is a pixel 260 | return (b * -1):linear({ 1 }, a) 261 | end 262 | end 263 | end 264 | 265 | function Image.mt.__mul(a, b) 266 | a, b = swap_Image_left(a, b) 267 | 268 | if type(b) == "number" then 269 | return a:linear({ b }, { 0 }) 270 | elseif is_pixel(b) then 271 | return a:linear(b, { 0 }) 272 | else 273 | return a:multiply(b) 274 | end 275 | end 276 | 277 | function Image.mt.__div(a, b) 278 | if Image.is_Image(a) then 279 | if type(b) == "number" then 280 | return a:linear({ 1 / b }, { 0 }) 281 | elseif is_pixel(b) then 282 | return a:linear(map(function(x) 283 | return x ^ -1 284 | end, b), { 0 }) 285 | else 286 | return a:divide(b) 287 | end 288 | else 289 | -- therefore a is a constant and b is an image 290 | if type(a) == "number" then 291 | return (b ^ -1):linear({ a }, { 0 }) 292 | else 293 | -- assume a is a pixel 294 | return (b ^ -1):linear(a, { 0 }) 295 | end 296 | end 297 | end 298 | 299 | function Image.mt.__mod(a, b) 300 | if not Image.is_Image(a) then 301 | error("constant % image not supported by libvips") 302 | end 303 | 304 | if type(b) == "number" then 305 | return a:remainder_const({ b }) 306 | elseif is_pixel(b) then 307 | return a:remainder_const(b) 308 | else 309 | return a:remainder(b) 310 | end 311 | end 312 | 313 | function Image.mt.__unm(self) 314 | return self * -1 315 | end 316 | 317 | function Image.mt.__pow(a, b) 318 | if Image.is_Image(a) then 319 | return a:pow(b) 320 | else 321 | return b:wop(a) 322 | end 323 | end 324 | 325 | -- unfortunately, lua does not let you return non-bools from <, >, <=, >=, ==, 326 | -- ~=, so there's no point overloading these ... call :more(2) etc. instead 327 | 328 | function Image.mt:__tostring() 329 | local result = (self:filename() or "(nil)") .. ": " .. 330 | self:width() .. "x" .. self:height() .. " " .. 331 | self:format() .. ", " .. 332 | self:bands() .. " bands, " .. 333 | self:interpretation() 334 | 335 | if self:get_typeof("vips-loader") ~= 0 then 336 | result = result .. ", " .. self:get("vips-loader") 337 | end 338 | 339 | return result 340 | end 341 | 342 | function Image.mt:__call(x, y) 343 | -- getpoint() will return a table for a pixel 344 | return unpack(self:getpoint(x, y)) 345 | end 346 | 347 | function Image.mt:__concat(other) 348 | return self:bandjoin(other) 349 | end 350 | 351 | -- instance methods 352 | 353 | local Image_method = {} 354 | 355 | function Image_method:vobject() 356 | -- TODO: Could we use `self.vimage.parent_instance` here? 357 | return ffi.cast(vobject.typeof, self.vimage) 358 | end 359 | 360 | -- handy to have as instance methods too 361 | 362 | function Image_method:imageize(value) 363 | return Image.imageize(self, value) 364 | end 365 | 366 | function Image_method:new_from_image(value) 367 | return Image.new_from_image(self, value) 368 | end 369 | 370 | function Image_method:copy_memory() 371 | local vimage = vips_lib.vips_image_copy_memory(self.vimage) 372 | if vimage == ffi.NULL then 373 | error(verror.get()) 374 | end 375 | return Image.new(vimage) 376 | end 377 | 378 | -- writers 379 | 380 | function Image_method:write_to_file(vips_filename, ...) 381 | collectgarbage("stop") 382 | local filename = to_string_copy(vips_lib.vips_filename_get_filename(vips_filename)) 383 | local options = to_string_copy(vips_lib.vips_filename_get_options(vips_filename)) 384 | local name = vips_lib.vips_foreign_find_save(filename) 385 | collectgarbage("restart") 386 | if name == ffi.NULL then 387 | error(verror.get()) 388 | end 389 | return voperation.call(ffi.string(name), options, 390 | self, filename, unpack { ... }) 391 | end 392 | 393 | function Image_method:write_to_buffer(format_string, ...) 394 | collectgarbage("stop") 395 | local options = to_string_copy(vips_lib.vips_filename_get_options(format_string)) 396 | local name = vips_lib.vips_foreign_find_save_buffer(format_string) 397 | collectgarbage("restart") 398 | if name == ffi.NULL then 399 | error(verror.get()) 400 | end 401 | 402 | return voperation.call(ffi.string(name), options, self, unpack { ... }) 403 | end 404 | 405 | function Image_method:write_to_memory() 406 | local psize = ffi.new(gvalue.psize_typeof, 1) 407 | local vips_memory = vips_lib.vips_image_write_to_memory(self.vimage, psize) 408 | local size = tonumber(psize[0]) 409 | 410 | local lua_memory = ffi.new(gvalue.mem_typeof, size) 411 | ffi.copy(lua_memory, vips_memory, size) 412 | glib_lib.g_free(vips_memory) 413 | 414 | return lua_memory 415 | end 416 | 417 | function Image_method:write_to_memory_ptr() 418 | local psize = ffi.new(gvalue.psize_typeof, 1) 419 | local vips_memory = vips_lib.vips_image_write_to_memory(self.vimage, psize) 420 | 421 | return ffi.gc(vips_memory, glib_lib.g_free), tonumber(psize[0]) 422 | end 423 | 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 435 | -- get/set metadata 436 | 437 | function Image_method:get_typeof(name) 438 | -- on libvips 8.4 and earlier, we need to fetch the type via 439 | -- our superclass get_typeof(), since vips_image_get_typeof() returned 440 | -- enum properties as ints 441 | if not version.at_least(8, 5) then 442 | local vob = self:vobject() 443 | local gtype = vob:get_typeof(name) 444 | if gtype ~= 0 then 445 | return vob:get_type(name, gtype) 446 | end 447 | 448 | -- we must clear the error buffer after vobject typeof fails 449 | verror.get() 450 | end 451 | 452 | return vips_lib.vips_image_get_typeof(self.vimage, name) 453 | end 454 | 455 | function Image_method:get(name) 456 | -- on libvips 8.4 and earlier, we need to fetch gobject properties via 457 | -- our superclass get(), since vips_image_get() returned enum properties 458 | -- as ints 459 | if not version.at_least(8, 5) then 460 | local vo = self:vobject() 461 | local gtype = vo:get_typeof(name) 462 | if gtype ~= 0 then 463 | return vo:get(name) 464 | end 465 | 466 | -- we must clear the error buffer after vobject typeof fails 467 | verror.get() 468 | end 469 | 470 | local pgv = gvalue(true) 471 | 472 | local result = vips_lib.vips_image_get(self.vimage, name, pgv) 473 | if result ~= 0 then 474 | error("unable to get " .. name) 475 | end 476 | 477 | result = pgv[0]:get() 478 | gobject_lib.g_value_unset(pgv[0]) 479 | 480 | return result 481 | end 482 | 483 | function Image_method:set_type(gtype, name, value) 484 | local pgv = gvalue(true) 485 | pgv[0]:init(gtype) 486 | pgv[0]:set(value) 487 | vips_lib.vips_image_set(self.vimage, name, pgv) 488 | gobject_lib.g_value_unset(pgv[0]) 489 | end 490 | 491 | function Image_method:set(name, value) 492 | local gtype = self:get_typeof(name) 493 | self:set_type(gtype, name, value) 494 | end 495 | 496 | function Image_method:remove(name) 497 | return vips_lib.vips_image_remove(self.vimage, name) ~= 0 498 | end 499 | 500 | function Image_method:get_gainmap() 501 | collectgarbage("stop") 502 | local vimage = vips_lib.vips_image_get_gainmap(self.vimage) 503 | collectgarbage("restart") 504 | if vimage == ffi.NULL then 505 | return nil 506 | end 507 | 508 | return Image.new(vimage) 509 | end 510 | -- standard header fields 511 | 512 | function Image_method:width() 513 | return self:get("width") 514 | end 515 | 516 | function Image_method:height() 517 | return self:get("height") 518 | end 519 | 520 | function Image_method:size() 521 | return self:width(), self:height() 522 | end 523 | 524 | function Image_method:bands() 525 | return self:get("bands") 526 | end 527 | 528 | function Image_method:format() 529 | return self:get("format") 530 | end 531 | 532 | function Image_method:interpretation() 533 | return self:get("interpretation") 534 | end 535 | 536 | function Image_method:xres() 537 | return self:get("xres") 538 | end 539 | 540 | function Image_method:yres() 541 | return self:get("yres") 542 | end 543 | 544 | function Image_method:xoffset() 545 | return self:get("xoffset") 546 | end 547 | 548 | function Image_method:yoffset() 549 | return self:get("yoffset") 550 | end 551 | 552 | function Image_method:filename() 553 | return self:get("filename") 554 | end 555 | 556 | -- many-image input operations 557 | -- 558 | -- these don't wrap well automatically, since self is held separately 559 | 560 | function Image_method:bandjoin(other, options) 561 | -- allow a single untable arg as well 562 | if type(other) == "number" or Image.is_Image(other) then 563 | other = { other } 564 | end 565 | 566 | -- if other is all constants, we can use bandjoin_const 567 | local all_constant = true 568 | for i = 1, #other do 569 | if type(other[i]) ~= "number" then 570 | all_constant = false 571 | break 572 | end 573 | end 574 | 575 | if all_constant then 576 | return voperation.call("bandjoin_const", "", self, other, options) 577 | else 578 | return voperation.call("bandjoin", "", { self, unpack(other) }, options) 579 | end 580 | end 581 | 582 | function Image_method:bandrank(other, options) 583 | if type(other) ~= "table" then 584 | other = { other } 585 | end 586 | 587 | return voperation.call("bandrank", "", { self, unpack(other) }, options) 588 | end 589 | 590 | function Image_method:composite(other, mode, options) 591 | -- allow a single untable arg as well 592 | if type(other) == "number" or Image.is_Image(other) then 593 | other = { other } 594 | end 595 | if type(mode) ~= "table" then 596 | mode = { mode } 597 | end 598 | 599 | -- need to map str -> int by hand, since the mode arg is actually 600 | -- arrayint 601 | for i = 1, #mode do 602 | mode[i] = gvalue.to_enum(gvalue.blend_mode_type, mode[i]) 603 | end 604 | 605 | return voperation.call("composite", "", { self, unpack(other) }, mode, options) 606 | end 607 | 608 | -- convenience functions 609 | 610 | function Image_method:hasalpha() 611 | return vips_lib.vips_image_hasalpha(self.vimage) ~= 0 612 | end 613 | 614 | -- addalpha was made a VipsOperation in vips 8.16; earlier versions need this polyfill 615 | if not version.at_least(8, 16) then 616 | function Image_method:addalpha() 617 | local max_alpha 618 | if self:interpretation() == "rgb16" or self:interpretation() == "grey16" then 619 | max_alpha = 65535 620 | elseif self:interpretation() == "scrgb" then 621 | max_alpha = 1.0 622 | else 623 | max_alpha = 255 624 | end 625 | 626 | return self:bandjoin(max_alpha) 627 | end 628 | end 629 | 630 | function Image_method:bandsplit() 631 | local result 632 | 633 | result = {} 634 | for i = 0, self:bands() - 1 do 635 | result[i + 1] = self:extract_band(i) 636 | end 637 | 638 | return result 639 | end 640 | 641 | -- special behaviour wrappers 642 | 643 | function Image_method:ifthenelse(then_value, else_value, options) 644 | -- We need different imageize rules for this. We need then_value 645 | -- and else_value to match each other first, and only if they 646 | -- are both constants do we match to self. 647 | 648 | local match_image 649 | 650 | for _, v in pairs({ then_value, else_value, self }) do 651 | if Image.is_Image(v) then 652 | match_image = v 653 | break 654 | end 655 | end 656 | 657 | if not Image.is_Image(then_value) then 658 | then_value = match_image:imageize(then_value) 659 | end 660 | 661 | if not Image.is_Image(else_value) then 662 | else_value = match_image:imageize(else_value) 663 | end 664 | 665 | return voperation.call("ifthenelse", "", self, then_value, else_value, options) 666 | end 667 | 668 | -- enum expansions 669 | 670 | function Image_method:pow(other) 671 | return call_enum(self, other, "math2", "pow") 672 | end 673 | 674 | function Image_method:wop(other) 675 | return call_enum(self, other, "math2", "wop") 676 | end 677 | 678 | function Image_method:lshift(other) 679 | return call_enum(self, other, "boolean", "lshift") 680 | end 681 | 682 | function Image_method:rshift(other) 683 | return call_enum(self, other, "boolean", "rshift") 684 | end 685 | 686 | function Image_method:andimage(other) 687 | return call_enum(self, other, "boolean", "and") 688 | end 689 | 690 | function Image_method:orimage(other) 691 | return call_enum(self, other, "boolean", "or") 692 | end 693 | 694 | function Image_method:eorimage(other) 695 | return call_enum(self, other, "boolean", "eor") 696 | end 697 | 698 | function Image_method:less(other) 699 | return call_enum(self, other, "relational", "less") 700 | end 701 | 702 | function Image_method:lesseq(other) 703 | return call_enum(self, other, "relational", "lesseq") 704 | end 705 | 706 | function Image_method:more(other) 707 | return call_enum(self, other, "relational", "more") 708 | end 709 | 710 | function Image_method:moreeq(other) 711 | return call_enum(self, other, "relational", "moreeq") 712 | end 713 | 714 | function Image_method:equal(other) 715 | return call_enum(self, other, "relational", "equal") 716 | end 717 | 718 | function Image_method:noteq(other) 719 | return call_enum(self, other, "relational", "noteq") 720 | end 721 | 722 | function Image_method:floor() 723 | return self:round("floor") 724 | end 725 | 726 | function Image_method:ceil() 727 | return self:round("ceil") 728 | end 729 | 730 | function Image_method:rint() 731 | return self:round("rint") 732 | end 733 | 734 | function Image_method:bandand() 735 | return self:bandbool("and") 736 | end 737 | 738 | function Image_method:bandor() 739 | return self:bandbool("or") 740 | end 741 | 742 | function Image_method:bandeor() 743 | return self:bandbool("eor") 744 | end 745 | 746 | function Image_method:real() 747 | return self:complexget("real") 748 | end 749 | 750 | function Image_method:imag() 751 | return self:complexget("imag") 752 | end 753 | 754 | function Image_method:polar() 755 | return self:complex("polar") 756 | end 757 | 758 | function Image_method:rect() 759 | return self:complex("rect") 760 | end 761 | 762 | function Image_method:conj() 763 | return self:complex("conj") 764 | end 765 | 766 | function Image_method:sin() 767 | return self:math("sin") 768 | end 769 | 770 | function Image_method:cos() 771 | return self:math("cos") 772 | end 773 | 774 | function Image_method:tan() 775 | return self:math("tan") 776 | end 777 | 778 | function Image_method:asin() 779 | return self:math("asin") 780 | end 781 | 782 | function Image_method:acos() 783 | return self:math("acos") 784 | end 785 | 786 | function Image_method:atan() 787 | return self:math("atan") 788 | end 789 | 790 | function Image_method:exp() 791 | return self:math("exp") 792 | end 793 | 794 | function Image_method:exp10() 795 | return self:math("exp10") 796 | end 797 | 798 | function Image_method:log() 799 | return self:math("log") 800 | end 801 | 802 | function Image_method:log10() 803 | return self:math("log10") 804 | end 805 | 806 | function Image_method:erode(mask) 807 | return self:morph(mask, "erode") 808 | end 809 | 810 | function Image_method:dilate(mask) 811 | return self:morph(mask, "dilate") 812 | end 813 | 814 | function Image_method:fliphor() 815 | return self:flip("horizontal") 816 | end 817 | 818 | function Image_method:flipver() 819 | return self:flip("vertical") 820 | end 821 | 822 | function Image_method:rot90() 823 | return self:rot("d90") 824 | end 825 | 826 | function Image_method:rot180() 827 | return self:rot("d180") 828 | end 829 | 830 | function Image_method:rot270() 831 | return self:rot("d270") 832 | end 833 | 834 | -- this is for undefined class / instance methods, like Image.text or image:avg 835 | local fall_back = function(name) 836 | return function(...) 837 | return voperation.call(name, "", unpack { ... }) 838 | end 839 | end 840 | 841 | function Image.mt.__index(_, name) 842 | -- try to get instance method otherwise fallback to voperation 843 | return rawget(Image_method, name) or fall_back(name) 844 | end 845 | 846 | return setmetatable(Image, { 847 | __index = function(_, name) 848 | -- undefined class methods 849 | return fall_back(name) 850 | end 851 | }) 852 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-vips 2 | 3 | [![CI](https://github.com/libvips/lua-vips/workflows/CI/badge.svg)](https://github.com/libvips/lua-vips/actions) 4 | 5 | This is a Lua binding for the [libvips image processing 6 | library](http://libvips.github.io/libvips). `libvips` 7 | is a [fast image processing library with low memory needs](https://github.com/jcupitt/lua-vips-bench). 8 | `lua-vips` uses ffi and needs either 9 | - luajit >= 2.0 or 10 | - standard Lua (5.1 up to 5.4) combined with the [`luaffi-tkl`](https://luarocks.org/modules/sudheerhebbale/luaffi-tkl) Lua package. 11 | 12 | On the x64 architecture `lua-vips` is continuously tested 13 | - on Linux and MacOS with Lua 5.1, 5.2, 5.3, 5.4 and openresty-luajit 14 | - on Windows using [MSYS2 MinGW-w64](https://www.msys2.org/) with Lua 5.3, 5.4 and luajit 15 | 16 | `lua-vips` should work on arm64 (recently tested on a Pinephone Pro using PostmarketOS) and possibly x86 (currently untested) 17 | as well. 18 | 19 | The libvips documentation includes a handy searchable table of [every operation in 20 | libvips](http://libvips.github.io/libvips/API/current/func-list.html). This 21 | is a good place to check if it supports some feature you need. Read on to 22 | see how to call libvips operations. 23 | 24 | # Example 25 | 26 | [Install the libvips shared 27 | library](https://libvips.github.io/libvips/install.html), then install this rock with: 28 | 29 | ```shell 30 | luarocks install lua-vips 31 | ``` 32 | 33 | When used with LuaJIT please first exhibit luaffi-tkl as provided by the VM via: 34 | ```shell 35 | luarocks config --lua-version=5.1 rocks_provided.luaffi-tkl 2.1-1 36 | ``` 37 | 38 | Example: 39 | 40 | ```lua 41 | local vips = require "vips" 42 | 43 | -- fast thumbnail generator 44 | local image = vips.Image.thumbnail("somefile.jpg", 128) 45 | image:write_to_file("tiny.jpg") 46 | 47 | -- make a new image with some text rendered on it 48 | image = vips.Image.text("Hello World!", {dpi = 300}) 49 | 50 | -- call a method 51 | image = image:invert() 52 | 53 | -- use the `..` operator to join images bandwise 54 | image = image .. image .. image 55 | 56 | -- add a constant 57 | image = image + 12 58 | -- add a different value to each band 59 | image = image + { 1, 2, 3 } 60 | -- add two images 61 | image = image + image 62 | 63 | -- split bands up again 64 | b1, b2, b3 = image:bandsplit() 65 | 66 | -- read a pixel from coordinate (10, 20) 67 | r, g, b = image(10, 20) 68 | 69 | -- make all pixels less than 128 bright blue 70 | -- :less(128) makes an 8-bit image where each band is 255 (true) if that 71 | -- value is less than 128, and 0 (false) if it's >= 128 ... you can use 72 | --- images or {1,2,3} constants as well as simple values 73 | -- :bandand() joins all image bands together with bitwise AND, so you get a 74 | -- one-band image which is true where all bands are true 75 | -- condition:ifthenelse(then, else) takes a condition image and uses true or 76 | -- false values to pick pixels from the then or else images ... then and 77 | -- else can be constants or images 78 | image = image:less(128):bandand():ifthenelse({ 0, 0, 255 }, image) 79 | 80 | -- go to Yxy colourspace 81 | image = image:colourspace("yxy") 82 | 83 | -- pass options to a save operation 84 | image:write_to_file("x.png", { compression = 9 }) 85 | ``` 86 | 87 | # How it works 88 | 89 | libvips has quite a bit of introspection machinery built in. 90 | 91 | When you call something like `image:hough_circle{ scale = 4 }`, the `__index` 92 | method on the `lua-vips` image class opens libvips with ffi and searches 93 | for an operation called `hough_circle`. It discovers what arguments the 94 | operation takes, checks you supplied the correct arguments, and transforms 95 | them into the form that libvips needs. It executes the operator, then pulls 96 | out all the results and returns them as a Lua table. 97 | 98 | This means that, although `lua-vips` supports almost 300 operators, the 99 | binding itself is small, should be simple to maintain, and should always 100 | be up to date. 101 | 102 | # Getting more help 103 | 104 | The libvips website has a handy table of [all the libvips 105 | operators](http://libvips.github.io/libvips/API/current/func-list.html). Each 106 | one links to the main API docs so you can see what you need to pass to it. 107 | 108 | A simple way to see the arguments for an operation is to try running it 109 | from the command-line. For example: 110 | 111 | ```bash 112 | $ vips embed 113 | embed an image in a larger image 114 | usage: 115 | embed in out x y width height 116 | where: 117 | in - Input image, input VipsImage 118 | out - Output image, output VipsImage 119 | x - Left edge of input in output, input gint 120 | default: 0 121 | min: -1000000000, max: 1000000000 122 | y - Top edge of input in output, input gint 123 | default: 0 124 | min: -1000000000, max: 1000000000 125 | width - Image width in pixels, input gint 126 | default: 1 127 | min: 1, max: 1000000000 128 | height - Image height in pixels, input gint 129 | default: 1 130 | min: 1, max: 1000000000 131 | optional arguments: 132 | extend - How to generate the extra pixels, input VipsExtend 133 | default: black 134 | allowed: black, copy, repeat, mirror, white, background 135 | background - Color for background pixels, input VipsArrayDouble 136 | operation flags: sequential 137 | ``` 138 | 139 | So you can call `embed` like this: 140 | 141 | ```lua 142 | local image = image:embed(100, 100, image:width() + 200, image:height() + 200, 143 | { extend = "mirror" }) 144 | ``` 145 | 146 | To add a 100 pixel mirror edge around an image. 147 | 148 | # Features 149 | 150 | This section runs through the main features of the binding. 151 | 152 | To load the binding use: 153 | 154 | ```lua 155 | local vips = require "vips" 156 | ``` 157 | 158 | ## Make images 159 | 160 | You can make images from files or from buffers (Lua strings), you can wrap 161 | a vips image around an ffi array, or you can use one of the libvips create 162 | operators to make an image for you. 163 | 164 | ### `image = vips.Image.new_from_file(filename [, options])` 165 | 166 | Opens the file and returns an image. You can pass a set of options in a final 167 | table argument, for example: 168 | 169 | ```lua 170 | local image = vips.Image.new_from_file("somefile.jpg", 171 | { access = "sequential" }) 172 | ``` 173 | 174 | Some options are specific to some file types, for example, `shrink`, meaning 175 | shrink by an integer factor during load, only applies to images loaded via 176 | libjpeg. 177 | 178 | You can embed options in filenames using the standard libvips syntax. For 179 | example, these are equivalent: 180 | 181 | ```lua 182 | local image = vips.Image.new_from_file("somefile.jpg", { shrink = 2 }) 183 | local image = vips.Image.new_from_file("somefile.jpg[shrink=2]") 184 | ``` 185 | 186 | You can call specific file format loaders directly, for example: 187 | 188 | ```lua 189 | local image = vips.Image.jpegload("somefile.jpg", { shrink = 4 }) 190 | ``` 191 | 192 | The [loader section in the API 193 | docs](http://libvips.github.io/libvips/API/current/VipsForeignSave.html) lists 194 | all loaders and their options. 195 | 196 | ### `image = vips.Image.new_from_buffer(string [, string_options, options])` 197 | 198 | The string argument should contain an image file in some container format, such 199 | as JPEG. You can supply options, just as with `new_from_file`. These are 200 | equivalent: 201 | 202 | ```lua 203 | local image = vips.Image.new_from_buffer(string, "", { shrink = 2 }) 204 | local image = vips.Image.new_from_buffer(string, "shrink=2") 205 | ``` 206 | 207 | Use (for example) `vips.Image.jpegload_buffer` to call a loader directly. 208 | 209 | ### `image = vips.Image.new_from_memory(data, width, height, bands, format)` 210 | 211 | This wraps a libvips image around a FFI memory array. The memory array should be 212 | formatted as a C-style array. Images are always band-interleaved, so an RGB 213 | image three pixels across and two pixels down, for example, is laid out as: 214 | 215 | ``` 216 | RGBRGBRGB 217 | RGBRGBRGB 218 | ``` 219 | 220 | Example: 221 | 222 | ```lua 223 | local width = 64 224 | local height = 32 225 | local data = ffi.new("unsigned char[?]", width * height) 226 | local im = vips.Image.new_from_memory(data, width, height, 1, "uchar") 227 | ``` 228 | 229 | The returned image is using a pointer to the `data` area, but the Lua/LuaJIT interpreter won't always know this. You should keep a reference to `data` alive for as long as you are using any downstream images, or you'll get a crash. 230 | 231 | ### `image = vips.Image.new_from_memory_ptr(data, size, width, height, bands, format)` 232 | 233 | Same as `new_from_memory`, but for any kind of data pointer (non-FFI allocated) by specifying the length of the data in bytes. The pointed data must be valid for the lifespan of the image and any downstream images. 234 | 235 | A string can be used as the data pointer thanks to FFI semantics. 236 | 237 | ### `image = vips.Image.new_from_image(image, pixel)` 238 | 239 | Makes a new image with the size, format, and resolution of `image`, but with 240 | each pixel having the value `pixel`. For example: 241 | 242 | ```lua 243 | local new_image = vips.Image.new_from_image(image, 12) 244 | ``` 245 | 246 | Will make a new image with one band where every pixel has the value 12. You can 247 | call it as a member function. `pixel` can be a table to make a many-band image, 248 | for example: 249 | 250 | ```lua 251 | local new_image = image:new_from_image{ 1, 2, 3 } 252 | ``` 253 | 254 | Will make a new three-band image, where all the red pixels have the value 1, 255 | greens are 2 and blues are 3. 256 | 257 | ### `image = vips.Image.new_from_array(array [, scale [, offset]])` 258 | 259 | Makes a new image from a Lua table. For example: 260 | 261 | ```lua 262 | local image = vips.Image.new_from_array{ 1, 2, 3 } 263 | ``` 264 | 265 | Makes a one-band image, three pixels across and one high. Use nested tables for 266 | 2D images. You can set a scale and offset with two extra number parameters -- 267 | handy for integer convolution masks. 268 | 269 | ```lua 270 | local mask = vips.Image.new_from_array( 271 | {{-1, -1, -1}, 272 | {-1, 16, -1}, 273 | {-1, -1, -1}}, 8) 274 | local image = image:conv(mask, { precision = "integer" }) 275 | ``` 276 | 277 | ### `image = vips.Image.copy_memory(self)` 278 | 279 | The image is rendered to a large memory buffer, and a new image is returned 280 | which represents the memory area. 281 | 282 | This is handy for breaking a pipeline. 283 | 284 | ### `image = vips.Image.black(width, height)` 285 | 286 | Makes a new one band, 8 bit, black image. You can call any of the libvips image 287 | creation operators in this way, for example: 288 | 289 | ```lua 290 | local noise = vips.Image.perlin(256, 256, { cell_size = 128 }) 291 | ``` 292 | 293 | See: 294 | 295 | [http://libvips.github.io/libvips/API/current/libvips-create.html](http://libvips.github.io/libvips/API/current/libvips-create.html) 296 | 297 | ## Get and set image metadata 298 | 299 | You can read and write aribitrary image metadata. 300 | 301 | ### `number = vips.Image.get_typeof(image, field_name)` 302 | 303 | This returns the GType for a field, or 0 if the field does not exist. 304 | `vips.gvalue` has a set of GTypes you can check against. 305 | 306 | ### `mixed = vips.Image.get(image, field_name)` 307 | 308 | This reads any named piece of metadata from the image, for example: 309 | 310 | ```lua 311 | local version = image:get("exif-ifd2-ExifVersion") 312 | ``` 313 | 314 | The item is converted to some Lua type in the obvious way. There are convenient 315 | shortcuts for many of the standard fields, so these are equivalent: 316 | 317 | ```lua 318 | local width = image:get("width") 319 | local width = image:width() 320 | ``` 321 | 322 | If the field does not exist, `lua-vips` will throw an error. Use `get_typeof` 323 | to check for the existence of a field. 324 | 325 | ### `vips.Image.set_type(image, gtype, field_name, value)` 326 | 327 | This creates a new metadata item of the specified type, name and value. 328 | 329 | ### `vips.Image.set(image, field_name, value)` 330 | 331 | This changes the value of an existing field, but will not change its type. 332 | 333 | You can't use `set()` to change core fields such as like `width` or 334 | `interpretation`. Use `copy()` instead. 335 | 336 | Image references will be shared by the operation cache, so modifying an image 337 | can change an image somewhere else in your program. Before changing an image, 338 | you must make sure you own a private copy of an image with `copy`. 339 | 340 | ```lua 341 | local new_image = image:copy() 342 | new_image:set("orientation", 7) 343 | ``` 344 | 345 | ### `boolean = vips.Image.remove(image, field_name)` 346 | 347 | This will remove a piece of metadata. It returns `true` if an item was 348 | successfully removed, `false` otherwise. 349 | 350 | As with `set`, you must use copy before removing a metadata item. 351 | 352 | ## Call any libvips operation 353 | 354 | You can call any libvips operation as a member function, for example 355 | `hough_circle`, the circular Hough transform: 356 | 357 | [http://libvips.github.io/libvips/API/current/libvips-arithmetic.html#vips-hough-circle](http://libvips.github.io/libvips/API/current/libvips-arithmetic.html#vips-hough-circle) 358 | 359 | Can be called from Lua like this: 360 | 361 | ```lua 362 | local image2 = image:hough_circle{ scale = 2, max_radius = 50 } 363 | ``` 364 | 365 | The rules are: 366 | 367 | 1. `self` is used to set the first required input image argument. 368 | 369 | 2. If you supply one more argument than the number of required arguments, 370 | and the final argument you supply is a table, 371 | that extra table is used to set any optional input arguments. 372 | 373 | 3. If you supply a constant (a number, or a table of numbers) and libvips 374 | wants an image, your constant is automatically turned into an image using 375 | the first input image you supplied as a guide. 376 | 377 | 4. For enums, you can supply a number or a string. The string is an enum member 378 | nickname (the part after the final underscore). 379 | 380 | 5. `MODIFY` arguments, for example the image you pass to `draw_circle`, are 381 | copied to memory before being set, and the new image is returned as one of 382 | the results. 383 | 384 | 6. Operation results are returned as an unpacked array in the order: all 385 | required output args, then all optional output args, then all deprecated 386 | output args. 387 | 388 | You can write (for example): 389 | 390 | ```lua 391 | max_value = image:max() 392 | ``` 393 | 394 | To get the maximum value from an image. If you look at [the `max` 395 | operator](http://libvips.github.io/libvips/API/current/libvips-arithmetic.html#vips-max), 396 | it can actually return a lot more than this. You can write: 397 | 398 | ```lua 399 | max_value, x, y = image:max() 400 | ``` 401 | 402 | To get the position of the maximum, or: 403 | 404 | ```lua 405 | max_value, x, y, maxes = image:max{ size = 10 } 406 | ``` 407 | 408 | and `maxes` will be an array of the top 10 maximum values in order. 409 | 410 | ## Operator overloads 411 | 412 | The Lua operators are overloaded in the obvious way, so you can write (for 413 | example): 414 | 415 | ```lua 416 | image = (image * 2 + 13) % 4 417 | ``` 418 | 419 | and the appropriate vips operations will be called. You can mix images, number 420 | constants, and array constants freely. 421 | 422 | The relational operators are not overloaded, unfortunately; Lua does not 423 | permit this. You must write something like: 424 | 425 | ```lua 426 | image = image:less(128):ifthenelse(128, image) 427 | ``` 428 | 429 | to set all values less than 128 to 128. 430 | 431 | `__call` (ie. `()`) is overloaded to call the libvips `getpoint` operator. 432 | You can write: 433 | 434 | ```lua 435 | image = vips.Image.new_from_file("k2.jpg") 436 | r, g, b = image(10, 10) 437 | ``` 438 | 439 | and `r`, `g`, `b` will be the RGB values for the pixel at coordinate (10, 10). 440 | 441 | `..` is overloaded to mean `bandjoin`. 442 | 443 | Use `im:bands()` to get the number of bands and `im:extract_band(N)` to extract a 444 | band (note bands number from zero). lua-vips does not overload `#` and `[]` for 445 | this, since mixing numbering from zero and one causes confusion. 446 | 447 | ## Convenience functions 448 | 449 | A set of convenience functions are also defined. 450 | 451 | ### `array = image:bandsplit()` 452 | 453 | This splits a many-band image into an array of one band images. 454 | 455 | ### `image:bandjoin()` 456 | 457 | The `bandjoin` operator takes an array of images as input. This can be awkward 458 | to call --- you must write: 459 | 460 | 461 | ```lua 462 | image = vips.Image.bandjoin{ image, image } 463 | ``` 464 | 465 | to join an image to itself. Instead, `lua-vips` defines `bandjoin` as a member 466 | function, so you write: 467 | 468 | ```lua 469 | image = image:bandjoin(image) 470 | ``` 471 | 472 | to join an image to itself, or perhaps: 473 | 474 | ```lua 475 | image = R:bandjoin{ G, B } 476 | ``` 477 | 478 | to join three RGB bands. Constants work too, so you can write: 479 | 480 | ```lua 481 | image = image:bandjoin(255) 482 | image = R:bandjoin{ 128, 23 } 483 | ``` 484 | 485 | The `bandrank` and `composite` operators works in the same way. 486 | 487 | ### `image = condition_image:ifthenelse(then_image, else_image [, options])` 488 | 489 | This uses the condition image to pick pixels between then and else. Unlike all 490 | other operators, if you use a constant for `then_image` or `else_image`, they 491 | first match to each other, and only match to the condition image if both then 492 | and else are constants. 493 | 494 | ### `image = image:sin()` 495 | 496 | Many vips arithmetic operators are implemented by larger operators which take 497 | an enum to set their action. For example, sine is implemented by the `math` 498 | operator, so you must write: 499 | 500 | ```lua 501 | image = image:math("sin") 502 | ``` 503 | 504 | This is annoying, so a set of convenience functions are defined to enable you 505 | to write: 506 | 507 | ```lua 508 | image = image:sin() 509 | ``` 510 | 511 | There are about 40 of these. 512 | 513 | ## Write 514 | 515 | You can write images to files, to ffi arrays, or to formatted strings. 516 | 517 | ### `image:write_to_file(filename [, options])` 518 | 519 | The filename suffix is used to pick the save operator. Just as with 520 | `new_from_file`, not all options will be correct for all file 521 | types. You can call savers directly if you wish, for example: 522 | 523 | ```lua 524 | image:jpegsave("x.jpg", { Q = 90 }) 525 | ``` 526 | 527 | ### `string = image:write_to_buffer(suffix [, options])` 528 | 529 | The suffix is used to pick the saver that is used to generate the result, so 530 | `".jpg"` will make a JPEG-formatted string. Again, you can call the savers 531 | directly if you wish, perhaps: 532 | 533 | ```lua 534 | local str = image:jpegsave_buffer{ Q = 90 } 535 | ``` 536 | 537 | ### `memory = image:write_to_memory()` 538 | 539 | A large ffi char array is allocated and the image is rendered to it. 540 | 541 | ```lua 542 | local mem = image:write_to_memory() 543 | print("written ", ffi.sizeof(mem), "bytes to", mem) 544 | ``` 545 | 546 | ### `ptr, size = image:write_to_memory_ptr()` 547 | 548 | 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). 549 | 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 | 572 | ## Error handling 573 | 574 | Most `lua-vips` methods will call `error()` if they detect an error. Use 575 | `pcall()` to call a method and catch an error. 576 | 577 | Use `get_typeof` to test for a field of a certain name without throwing an 578 | error. 579 | 580 | ## The libvips operation cache 581 | 582 | libvips keeps a cache of recent operations, such as load, save, shrink, and 583 | so on. If you repeat an operation, you'll get the cached result back. 584 | 585 | It keeps track of the number of open files, allocated memory and cached 586 | operations, and will trim the cache if more than 100 files are open at once, 587 | more than 100mb of memory has been allocated, or more than 1,000 operations 588 | are being held. 589 | 590 | Normally this cache is useful and harmless, but for some applications you may 591 | want to change these values. 592 | 593 | ```lua 594 | -- set number of cached operations 595 | vips.cache_set_max(100) 596 | -- set maximum cache memory use 597 | vips.cache_set_max_mem(10 * 1024 * 1024) 598 | -- set maximum number of open files 599 | vips.cache_set_max_files(10) 600 | ``` 601 | 602 | # Development 603 | 604 | ### Setup for Ubuntu 605 | 606 | Configure `luarocks` for a local tree 607 | 608 | ```shell 609 | luarocks help path 610 | ``` 611 | 612 | append 613 | 614 | ```shell 615 | eval `luarocks path` 616 | export PATH="$HOME/.luarocks/bin:$PATH" 617 | ``` 618 | 619 | to `~/.bashrc`. 620 | 621 | ### Install 622 | 623 | ```shell 624 | luarocks --local make 625 | ``` 626 | 627 | ### Unit testing 628 | 629 | You need: 630 | 631 | ```shell 632 | luarocks --local install busted 633 | luarocks --local install luacov 634 | luarocks --local install say 635 | ``` 636 | 637 | Then to run the test suite: 638 | 639 | ```shell 640 | busted . 641 | ``` 642 | 643 | for verbose output: 644 | 645 | ```shell 646 | busted . -o gtest -v 647 | ``` 648 | 649 | ### Linting and static analysis 650 | 651 | You need: 652 | 653 | ```shell 654 | luarocks --local install luacheck 655 | ``` 656 | 657 | Then to run the linter: 658 | 659 | ```shell 660 | luacheck . 661 | ``` 662 | 663 | ### Test 664 | 665 | Run the example script with: 666 | 667 | ```shell 668 | lua example/hello-world.lua 669 | ``` 670 | 671 | ### Update rock 672 | 673 | ```shell 674 | rm *.src.rock 675 | luarocks upload lua-vips-1.1-12.rockspec --api-key=xxxxxxxxxxxxxx 676 | ``` 677 | 678 | ### Links 679 | 680 | http://luajit.org/ext_ffi_api.html 681 | 682 | http://luajit.org/ext_ffi_semantics.html 683 | 684 | https://github.com/luarocks/luarocks/wiki/creating-a-rock 685 | 686 | https://olivinelabs.com/busted/ 687 | 688 | ### Running on Windows using Mingw-w64 689 | 690 | Installing `lua-vips` on Windows is a bit harder than on Unix systems. We recommend using MinGW (Minimalist GNU for Windows) for the installation. Here are the steps: 691 | 692 | 1. Install [MSYS2](https://www.msys2.org/) to the default path. 693 | 2. Start Mingw-w64 64bit console from the start menu. Check that is says MINGW64. The following steps happen in that console. 694 | 3. Update MSYS2 using 695 | ```shell 696 | pacman -Syuu 697 | ``` 698 | 4. Install the build tools (including Lua 5.4 and Luarocks) via 699 | ```shell 700 | pacman -S git make mingw-w64-x86_64-toolchain mingw-w64-x86_64-lua-luarocks 701 | ``` 702 | 5. Install `libvips` with (optional) dependencies via 703 | ```shell 704 | pacman -S 705 | mingw-w64-x86_64-libvips 706 | mingw-w64-x86_64-openslide 707 | mingw-w64-x86_64-libheif 708 | mingw-w64-x86_64-libjxl 709 | mingw-w64-x86_64-imagemagick 710 | mingw-w64-x86_64-poppler 711 | ``` 712 | 6. Optionally: If you want to use `lua-vips` with LuaJIT instead of Lua 5.4 install LuaJIT via 713 | ```shell 714 | pacman -S mingw-w64-x86_64-luajit 715 | luarocks config --scope system lua_version 5.1 716 | luarocks config --scope system lua_interpreter luajit.exe 717 | luarocks config --scope system lua_dir /mingw64 718 | luarocks config --scope system rocks_provided.luaffi-tkl 2.1-1 719 | ``` 720 | 7. Install `lua-vips` via 721 | ```shell 722 | luarocks install lua-vips 723 | ``` 724 | or clone the repository and run `luarocks make` in the `lua-vips` folder. 725 | 8. Add `C:\msys64\mingw64\bin` and `C:\msys64\usr\bin` to the top of your `PATH` 726 | environment variable in the Windows Advanced system settings and restart the console. 727 | 728 | 9. Run `lua` or `luajit` and try 729 | ```lua 730 | vips = require "vips" 731 | print(vips.Image.xyz(3,2)) 732 | ``` 733 | 734 | ### Running under Wine (Windows emulation on Linux) 735 | 736 | @jcupitt used the luapower all-in-one to get a 64-bit Windows LuaJIT build: 737 | 738 | https://luapower.com/ 739 | 740 | LuaJIT on Windows searches `PATH` to find DLLs. You can't set this directly 741 | from Linux, you have to change the registry. See: 742 | 743 | https://www.winehq.org/docs/wineusr-guide/environment-variables 744 | 745 | Then add the `bin` area of the libvips Windows build to `PATH`. 746 | 747 | ``` 748 | z:\home\john\GIT\build-win64\8.5\vips-dev-8.5\bin 749 | ``` 750 | 751 | You must have no trailing backslash. 752 | 753 | Try LuaJIT: 754 | 755 | ``` 756 | $ ~/packages/luajit/luapower-all-master/bin/mingw64/luajit.exe 757 | LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2016 Mike Pall. 758 | http://luajit.org/ 759 | JIT: ON SSE2 SSE3 SSE4.1 fold cse dce fwd dse narrow loop abc sink fuse 760 | > print(os.getenv("PATH")) 761 | C:\windows\system32;C:\windows;C:\windows\system32\wbem;z:\home\john\GIT\build-win64\8.5\vips-dev-8.5\bin 762 | > ffi = require "ffi" 763 | > ffi.load("libvips-42.dll") 764 | > ^D 765 | ``` 766 | 767 | The Windows luajit will pick up your `.luarocks/share/lua/5.1/vips.lua` install, 768 | so to test just install and run: 769 | 770 | ``` 771 | $ ~/packages/luajit/luapower-all-master/bin/mingw64/luajit.exe 772 | LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2016 Mike Pall. http://luajit.org/ 773 | JIT: ON SSE2 SSE3 SSE4.1 fold cse dce fwd dse narrow loop abc sink fuse 774 | > vips = require "vips" 775 | > x = vips.Image.new_from_file("z:\\data\\john\\pics\\k2.jpg") 776 | > print(x:width()) 777 | 1450 778 | > x = vips.Image.text("hello", {dpi = 300}) 779 | > x:write_to_file("x.png") 780 | > 781 | ``` 782 | 783 | --------------------------------------------------------------------------------