diff --git a/.busted b/.busted index 8235a28..684630c 100644 --- a/.busted +++ b/.busted @@ -1,5 +1,6 @@ return { - _all = { - directory = "spec", - }, + default = { + helper = "./spec/helpers.lua", + coverage = os.getenv("TEST_COVERAGE") == "1" + } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8ccd07f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,114 @@ +name: "CI" + +on: [push, pull_request] + +jobs: + unix: + strategy: + fail-fast: false + matrix: + luaVersion: ["5.1", "5.2", "5.3", "5.4", "luajit-openresty"] + os: ["ubuntu-latest", "macos-latest"] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - uses: leafo/gh-actions-lua@v10 + with: + luaVersion: ${{ matrix.luaVersion }} + + - uses: leafo/gh-actions-luarocks@v4 + + - name: Install Ubuntu dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends libvips-dev + + - name: Install macOS dependencies + if: runner.os == 'macOS' + run: | + brew install vips + echo "DYLD_LIBRARY_PATH=$(brew --prefix vips)/lib:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV + + - name: Install lua-vips + run: | + if [[ ${{ matrix.luaVersion }} == luajit* ]]; then + luarocks config --scope system rocks_provided.luaffi-tkl 2.1-1 + fi + luarocks make + + - name: Lint with luacheck + run: | + luarocks install luacheck + luacheck -q . + + - name: Busted tests + run: | + luarocks test spec/ -- -o gtest -v spec + + + windows: + strategy: + fail-fast: false + matrix: + lua: [ + #{name: "lua51", exe: "lua5.1", version: 5.1, incdir: "/mingw64/include/lua5.1/"}, #(two tests are failing) + {name: "lua53", exe: "lua5.3", version: 5.3, incdir: "/mingw64/include/lua5.3/"}, + {name: "lua", exe: "lua", version: 5.4, incdir: "/mingw64/include/"}, + {name: "luajit", exe: "luajit", version: 5.1, incdir: "/mingw64/include/luajit-2.1/"} + ] + + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v4 + - uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: git + make + mingw-w64-x86_64-toolchain + mingw-w64-x86_64-libvips + mingw-w64-x86_64-openslide + mingw-w64-x86_64-libheif + mingw-w64-x86_64-libjxl + mingw-w64-x86_64-imagemagick + mingw-w64-x86_64-poppler + mingw-w64-x86_64-lua-luarocks + mingw-w64-x86_64-${{ matrix.lua.name }} + + - name: Lua dependencies + run: | + if [[ ${{ matrix.lua.exe }} == lua5.3 ]]; then + cp /mingw64/etc/luarocks/config-5.{4,3}.lua + fi + luarocks config --scope system lua_version ${{ matrix.lua.version }} + luarocks config --scope system lua_interpreter ${{ matrix.lua.exe }}.exe + luarocks config --scope system variables.LUA_DIR /mingw64/bin + luarocks config --scope system variables.LUA_INCDIR ${{ matrix.lua.incdir }} + if [[ ${{ matrix.lua.exe }} == luajit ]]; then + luarocks config --scope system rocks_provided.luaffi-tkl 2.1-1 + fi + + - name: Add to PATH + run: | + echo $RUNNER_TEMP/msys64/mingw64/bin:$HOME/.luarocks/bin >> $GITHUB_PATH + + - name: Install lua-vips + run: | + luarocks make + + - name: Lint with luacheck + run: | + luarocks install luacheck + luacheck.bat -q . + + - name: Busted tests + run: | + luarocks test spec -- --lua=${{ matrix.lua.exe }} -o gtest -v diff --git a/.gitignore b/.gitignore index a01ee28..41a4e53 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ .*.swp +lua-vips-*.src.rock + +# LuaCov +luacov.stats.out +luacov.report.out diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..c3ad42b --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,4 @@ +files["spec/**/*.lua"] = { + std = "+busted", +} +exclude_files = { ".install", ".luarocks", ".lua" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 705f93c..37eeceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,47 @@ All notable changes to `lua-vips` will be documented in this file. +# master + +# 1.1-12 - 2025-02-09 + +- add `vips.Connection`, `vips.Source` and `vips.Target` for true streaming support [rolandlo] + +# 1.1-11 - 2024-04-16 + +- add standard Lua support [rolandlo] +- add `vips.Interpolate` [jcupitt, rolandlo] +- add `vips.concurrency_get()` and `set()` [kamyabzad] +- add `hasalpha`/`addalpha` [RiskoZoSlovenska] + +# 1.1-10 - 2021-04-18 + +- fix NYI warnings [kleisauke] + +# 1.1-9 - 2018-08-03 + +- add `vips.leak_set()` [jcupitt] +- add `soak.lua` example [jcupitt] +- fix five minor memleaks [kleisauke] +- update links for new home [jcupitt] + +# 1.1-8 - 2018-07-25 + +- cleanups and some reorganisation [kleisauke] +- fix regressions from 1.1-7 [kleisauke] +- add `find_load` [kleisauke] +- add `find_load_buffer` [kleisauke] + +# 1.1-7 - 2018-03-23 + +- cleanups and some reorganisation +- renamed cache control funcs, the names were missing the `cache_` prefix +- fix `image:remove()` [kleisauke] + +# 1.1-6 - 2018-03-23 + +- add operation cache control + # 1.1-5 - 2017-10-09 - add verror: handle libvips error buffer @@ -10,6 +51,7 @@ All notable changes to `lua-vips` will be documented in this file. - add `composite` - add `new_from_memory` - add `write_to_memory` +- remove `[]` and `#` overloads -- too confusing, and they broke debuggers # 1.1-4 - 2017-08-30 @@ -33,4 +75,3 @@ All notable changes to `lua-vips` will be documented in this file. # 1.0-1 - 2017-06-04 - first API stable release - diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..031ce0a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 John Cupitt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4f698fa..b1bce31 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,47 @@ # lua-vips +[![CI](https://github.com/libvips/lua-vips/workflows/CI/badge.svg)](https://github.com/libvips/lua-vips/actions) + This is a Lua binding for the [libvips image processing -library](http://jcupitt.github.io/libvips). libvips -is a [fast image processing library with low memory -needs](https://github.com/jcupitt/lua-vips-bench). `lua-vips` uses ffi -and needs luajit 2.0 or later. +library](http://libvips.github.io/libvips). `libvips` +is a [fast image processing library with low memory needs](https://github.com/jcupitt/lua-vips-bench). +`lua-vips` uses ffi and needs either +- luajit >= 2.0 or +- standard Lua (5.1 up to 5.4) combined with the [`luaffi-tkl`](https://luarocks.org/modules/sudheerhebbale/luaffi-tkl) Lua package. + +On the x64 architecture `lua-vips` is continuously tested +- on Linux and MacOS with Lua 5.1, 5.2, 5.3, 5.4 and openresty-luajit +- on Windows using [MSYS2 MinGW-w64](https://www.msys2.org/) with Lua 5.3, 5.4 and luajit -The libvips documentation includes a handy -searchable table of [every operation in -libvips](http://jcupitt.github.io/libvips/API/current/func-list.html). This is -a good place to check if it supports some feature you need. Read on to see how -to call libvips operations. +`lua-vips` should work on arm64 (recently tested on a Pinephone Pro using PostmarketOS) and possibly x86 (currently untested) +as well. + +The libvips documentation includes a handy searchable table of [every operation in +libvips](http://libvips.github.io/libvips/API/current/func-list.html). This +is a good place to check if it supports some feature you need. Read on to +see how to call libvips operations. # Example [Install the libvips shared -library](https://jcupitt.github.io/libvips/install.html), then install this rock with: +library](https://libvips.github.io/libvips/install.html), then install this rock with: - luarocks install lua-vips +```shell +luarocks install lua-vips +``` + +When used with LuaJIT please first exhibit luaffi-tkl as provided by the VM via: +```shell +luarocks config --lua-version=5.1 rocks_provided.luaffi-tkl 2.1-1 +``` Example: ```lua -vips = require "vips" +local vips = require "vips" -- fast thumbnail generator -image = vips.Image.thumbnail("somefile.jpg", 128) +local image = vips.Image.thumbnail("somefile.jpg", 128) image:write_to_file("tiny.jpg") -- make a new image with some text rendered on it @@ -40,7 +56,7 @@ image = image .. image .. image -- add a constant image = image + 12 -- add a different value to each band -image = image + {1, 2, 3} +image = image + { 1, 2, 3 } -- add two images image = image + image @@ -51,38 +67,46 @@ b1, b2, b3 = image:bandsplit() r, g, b = image(10, 20) -- make all pixels less than 128 bright blue -image = image:less(128):ifthenelse({0, 0, 255}, image) +-- :less(128) makes an 8-bit image where each band is 255 (true) if that +-- value is less than 128, and 0 (false) if it's >= 128 ... you can use +--- images or {1,2,3} constants as well as simple values +-- :bandand() joins all image bands together with bitwise AND, so you get a +-- one-band image which is true where all bands are true +-- condition:ifthenelse(then, else) takes a condition image and uses true or +-- false values to pick pixels from the then or else images ... then and +-- else can be constants or images +image = image:less(128):bandand():ifthenelse({ 0, 0, 255 }, image) -- go to Yxy colourspace image = image:colourspace("yxy") -- pass options to a save operation -image:write_to_file("x.png", {compression = 9}) +image:write_to_file("x.png", { compression = 9 }) ``` # How it works libvips has quite a bit of introspection machinery built in. -When you call something like `image:hough_circle{scale = 4}`, the `__index` +When you call something like `image:hough_circle{ scale = 4 }`, the `__index` method on the `lua-vips` image class opens libvips with ffi and searches for an operation called `hough_circle`. It discovers what arguments the operation takes, checks you supplied the correct arguments, and transforms -them into the form that libvips needs. It executes the operator, then -pulls out all the results and returns them as a Lua table. +them into the form that libvips needs. It executes the operator, then pulls +out all the results and returns them as a Lua table. -This means that, although `lua-vips` supports almost 300 operators, -the binding itself is small, should be simple to maintain, and should always be -up to date. +This means that, although `lua-vips` supports almost 300 operators, the +binding itself is small, should be simple to maintain, and should always +be up to date. # Getting more help The libvips website has a handy table of [all the libvips -operators](http://jcupitt.github.io/libvips/API/current/func-list.html). Each +operators](http://libvips.github.io/libvips/API/current/func-list.html). Each one links to the main API docs so you can see what you need to pass to it. -A simple way to see the arguments for an operation is to try running it from the -command-line. For example: +A simple way to see the arguments for an operation is to try running it +from the command-line. For example: ```bash $ vips embed @@ -116,7 +140,7 @@ So you can call `embed` like this: ```lua local image = image:embed(100, 100, image:width() + 200, image:height() + 200, - {extend = "mirror"}) + { extend = "mirror" }) ``` To add a 100 pixel mirror edge around an image. @@ -128,13 +152,14 @@ This section runs through the main features of the binding. To load the binding use: ```lua -vips = require "vips" +local vips = require "vips" ``` ## Make images -You can make images from files or from memory buffers (Lua strings), or you can -use one of the libvips create operators to make an image for you. +You can make images from files or from buffers (Lua strings), you can wrap +a vips image around an ffi array, or you can use one of the libvips create +operators to make an image for you. ### `image = vips.Image.new_from_file(filename [, options])` @@ -142,7 +167,8 @@ Opens the file and returns an image. You can pass a set of options in a final table argument, for example: ```lua -local image = vips.Image.new_from_file("somefile.jpg", {access = "sequential"}) +local image = vips.Image.new_from_file("somefile.jpg", + { access = "sequential" }) ``` Some options are specific to some file types, for example, `shrink`, meaning @@ -153,18 +179,18 @@ You can embed options in filenames using the standard libvips syntax. For example, these are equivalent: ```lua -local image = vips.Image.new_from_file("somefile.jpg", {shrink = 2}) +local image = vips.Image.new_from_file("somefile.jpg", { shrink = 2 }) local image = vips.Image.new_from_file("somefile.jpg[shrink=2]") ``` You can call specific file format loaders directly, for example: ```lua -local image = vips.Image.jpegload("somefile.jpg", {shrink = 4}) +local image = vips.Image.jpegload("somefile.jpg", { shrink = 4 }) ``` The [loader section in the API -docs](http://jcupitt.github.io/libvips/API/current/VipsForeignSave.html) lists +docs](http://libvips.github.io/libvips/API/current/VipsForeignSave.html) lists all loaders and their options. ### `image = vips.Image.new_from_buffer(string [, string_options, options])` @@ -174,15 +200,15 @@ as JPEG. You can supply options, just as with `new_from_file`. These are equivalent: ```lua -local image = vips.Image.new_from_buffer(string, "", {shrink = 2}) +local image = vips.Image.new_from_buffer(string, "", { shrink = 2 }) local image = vips.Image.new_from_buffer(string, "shrink=2") ``` Use (for example) `vips.Image.jpegload_buffer` to call a loader directly. -### `image = vips.Image.new_from_memory(ptr, width, height, bands, format)` +### `image = vips.Image.new_from_memory(data, width, height, bands, format)` -This wraps a libvips image around a FFI memory area. The memory area should be +This wraps a libvips image around a FFI memory array. The memory array should be formatted as a C-style array. Images are always band-interleaved, so an RGB image three pixels across and two pixels down, for example, is laid out as: @@ -200,9 +226,13 @@ local data = ffi.new("unsigned char[?]", width * height) local im = vips.Image.new_from_memory(data, width, height, 1, "uchar") ``` -The returned image is using a pointer to the `data` area, but luajit 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. +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. + +### `image = vips.Image.new_from_memory_ptr(data, size, width, height, bands, format)` + +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. + +A string can be used as the data pointer thanks to FFI semantics. ### `image = vips.Image.new_from_image(image, pixel)` @@ -218,7 +248,7 @@ call it as a member function. `pixel` can be a table to make a many-band image, for example: ```lua -local new_image = image:new_from_image{1, 2, 3} +local new_image = image:new_from_image{ 1, 2, 3 } ``` Will make a new three-band image, where all the red pixels have the value 1, @@ -229,19 +259,19 @@ greens are 2 and blues are 3. Makes a new image from a Lua table. For example: ```lua -local image = vips.Image.new_from_array{1, 2, 3} +local image = vips.Image.new_from_array{ 1, 2, 3 } ``` Makes a one-band image, three pixels across and one high. Use nested tables for -2D images. You can set a scale and offset with two extra number parameters, -which is handy for integer convolution masks. +2D images. You can set a scale and offset with two extra number parameters -- +handy for integer convolution masks. ```lua local mask = vips.Image.new_from_array( {{-1, -1, -1}, {-1, 16, -1}, {-1, -1, -1}}, 8) -local image = image:conv(mask, {precision = "integer"}) +local image = image:conv(mask, { precision = "integer" }) ``` ### `image = vips.Image.copy_memory(self)` @@ -257,12 +287,12 @@ Makes a new one band, 8 bit, black image. You can call any of the libvips image creation operators in this way, for example: ```lua -local noise = vips.Image.perlin(256, 256, {cell_size = 128}) +local noise = vips.Image.perlin(256, 256, { cell_size = 128 }) ``` See: -[http://jcupitt.github.io/libvips/API/current/libvips-create.html](http://jcupitt.github.io/libvips/API/current/libvips-create.html) +[http://libvips.github.io/libvips/API/current/libvips-create.html](http://libvips.github.io/libvips/API/current/libvips-create.html) ## Get and set image metadata @@ -300,22 +330,36 @@ This creates a new metadata item of the specified type, name and value. This changes the value of an existing field, but will not change its type. +You can't use `set()` to change core fields such as like `width` or +`interpretation`. Use `copy()` instead. + +Image references will be shared by the operation cache, so modifying an image +can change an image somewhere else in your program. Before changing an image, +you must make sure you own a private copy of an image with `copy`. + +```lua +local new_image = image:copy() +new_image:set("orientation", 7) +``` + ### `boolean = vips.Image.remove(image, field_name)` This will remove a piece of metadata. It returns `true` if an item was successfully removed, `false` otherwise. +As with `set`, you must use copy before removing a metadata item. + ## Call any libvips operation You can call any libvips operation as a member function, for example `hough_circle`, the circular Hough transform: -[http://jcupitt.github.io/libvips/API/current/libvips-arithmetic.html#vips-hough-circle](http://jcupitt.github.io/libvips/API/current/libvips-arithmetic.html#vips-hough-circle) +[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) Can be called from Lua like this: ```lua -local image2 = image:hough_circle{scale = 2, max_radius = 50} +local image2 = image:hough_circle{ scale = 2, max_radius = 50 } ``` The rules are: @@ -348,7 +392,7 @@ max_value = image:max() ``` To get the maximum value from an image. If you look at [the `max` -operator](http://jcupitt.github.io/libvips/API/current/libvips-arithmetic.html#vips-max), +operator](http://libvips.github.io/libvips/API/current/libvips-arithmetic.html#vips-max), it can actually return a lot more than this. You can write: ```lua @@ -358,7 +402,7 @@ max_value, x, y = image:max() To get the position of the maximum, or: ```lua -max_value, x, y, maxes = image:max{size = 10} +max_value, x, y, maxes = image:max{ size = 10 } ``` and `maxes` will be an array of the top 10 maximum values in order. @@ -375,14 +419,14 @@ image = (image * 2 + 13) % 4 and the appropriate vips operations will be called. You can mix images, number constants, and array constants freely. -The relational operators are not overloaded, unfortunately, Lua does not +The relational operators are not overloaded, unfortunately; Lua does not permit this. You must write something like: ```lua image = image:less(128):ifthenelse(128, image) ``` -to set all pixels less than 128 to 128. +to set all values less than 128 to 128. `__call` (ie. `()`) is overloaded to call the libvips `getpoint` operator. You can write: @@ -396,10 +440,9 @@ and `r`, `g`, `b` will be the RGB values for the pixel at coordinate (10, 10). `..` is overloaded to mean `bandjoin`. -`#` is overloaded to get the number of bands in an image, although this seems -to only work with LuaJIT 2.1. - -`[]` is overloaded to mean `extract_band`. libvips bands number from zero. +Use `im:bands()` to get the number of bands and `im:extract_band(N)` to extract a +band (note bands number from zero). lua-vips does not overload `#` and `[]` for +this, since mixing numbering from zero and one causes confusion. ## Convenience functions @@ -416,7 +459,7 @@ to call --- you must write: ```lua -image = vips.Image.bandjoin{image, image} +image = vips.Image.bandjoin{ image, image } ``` to join an image to itself. Instead, `lua-vips` defines `bandjoin` as a member @@ -429,14 +472,14 @@ image = image:bandjoin(image) to join an image to itself, or perhaps: ```lua -image = R:bandjoin{G, B} +image = R:bandjoin{ G, B } ``` to join three RGB bands. Constants work too, so you can write: ```lua image = image:bandjoin(255) -image = R:bandjoin{128, 23} +image = R:bandjoin{ 128, 23 } ``` The `bandrank` and `composite` operators works in the same way. @@ -469,7 +512,7 @@ There are about 40 of these. ## Write -You can write images to files or to formatted strings. +You can write images to files, to ffi arrays, or to formatted strings. ### `image:write_to_file(filename [, options])` @@ -478,7 +521,7 @@ The filename suffix is used to pick the save operator. Just as with types. You can call savers directly if you wish, for example: ```lua -image:jpegsave("x.jpg", {Q = 90}) +image:jpegsave("x.jpg", { Q = 90 }) ``` ### `string = image:write_to_buffer(suffix [, options])` @@ -488,7 +531,7 @@ The suffix is used to pick the saver that is used to generate the result, so directly if you wish, perhaps: ```lua -local str = image:jpegsave_buffer{Q = 90} +local str = image:jpegsave_buffer{ Q = 90 } ``` ### `memory = image:write_to_memory()` @@ -500,6 +543,32 @@ local mem = image:write_to_memory() print("written ", ffi.sizeof(mem), "bytes to", mem) ``` +### `ptr, size = image:write_to_memory_ptr()` + +An allocated char array pointer (GCd with a `ffi.gc` callback) and the length in bytes of the image data is directly returned from libvips (no intermediate FFI allocation). + +## True Streaming + +When processing images an image library would usually read an image from a file into memory, decode and process it and finally write the encoded result into a file. The processing can only start when the image is fully read into memory and the writing can only start when the processing step is completed. +Libvips can process images directly from a pipe and write directly to a pipe, without the need to read the whole image to memory before being able to start and without the need to finish processing before being able to start writing. This is achieved using a technique called true streaming. In this context there are sources and targets and the processing step happens from source to target. Sources can be created from files, memory or descriptors (like stdin) and targets can be created to files, memory or descriptors (like stdout). Here is an example: + +```lua test.lua +local vips = require "vips" +local stdin, stdout = 0, 1 +local source = vips.Source.new_from_descriptor(stdin) +local target = vips.Target.new_to_descriptor(stdout) +local image = vips.Image.new_from_source(source, '', { access = 'sequential' }) +image = image:invert() +image:write_to_target(target, '.jpg') +``` + +Running this script in a Unix terminal via +```term +curl https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/600px-Cat03.jpg | lua test.lua > out.jpg +``` + +will feed a cat image from the internet into standard input, from which the Lua script reads and inverts it and writes it to standard output, where it is redirected to a file. This all happens simultaneously, so the processing and writing doesn't need to wait until the whole image is downloaded from the internet. + ## Error handling Most `lua-vips` methods will call `error()` if they detect an error. Use @@ -508,97 +577,207 @@ Most `lua-vips` methods will call `error()` if they detect an error. Use Use `get_typeof` to test for a field of a certain name without throwing an error. +## The libvips operation cache + +libvips keeps a cache of recent operations, such as load, save, shrink, and +so on. If you repeat an operation, you'll get the cached result back. + +It keeps track of the number of open files, allocated memory and cached +operations, and will trim the cache if more than 100 files are open at once, +more than 100mb of memory has been allocated, or more than 1,000 operations +are being held. + +Normally this cache is useful and harmless, but for some applications you may +want to change these values. + +```lua +-- set number of cached operations +vips.cache_set_max(100) +-- set maximum cache memory use +vips.cache_set_max_mem(10 * 1024 * 1024) +-- set maximum number of open files +vips.cache_set_max_files(10) +``` + # Development -### Setup for ubuntu 17.04 +### Setup for Ubuntu Configure `luarocks` for a local tree - luarocks help path +```shell +luarocks help path +``` append - eval `luarocks path` - export PATH="$HOME/.luarocks/bin:$PATH" +```shell +eval `luarocks path` +export PATH="$HOME/.luarocks/bin:$PATH" +``` -to `~/.bashrc` +to `~/.bashrc`. ### Install - luarocks --local make +```shell +luarocks --local make +``` ### Unit testing You need: - luarocks --local install busted - luarocks --local install luacov - luarocks --local install say +```shell +luarocks --local install busted +luarocks --local install luacov +luarocks --local install say +``` Then to run the test suite: - (cd spec; for i in *_spec.lua; do luajit $i; done) +```shell +busted . +``` -You can't do `busted .`, unfortunately, since busted `fork()`s between files -and this breaks luajit GC with ffi (I think). +for verbose output: + +```shell +busted . -o gtest -v +``` + +### Linting and static analysis + +You need: + +```shell +luarocks --local install luacheck +``` + +Then to run the linter: + +```shell +luacheck . +``` ### Test Run the example script with: - luajit example/hello-world.lua +```shell +lua example/hello-world.lua +``` ### Update rock - - luarocks upload lua-vips-1.1-5.rockspec --api-key=xxxxxxxxxxxxxx + +```shell +rm *.src.rock +luarocks upload lua-vips-1.1-12.rockspec --api-key=xxxxxxxxxxxxxx +``` ### Links - http://luajit.org/ext_ffi_api.html - http://luajit.org/ext_ffi_semantics.html - https://github.com/luarocks/luarocks/wiki/creating-a-rock - https://olivinelabs.com/busted/ +http://luajit.org/ext_ffi_api.html + +http://luajit.org/ext_ffi_semantics.html + +https://github.com/luarocks/luarocks/wiki/creating-a-rock + +https://olivinelabs.com/busted/ + +### Running on Windows using Mingw-w64 + +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: + +1. Install [MSYS2](https://www.msys2.org/) to the default path. +2. Start Mingw-w64 64bit console from the start menu. Check that is says MINGW64. The following steps happen in that console. +3. Update MSYS2 using + ```shell + pacman -Syuu + ``` +4. Install the build tools (including Lua 5.4 and Luarocks) via + ```shell + pacman -S git make mingw-w64-x86_64-toolchain mingw-w64-x86_64-lua-luarocks + ``` +5. Install `libvips` with (optional) dependencies via + ```shell + pacman -S + mingw-w64-x86_64-libvips + mingw-w64-x86_64-openslide + mingw-w64-x86_64-libheif + mingw-w64-x86_64-libjxl + mingw-w64-x86_64-imagemagick + mingw-w64-x86_64-poppler + ``` +6. Optionally: If you want to use `lua-vips` with LuaJIT instead of Lua 5.4 install LuaJIT via + ```shell + pacman -S mingw-w64-x86_64-luajit + luarocks config --scope system lua_version 5.1 + luarocks config --scope system lua_interpreter luajit.exe + luarocks config --scope system variables.LUA_DIR /mingw64/bin + luarocks config --scope system variables.LUA_INCDIR /mingw64/include/luajit-2.1/ + luarocks config --scope system rocks_provided.luaffi-tkl 2.1-1 + ``` +7. Install `lua-vips` via + ```shell + luarocks install lua-vips + ``` + or clone the repository and run `luarocks make` in the `lua-vips` folder. +8. Add `C:\msys64\mingw64\bin` and `C:\msys64\usr\bin` to the top of your `PATH` + environment variable in the Windows Advanced system settings and restart the console. + +9. Run `lua` or `luajit` and try + ```lua + vips = require "vips" + print(vips.Image.xyz(3,2)) + ``` ### Running under Wine (Windows emulation on Linux) -I used the luapower all-in-one to get a 64-bit Windows LuaJIT build: +@jcupitt used the luapower all-in-one to get a 64-bit Windows LuaJIT build: - https://luapower.com/ +https://luapower.com/ LuaJIT on Windows searches `PATH` to find DLLs. You can't set this directly from Linux, you have to change the registry. See: - https://www.winehq.org/docs/wineusr-guide/environment-variables +https://www.winehq.org/docs/wineusr-guide/environment-variables Then add the `bin` area of the libvips Windows build to `PATH`. - z:\home\john\GIT\build-win64\8.5\vips-dev-8.5\bin +``` +z:\home\john\GIT\build-win64\8.5\vips-dev-8.5\bin +``` You must have no trailing backslash. Try LuaJIT: - $ ~/packages/luajit/luapower-all-master/bin/mingw64/luajit.exe - LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2016 Mike Pall. - http://luajit.org/ - JIT: ON SSE2 SSE3 SSE4.1 fold cse dce fwd dse narrow loop abc sink fuse - > print(os.getenv("PATH")) - C:\windows\system32;C:\windows;C:\windows\system32\wbem;z:\home\john\GIT\build-win64\8.5\vips-dev-8.5\bin - > ffi = require "ffi" - > ffi.load("libvips-42.dll") - > ^D - -The Windows luajit will pick up your .luarocks/share/lua/5.1/vips.lua install, +``` +$ ~/packages/luajit/luapower-all-master/bin/mingw64/luajit.exe +LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2016 Mike Pall. +http://luajit.org/ +JIT: ON SSE2 SSE3 SSE4.1 fold cse dce fwd dse narrow loop abc sink fuse +> print(os.getenv("PATH")) +C:\windows\system32;C:\windows;C:\windows\system32\wbem;z:\home\john\GIT\build-win64\8.5\vips-dev-8.5\bin +> ffi = require "ffi" +> ffi.load("libvips-42.dll") +> ^D +``` + +The Windows luajit will pick up your `.luarocks/share/lua/5.1/vips.lua` install, so to test just install and run: - $ ~/packages/luajit/luapower-all-master/bin/mingw64/luajit.exe - LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2016 Mike Pall. http://luajit.org/ - JIT: ON SSE2 SSE3 SSE4.1 fold cse dce fwd dse narrow loop abc sink fuse - > vips = require "vips" - > x = vips.Image.new_from_file("z:\\data\\john\\pics\\k2.jpg") - > print(x:width()) - 1450 - > x = vips.Image.text("hello", {dpi = 300}) - > x:write_to_file("x.png") - > +``` +$ ~/packages/luajit/luapower-all-master/bin/mingw64/luajit.exe +LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2016 Mike Pall. http://luajit.org/ + JIT: ON SSE2 SSE3 SSE4.1 fold cse dce fwd dse narrow loop abc sink fuse +> vips = require "vips" +> x = vips.Image.new_from_file("z:\\data\\john\\pics\\k2.jpg") +> print(x:width()) +1450 +> x = vips.Image.text("hello", {dpi = 300}) +> x:write_to_file("x.png") +> +``` diff --git a/example/array.lua b/example/array.lua new file mode 100755 index 0000000..4068ca1 --- /dev/null +++ b/example/array.lua @@ -0,0 +1,35 @@ +#!/usr/bin/luajit + +-- turn a vips image into a lua array + +local vips = require "vips" +local ffi = require "ffi" + +-- make a tiny two band u16 image whose pixels are their coordinates +local im = vips.Image.xyz(3, 2):cast("ushort") + +-- write as a C-style memory array, so band-interleaved, a series of scanlines +-- +-- "data" is a pointer of type uchar*, though the underlying memory is really +-- pairs of int16s, see above +local data = im:write_to_memory() + +-- the type of each pixel ... a pair of shorts +ffi.cdef [[ + typedef struct { + unsigned short x; + unsigned short y; + } pixel; +]] +-- and cast the image pointer to a 1D array of pixel structs +local ptype = ffi.typeof("pixel*") +local array = ffi.cast(ptype, data) + +-- and print! ffi arrays number from 0 +for y = 0, im:height() - 1 do + for x = 0, im:width() - 1 do + local i = x + y * im:width() + + print("x = ", x, "y = ", y, "value = ", array[i].x, array[i].y) + end +end diff --git a/example/buffer.lua b/example/buffer.lua new file mode 100755 index 0000000..7fb23aa --- /dev/null +++ b/example/buffer.lua @@ -0,0 +1,24 @@ +#!/usr/bin/luajit + +-- load and save images to and from memory buffers + +local vips = require "vips" + +if #arg ~= 1 then + print("usage: luajit buffer.lua image-file") + error() +end +local f = assert(io.open(arg[1], "rb")) +local content = f:read("*all") + +local im = vips.Image.new_from_buffer(content, "", {access = "sequential"}) + +-- brighten 20% +im = (im * 1.2):cast("uchar") + +-- print as mime jpg +local buffer = im:write_to_buffer(".jpg", {Q = 90}) +print("Content-length: " .. #buffer) +print("Content-type: image/jpeg") +print("") +print(buffer) diff --git a/example/combine.lua b/example/combine.lua index 512e17d..3a0d724 100644 --- a/example/combine.lua +++ b/example/combine.lua @@ -3,26 +3,18 @@ local vips = require "vips" -- uncomment for very chatty output -- vips.log.enable(true) -local main = vips.Image.new_from_file("images/Gugg_coloured.jpg") -local sub = vips.Image.new_from_file("images/watermark.png") -local x, y, width, height = 100, 100, sub:width(), sub:height() +local main_filename = "images/Gugg_coloured.jpg" +local watermark_filename = "images/PNG_transparency_demonstration_1.png" --- extract related area from main image -local extract = main:crop(x, y, width, height) +local main = vips.Image.new_from_file(main_filename) +local watermark = vips.Image.new_from_file(watermark_filename) --- get rgb channels from watermark image -local rgb = sub:extract_band(0, {n = 3}) +-- scale the alpha down to 30% transparency +watermark = watermark * { 1, 1, 1, 0.3 } --- get alpha channel from watermark image -local mask = sub:extract_band(3) - --- use ifthenelse to combine extracted image with sub respecting the --- alpha channel -local composite = mask:ifthenelse(rgb, extract, {blend = true}) - --- insert composite back in to main image on related area -local combined = main:insert(composite, x, y) +-- composite onto the base image at the top left +local result = main:composite(watermark, "over", { x = 10, y = 10 }) print("writing x.jpg ...") -combined:write_to_file("x.jpg") +result:write_to_file("x.jpg") diff --git a/example/hello-world.lua b/example/hello-world.lua index 08e6f69..897a09e 100644 --- a/example/hello-world.lua +++ b/example/hello-world.lua @@ -1,9 +1,9 @@ -vips = require "vips" +local vips = require "vips" -- uncomment for very chatty output -- vips.log.enable(true) -image1 = vips.Image.text("Hello World!", {dpi = 300}) +local image1 = vips.Image.text("Hello World!", { dpi = 300 }) print("writing to x.png ...") image1:write_to_file("x.png") diff --git a/example/images/PNG_transparency_demonstration_1.png b/example/images/PNG_transparency_demonstration_1.png new file mode 100644 index 0000000..3e6aab3 Binary files /dev/null and b/example/images/PNG_transparency_demonstration_1.png differ diff --git a/example/images/watermark.png b/example/images/watermark.png deleted file mode 100644 index 118e2fb..0000000 Binary files a/example/images/watermark.png and /dev/null differ diff --git a/example/noise.lua b/example/noise.lua old mode 100755 new mode 100644 index 59de9ae..6f4e200 --- a/example/noise.lua +++ b/example/noise.lua @@ -1,17 +1,16 @@ -#!/usr/bin/env luajit +local vips = require "vips" -vips = require 'vips' - -size = 1024 +local size = 1024 -- perlin's "turbulence" image -function turbulence(size) - local image = nil - local iterations = math.log(size, 2) - 2 +local function turbulence(turb_size) + local image + local iterations = math.log(turb_size, 2) - 2 for i = 0, iterations do -- make perlin noise at this scale - local layer = vips.Image.perlin(size, size, - {cell_size = size / math.pow(2, i)}) + local layer = vips.Image.perlin(turb_size, turb_size, { + cell_size = turb_size / math.pow(2, i) + }) layer = layer:abs() * (1.0 / (i + 1)) -- and sum @@ -25,25 +24,26 @@ function turbulence(size) return image end --- make a gradient colour map ... a smooth fade from start to stop, with +-- make a gradient colour map ... a smooth fade from start to stop, with -- start and stop as CIELAB colours, then map as sRGB -function gradient(start, stop) +local function gradient(start, stop) local lut = vips.Image.identity() / 255 lut = lut * start + (lut * -1 + 1) * stop - return lut:colourspace("srgb", {source_space = "lab"}) + return lut:colourspace("srgb", { source_space = "lab" }) end -- make a turbulent stripe pattern -stripe = vips.Image.xyz(size, size):extract_band(0) +local stripe = vips.Image.xyz(size, size):extract_band(0) stripe = (stripe * 360 * 4 / size + turbulence(size) * 700):sin() -- make a colour map ... we want a smooth gradient from white to dark brown -- colours here in CIELAB -dark_brown = {7.45, 4.3, 8} -white = {100, 0, 0} -lut = gradient(dark_brown, white) +local dark_brown = { 7.45, 4.3, 8 } +local white = { 100, 0, 0 } +local lut = gradient(dark_brown, white) -- rescale to 0 - 255 and colour with our lut stripe = ((stripe + 1) * 128):maplut(lut) +print("writing x.png ...") stripe:write_to_file("x.png") diff --git a/example/soak.lua b/example/soak.lua new file mode 100644 index 0000000..6b72e2c --- /dev/null +++ b/example/soak.lua @@ -0,0 +1,28 @@ +-- a lua version of +-- https://github.com/libvips/pyvips/blob/master/examples/soak-test.py +-- this should run in a steady amount of memory + +local vips = require "vips" + +vips.leak_set(true) +vips.cache_set_max(0) + +if #arg ~= 2 then + print("usage: luajit soak.lua image-file iterations") + error() +end + +local im + +for i = 0, tonumber(arg[2]) do + print("loop ", i) + + im = vips.Image.new_from_file(arg[1]) + im = im:embed(100, 100, 3000, 3000, { extend = "mirror" }) + -- local buf = im:write_to_buffer(".jpg") + -- im:write_to_file("x.jpg") + im:write_to_file("x.v") + im = nil -- luacheck: ignore + + collectgarbage() +end diff --git a/example/target.lua b/example/target.lua new file mode 100644 index 0000000..f64e2b7 --- /dev/null +++ b/example/target.lua @@ -0,0 +1,18 @@ +local vips = require "vips" + +if #arg ~= 2 then + error("Usage: lua target.lua ~/pics/k2.png .avif > x") +end + +local infilename = arg[1] +local fmt = arg[2] + +local descriptor = { + stdin = 0, + stdout = 1, + stderr = 2, +} + +local image = vips.Image.new_from_file(infilename) +local target = vips.Target.new_to_descriptor(descriptor.stdout) +image:write_to_target(target, fmt) diff --git a/example/watermark.lua b/example/watermark.lua new file mode 100755 index 0000000..537e076 --- /dev/null +++ b/example/watermark.lua @@ -0,0 +1,38 @@ +#!/usr/bin/luajit + +-- add a simple text watermark to an image +-- ./watermark.lua ~/pics/IMG_0073.JPG x.jpg "Hello world!" + +local vips = require "vips" + +-- uncomment for very chatty output +-- vips.log.enable(true) + +if #arg ~= 3 then + print("usage: luajit watermark.lua input-image-file output-image-file text") + error() +end +local im = vips.Image.new_from_file(arg[1], {access = "sequential"}) + +-- make the text mask +local text = vips.Image.text(arg[3], + {width = 200, dpi = 200, align = "centre", font = "sans bold"}) +text = text:rotate(-45) +-- make the text transparent +text = (text * 0.3):cast("uchar") +text = text:gravity("centre", 200, 200) +-- this block of pixels will be reused many times ... make a copy +text = text:copy_memory() +text = text:replicate(1 + math.floor(im:width() / text:width()), + 1 + math.floor(im:height() / text:height())) +text = text:crop(0, 0, im:width(), im:height()) + +-- we make a constant colour image and attach the text mask as the alpha +local overlay = + text:new_from_image({255, 128, 128}):copy{interpretation = "srgb"} +overlay = overlay:bandjoin(text) + +-- overlay the text +im = im:composite(overlay, "over") + +im:write_to_file(arg[2]) diff --git a/lua-vips-1.0-1.src.rock b/lua-vips-1.0-1.src.rock deleted file mode 100644 index 9235c6b..0000000 Binary files a/lua-vips-1.0-1.src.rock and /dev/null differ diff --git a/lua-vips-1.1-1.src.rock b/lua-vips-1.1-1.src.rock deleted file mode 100644 index 8d3b121..0000000 Binary files a/lua-vips-1.1-1.src.rock and /dev/null differ diff --git a/lua-vips-1.1-12.rockspec b/lua-vips-1.1-12.rockspec new file mode 100644 index 0000000..4cdc8ca --- /dev/null +++ b/lua-vips-1.1-12.rockspec @@ -0,0 +1,55 @@ +package = "lua-vips" +version = "1.1-12" +rockspec_format = "3.0" + +source = { + url = "git://github.com/libvips/lua-vips.git", + tag = "v1.1-12", +} + +description = { + summary = "A fast image processing library with low memory needs.", + detailed = [[ + This rock implements a binding for the libvips image processing library. + It is usually faster and needs less memory than similar libraries. + + For use with standard Lua, the dependency luaffi-tkl is used as a drop-in + replacement for LuaJIT's ffi module. + ]], + homepage = "https://github.com/libvips/lua-vips", + license = "MIT", + labels = { "image" } +} + +dependencies = { + "lua >= 5.1, < 5.5", -- standard Lua or LuaJIT >= 2.0 + "luaffi-tkl >= 1.0" -- provided by VM with LuaJIT, use `luarocks config rocks_provided.luaffi-tkl 2.1-1` in that case +} + +test_dependencies = { + "busted" +} + +test = { + type = "busted" +} + +build = { + type = "builtin", + modules = { + vips = "src/vips.lua", + ["vips.cdefs"] = "src/vips/cdefs.lua", + ["vips.verror"] = "src/vips/verror.lua", + ["vips.version"] = "src/vips/version.lua", + ["vips.log"] = "src/vips/log.lua", + ["vips.gvalue"] = "src/vips/gvalue.lua", + ["vips.vobject"] = "src/vips/vobject.lua", + ["vips.voperation"] = "src/vips/voperation.lua", + ["vips.Image"] = "src/vips/Image.lua", + ["vips.Image_methods"] = "src/vips/Image_methods.lua", + ["vips.Interpolate"] = "src/vips/Interpolate.lua", + ["vips.Connection"] = "src/vips/Connection.lua", + ["vips.Source"] = "src/vips/Source.lua", + ["vips.Target"] = "src/vips/Target.lua", + } +} diff --git a/lua-vips-1.1-2.src.rock b/lua-vips-1.1-2.src.rock deleted file mode 100644 index 687313a..0000000 Binary files a/lua-vips-1.1-2.src.rock and /dev/null differ diff --git a/lua-vips-1.1-3.src.rock b/lua-vips-1.1-3.src.rock deleted file mode 100644 index 08dfbb8..0000000 Binary files a/lua-vips-1.1-3.src.rock and /dev/null differ diff --git a/lua-vips-1.1-5.rockspec b/lua-vips-1.1-5.rockspec deleted file mode 100644 index cf76477..0000000 --- a/lua-vips-1.1-5.rockspec +++ /dev/null @@ -1,37 +0,0 @@ -package = "lua-vips" -version = "1.1-5" - -source = { - url = "git://github.com/jcupitt/lua-vips.git" -} - -description = { - summary = "A fast image processing library with low memory needs.", - detailed = [[ - This LuaJIT rock implements a binding for the libvips image - processing library. It is usually faster and needs less memory than - similar libraries. - ]], - homepage = "https://github.com/jcupitt/lua-vips", - license = "MIT" -} - -dependencies = { - "lua >= 5.1", -- "luajit >= 2.0.0" -} - -build = { - type = "builtin", - modules = { - vips = "src/vips.lua", - ["vips.verror"] = "src/vips/verror.lua", - ["vips.version"] = "src/vips/version.lua", - ["vips.log"] = "src/vips/log.lua", - ["vips.gvalue"] = "src/vips/gvalue.lua", - ["vips.vobject"] = "src/vips/vobject.lua", - ["vips.voperation"] = "src/vips/voperation.lua", - ["vips.vimage"] = "src/vips/vimage.lua", - ["vips.Image"] = "src/vips/Image.lua", - ["vips.Image_methods"] = "src/vips/Image_methods.lua" - } -} diff --git a/spec/cache_spec.lua b/spec/cache_spec.lua new file mode 100644 index 0000000..cc7a029 --- /dev/null +++ b/spec/cache_spec.lua @@ -0,0 +1,33 @@ +local vips = require "vips" + +-- test cache control +describe("cache control", function() + + setup(function() + -- vips.log.enable(true) + end) + + it("can set number of operations to cache", function() + local max = vips.get_max() + + vips.set_max(10) + assert.are.equal(vips.get_max(), 10) + vips.set_max(max) + end) + + it("can limit the number of operations to cache by open files", function() + local max = vips.get_max_files() + + vips.set_max_files(10) + assert.are.equal(vips.get_max_files(), 10) + vips.set_max_files(max) + end) + + it("can limit the number of operations to cache by memory", function() + local max = vips.get_max_mem() + + vips.set_max_mem(10) + assert.are.equal(vips.get_max_mem(), 10) + vips.set_max_mem(max) + end) +end) diff --git a/spec/connection_spec.lua b/spec/connection_spec.lua new file mode 100644 index 0000000..44c65be --- /dev/null +++ b/spec/connection_spec.lua @@ -0,0 +1,44 @@ +local vips = require "vips" +local ffi = require "ffi" + +local JPEG_FILE = "./spec/images/Gugg_coloured.jpg" +local TMP_FILE = ffi.os == "Windows" and os.getenv("TMP") .. "\\x.png" or "/tmp/x.png" + +describe("test connection", function() + setup(function() + -- vips.log.enable(true) + end) + + describe("to file target", function() + local target + + setup(function() + target = vips.Target.new_to_file(TMP_FILE) + end) + + it("can create image from file source", function() + local source = vips.Source.new_from_file(JPEG_FILE) + local image = vips.Image.new_from_source(source, '', { access = 'sequential' }) + image:write_to_target(target, '.png') + + local image1 = vips.Image.new_from_file(JPEG_FILE, { access = 'sequential' }) + local image2 = vips.Image.new_from_file(TMP_FILE, { access = 'sequential' }) + assert.is_true((image1 - image2):abs():max() < 10) + end) + + it("can create image from memory source", function() + local file = assert(io.open(JPEG_FILE, "rb")) + local content = file:read("*a") + file:close() + local mem = ffi.new("unsigned char[?]", #content) + ffi.copy(mem, content, #content) + local source = vips.Source.new_from_memory(mem) + local image = vips.Image.new_from_source(source, '', { access = 'sequential' }) + image:write_to_target(target, '.png') + + local image1 = vips.Image.new_from_file(JPEG_FILE, { access = 'sequential' }) + local image2 = vips.Image.new_from_file(TMP_FILE, { access = 'sequential' }) + assert.is_true((image1 - image2):abs():max() < 10) + end) + end) +end) diff --git a/spec/convenience_spec.lua b/spec/convenience_spec.lua index b4fd817..048963a 100644 --- a/spec/convenience_spec.lua +++ b/spec/convenience_spec.lua @@ -1,36 +1,16 @@ --- test image writers - -require 'busted.runner'() - -say = require("say") - -local function almost_equal(state, arguments) - local has_key = false - local threshold = arguments[3] or 0.001 - - if type(arguments[1]) ~= "number" or type(arguments[2]) ~= "number" then - return false - end - - return math.abs(arguments[1] - arguments[2]) < threshold -end - -say:set("assertion.almost_equal.positive", - "Expected %s to almost equal %s") -say:set("assertion.almost_equal.negative", - "Expected %s to not almost equal %s") -assert:register("assertion", "almost_equal", almost_equal, - "assertion.almost_equal.positive", - "assertion.almost_equal.negative") +local vips = require "vips" +-- test convenience functions describe("test convenience functions", function() - vips = require("vips") - -- vips.log.enable(true) + local array, im - local array = {1, 2, 3, 4} - local im = vips.Image.new_from_array(array) + setup(function() + array = { 1, 2, 3, 4 } + im = vips.Image.new_from_array(array) + -- vips.log.enable(true) + end) - it("can join one image bandwise", function () + it("can join one image bandwise", function() local im2 = im:bandjoin(im) assert.are.equal(im2:width(), 4) @@ -38,11 +18,10 @@ describe("test convenience functions", function() assert.are.equal(im2:bands(), 2) assert.are.equal(im2:extract_band(0):avg(), 2.5) assert.are.equal(im2:extract_band(1):avg(), 2.5) - end) - it("can join images bandwise", function () - local im2 = im:bandjoin({im + 1, im + 2}) + it("can join images bandwise", function() + local im2 = im:bandjoin { im + 1, im + 2 } assert.are.equal(im2:width(), 4) assert.are.equal(im2:height(), 1) @@ -50,10 +29,9 @@ describe("test convenience functions", function() assert.are.equal(im2:extract_band(0):avg(), 2.5) assert.are.equal(im2:extract_band(1):avg(), 3.5) assert.are.equal(im2:extract_band(2):avg(), 4.5) - end) - it("can join constants to images bandwise", function () + it("can join constants to images bandwise", function() local im2 = im:bandjoin(255) assert.are.equal(im2:width(), 4) @@ -61,11 +39,10 @@ describe("test convenience functions", function() assert.are.equal(im2:bands(), 2) assert.are.equal(im2:extract_band(0):avg(), 2.5) assert.are.equal(im2:extract_band(1):avg(), 255) - end) - it("can join images and constants bandwise", function () - local im2 = im:bandjoin({im + 1, 255, im + 2}) + it("can join images and constants bandwise", function() + local im2 = im:bandjoin { im + 1, 255, im + 2 } assert.are.equal(im2:width(), 4) assert.are.equal(im2:height(), 1) @@ -74,11 +51,10 @@ describe("test convenience functions", function() assert.are.equal(im2:extract_band(1):avg(), 3.5) assert.are.equal(im2:extract_band(2):avg(), 255) assert.are.equal(im2:extract_band(3):avg(), 4.5) - end) - it("can join images and array constants bandwise", function () - local im2 = im:bandjoin({im + 1, {255, 128}}) + it("can join images and array constants bandwise", function() + local im2 = im:bandjoin { im + 1, { 255, 128 } } assert.are.equal(im2:width(), 4) assert.are.equal(im2:height(), 1) @@ -87,12 +63,11 @@ describe("test convenience functions", function() assert.are.equal(im2:extract_band(1):avg(), 3.5) assert.are.equal(im2:extract_band(2):avg(), 255) assert.are.equal(im2:extract_band(3):avg(), 128) - end) - if vips.version.at_least(8, 6) then - it("can call composite", function () - local base = im + {10, 11, 12} + it("can call composite", function() + if vips.version.at_least(8, 6) then + local base = (im + { 10, 11, 12 }):copy { interpretation = "srgb" } local overlay = (base + 10):bandjoin(128) local comp = base:composite(overlay, "over") local pixel = comp:getpoint(0, 0) @@ -104,48 +79,54 @@ describe("test convenience functions", function() assert.is_true(math.abs(pixel[2] - 17) < 0.1) assert.is_true(math.abs(pixel[3] - 18) < 0.1) assert.are.equal(pixel[4], 255) - end) - end + end + end) - it("can call bandrank", function () - local im2 = im:bandrank(im + 1, {index = 0}) + it("can call bandrank", function() + local im2 = im:bandrank(im + 1, { index = 0 }) assert.are.equal(im2:width(), 4) assert.are.equal(im2:height(), 1) assert.are.equal(im2:bands(), 1) assert.are.equal(im2:extract_band(0):avg(), 2.5) - end) - it("can call bandsplit", function () - local bands = im:bandjoin({im + 1, {255, 128}}):bandsplit() + it("can call bandsplit", function() + local bands = im:bandjoin { im + 1, { 255, 128 } }:bandsplit() assert.are.equal(#bands, 4) assert.are.equal(bands[1]:width(), 4) assert.are.equal(bands[1]:height(), 1) assert.are.equal(bands[1]:bands(), 1) - end) - it("can call ifthenelse with an image and two constants", function () + it("can call ifthenelse with an image and two constants", function() local result = im:more(2):ifthenelse(1, 2) assert.are.equal(result:width(), 4) assert.are.equal(result:height(), 1) assert.are.equal(result:bands(), 1) assert.are.equal(result:avg(), 6 / 4) - end) - it("can call ifthenelse with two images and one constant", function () + it("can call ifthenelse with two images and one constant", function() local result = im:more(2):ifthenelse(im + 3, 2) assert.are.equal(result:width(), 4) assert.are.equal(result:height(), 1) assert.are.equal(result:bands(), 1) assert.are.equal(result:avg(), 17 / 4) + end) + + it("can call hasalpha", function() + local im1 = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") + local im2 = vips.Image.new_from_file("./spec/images/watermark.png") + assert.is_false(im1:hasalpha()) + assert.is_true(im2:hasalpha()) end) + it("can call addalpha", function () + assert.are.equal(im:addalpha():avg(), 128.75) + end) end) - diff --git a/spec/enum_spec.lua b/spec/enum_spec.lua index 89e0b19..61b4892 100644 --- a/spec/enum_spec.lua +++ b/spec/enum_spec.lua @@ -1,13 +1,14 @@ --- test metadata read/write - -require 'busted.runner'() +local vips = require "vips" +-- test metadata read/write describe("enum expansions", function() - vips = require("vips") - --vips.log.enable(true) + local array, im - local array = {1, 2, 3, 4} - local im = vips.Image.new_from_array(array) + setup(function() + array = { 1, 2, 3, 4 } + im = vips.Image.new_from_array(array) + -- vips.log.enable(true) + end) -- there are loads of expansions, just test one of each type @@ -18,7 +19,6 @@ describe("enum expansions", function() assert.are.equal(im2:height(), 1) assert.are.equal(im2:bands(), 1) assert.are.equal(im2:avg(), (1 + 4 + 9 + 16) / 4) - end) it("can call pow() with an image arg", function() @@ -28,7 +28,6 @@ describe("enum expansions", function() assert.are.equal(im2:height(), 1) assert.are.equal(im2:bands(), 1) assert.are.equal(im2:avg(), (1 ^ 1 + 2 ^ 2 + 3 ^ 3 + 4 ^ 4) / 4) - end) it("can call lshift()", function() @@ -38,7 +37,6 @@ describe("enum expansions", function() assert.are.equal(im2:height(), 1) assert.are.equal(im2:bands(), 1) assert.are.equal(im2:avg(), (2 + 4 + 6 + 8) / 4) - end) it("can call less()", function() @@ -48,7 +46,5 @@ describe("enum expansions", function() assert.are.equal(im2:height(), 1) assert.are.equal(im2:bands(), 1) assert.are.equal(im2:avg(), (255 + 0 + 0 + 0) / 4) - end) - end) diff --git a/spec/gvalue_spec.lua b/spec/gvalue_spec.lua index f00a182..a45b265 100644 --- a/spec/gvalue_spec.lua +++ b/spec/gvalue_spec.lua @@ -1,23 +1,79 @@ --- test image new/load/etc. - -require 'busted.runner'() +local vips = require "vips" +-- test gvalue describe("test gvalue", function() - vips = require("vips") - -- vips.log.enable(true) + local im, values + + setup(function() + im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") + values = im:bandsplit() + -- vips.log.enable(true) + end) it("can set/get an int-valued gvalue", function() - local value = vips.gvalue.new() + local value = vips.gvalue() value:init(vips.gvalue.gint_type) value:set(12) - assert.are.equal(value:get(), 12) + assert.are.equal(12, value:get()) end) it("can set/get a string-valued gvalue", function() - local value = vips.gvalue.new() + local value = vips.gvalue() value:init(vips.gvalue.gstr_type) value:set("banana") - assert.are.equal(value:get(), "banana") + assert.are.equal("banana", value:get()) + end) + + it("can set/get a bool-valued gvalue", function() + local value = vips.gvalue() + value:init(vips.gvalue.gbool_type) + value:set(true) + assert.are.equal(1, value:get()) + end) + + it("can set/get a double-valued gvalue", function() + local value = vips.gvalue() + value:init(vips.gvalue.gdouble_type) + value:set(3.1415) + assert.are.equal(3.1415, value:get()) + end) + + it("can set/get a enum-valued gvalue", function() + if vips.version.at_least(8, 6) then + local value = vips.gvalue() + value:init(vips.gvalue.blend_mode_type) + -- need to map str -> int by hand, since the mode arg is actually + -- arrayint + value:set(vips.gvalue.to_enum(vips.gvalue.blend_mode_type, 'over')) + assert.are.equal('over', value:get()) + end + end) + + it("can set/get a array-int-valued gvalue", function() + local value = vips.gvalue() + value:init(vips.gvalue.array_int_type) + value:set({ 1, 2, 3 }) + assert.are.same({ 1, 2, 3 }, value:get()) + end) + + it("can set/get a array-double-valued gvalue", function() + local value = vips.gvalue() + value:init(vips.gvalue.array_double_type) + value:set({ 1.1, 2.1, 3.1 }) + assert.are.same({ 1.1, 2.1, 3.1 }, value:get()) + end) + + it("can set/get a image-valued gvalue", function() + local value = vips.gvalue() + value:init(vips.gvalue.image_type) + value:set(im) + assert.are.same(im, value:get()) end) + it("can set/get a array-image-valued gvalue", function() + local value = vips.gvalue() + value:init(vips.gvalue.array_image_type) + value:set(values) + assert.are.same(values, value:get()) + end) end) diff --git a/spec/helpers.lua b/spec/helpers.lua new file mode 100644 index 0000000..00101f6 --- /dev/null +++ b/spec/helpers.lua @@ -0,0 +1,43 @@ +-- Pre-load the vips module +require "vips" + +local assert = require "luassert.assert" +local say = require "say" + +-- TODO: Wait for https://github.com/Olivine-Labs/luassert/issues/148 +-- Meanwhile, we're applying patch #150 here. + +-- Pre-load the ffi module, such that it becomes part of the environment +-- and Busted will not try to GC and reload it. The ffi is not suited +-- for that and will occasionally segfault if done so. +local ffi = require "ffi" + +-- Patch ffi.cdef to only be called once with each definition, as it +-- will error on re-registering. +local old_cdef = ffi.cdef +local exists = {} +ffi.cdef = function(def) + if exists[def] then + return + end + exists[def] = true + return old_cdef(def) +end + +local function almost_equal(_, arguments) + local threshold = arguments[3] or 0.001 + + if type(arguments[1]) ~= "number" or type(arguments[2]) ~= "number" then + return false + end + + return math.abs(arguments[1] - arguments[2]) < threshold +end + +say:set("assertion.almost_equal.positive", + "Expected %s to almost equal %s") +say:set("assertion.almost_equal.negative", + "Expected %s to not almost equal %s") +assert:register("assertion", "almost_equal", almost_equal, + "assertion.almost_equal.positive", + "assertion.almost_equal.negative") \ No newline at end of file diff --git a/spec/interpolate_spec.lua b/spec/interpolate_spec.lua new file mode 100644 index 0000000..d88c25a --- /dev/null +++ b/spec/interpolate_spec.lua @@ -0,0 +1,33 @@ +local vips = require "vips" + +-- test image interpolation +describe("image interpolation", function() + setup(function() + -- vips.log.enable(true) + end) + + it("can rotate an image using nearest interpolator", function() + local interpolate = vips.Interpolate.new("nearest") + local original = { + { 1, 2, 3 }, + { 4, 5, 6 }, + { 7, 8, 9 }, + } + local rotated = { + { 0.0, 0.0, 1.0, 0.0 }, + { 0.0, 0.0, 1.0, 2.0 }, + { 0.0, 7.0, 5.0, 3.0 }, + { 0.0, 8.0, 9.0, 6.0 } + } + local im = vips.Image.new_from_array(original) + local rot = im:rotate(45, { interpolate = interpolate }) + assert.are.equal(rot:width(), 4) + assert.are.equal(rot:height(), 4) + assert.are.equal(rot:bands(), 1) + for x = 1, 4 do + for y = 1, 4 do + assert.are_equal(rot(x - 1, y - 1), rotated[y][x]) + end + end + end) +end) diff --git a/spec/luacov.stats.out b/spec/luacov.stats.out deleted file mode 100644 index a8fd818..0000000 --- a/spec/luacov.stats.out +++ /dev/null @@ -1,54 +0,0 @@ -163:/home/john/.luarocks/share/lua/5.1/busted/block.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 0 0 0 1 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 0 1 0 0 0 0 0 0 1 0 1 1 0 0 0 0 0 1 1 0 1 1 1 0 1 1 0 0 0 0 0 0 1 0 0 0 0 1 1 0 1 0 1 0 1 1 0 0 0 2 0 1 1 1 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 -38:/home/john/.luarocks/share/lua/5.1/busted/compatibility.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 -124:/home/john/.luarocks/share/lua/5.1/busted/context.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 3 0 0 0 2 2 2 2 2 0 0 2 4 2 2 2 -315:/home/john/.luarocks/share/lua/5.1/busted/core.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 31 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 2 0 4 0 0 0 4 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 2 2 0 0 0 2 2 2 1 1 1 1 1 1 0 4 2 4 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 0 0 0 0 0 1 -72:/home/john/.luarocks/share/lua/5.1/busted/execute.lua -1 1 1 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 1 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 2 1 0 0 0 0 0 1 0 0 1 0 0 0 1 2 2 1 0 1 1 0 1 0 1 0 0 0 2 0 1 1 -127:/home/john/.luarocks/share/lua/5.1/busted/init.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 -49:/home/john/.luarocks/share/lua/5.1/busted/languages/en.lua -1 0 1 0 0 1 1 1 1 0 1 1 1 1 0 1 1 1 1 0 1 1 1 1 0 1 0 1 0 0 1 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 1 -23:/home/john/.luarocks/share/lua/5.1/busted/modules/files/lua.lua -1 0 1 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 1 0 1 -107:/home/john/.luarocks/share/lua/5.1/busted/modules/files/moonscript.lua -1 0 2 1 1 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 1 0 1 -103:/home/john/.luarocks/share/lua/5.1/busted/modules/filter_loader.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 1 0 0 8 14 10 0 0 9 0 1 0 0 0 0 0 0 0 0 1 1 1 1 0 0 1 1 1 1 1 -32:/home/john/.luarocks/share/lua/5.1/busted/modules/output_handler_loader.lua -0 0 0 0 0 1 0 2 1 0 1 0 0 1 0 2 0 1 0 0 0 1 0 0 0 0 1 0 0 0 2 1 -100:/home/john/.luarocks/share/lua/5.1/busted/modules/test_file_loader.lua -1 0 0 1 1 1 1 0 3 2 2 0 0 0 1 0 2 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 1 0 0 1 2 2 0 1 1 0 0 0 0 0 0 0 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 2 0 0 1 1 0 1 1 -179:/home/john/.luarocks/share/lua/5.1/busted/outputHandlers/base.lua -0 1 1 1 0 1 0 1 0 1 0 1 0 0 0 0 1 0 0 1 1 0 1 0 0 0 2 2 2 2 2 2 2 2 2 2 2 0 0 2 1 0 1 0 0 0 0 0 0 1 1 0 0 0 1 1 0 0 0 0 1 1 0 1 1 2 2 1 1 1 0 1 0 1 1 0 0 1 0 0 0 1 1 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 1 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 1 0 0 1 1 2 0 0 1 1 0 1 1 -187:/home/john/.luarocks/share/lua/5.1/busted/outputHandlers/utfTerminal.lua -1 1 0 1 0 2 0 0 0 1 0 0 0 1 1 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 1 0 0 0 0 0 0 1 1 0 0 3 1 3 0 0 1 1 3 2 0 0 0 0 0 0 0 0 1 0 0 0 1 1 0 0 1 1 1 1 0 1 1 1 1 1 0 1 2 0 0 0 0 1 2 0 0 0 0 1 2 0 0 0 0 1 0 1 2 0 0 1 0 2 2 2 2 3 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 1 0 1 1 0 0 1 2 0 1 0 0 0 0 1 0 0 0 0 2 1 2 0 0 1 1 0 0 0 0 0 0 1 0 2 2 2 2 2 2 2 0 1 1 -183:/home/john/.luarocks/share/lua/5.1/busted/runner.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 1 1 0 0 1 1 0 0 0 1 1 0 0 0 0 0 1 1 1 0 2 0 0 1 0 2 0 0 1 0 2 1 1 1 1 0 2 0 0 0 0 0 0 0 1 0 0 1 1 2 0 0 2 1 1 1 1 1 1 1 0 0 0 2 1 1 1 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 1 1 1 2 1 1 1 0 0 0 1 0 0 0 0 0 0 1 1 2 1 1 1 0 0 1 0 1 1 -42:/home/john/.luarocks/share/lua/5.1/busted/status.lua -0 2 0 0 0 0 0 0 0 0 2 0 0 0 2 2 4 2 2 2 0 0 0 2 0 0 0 2 0 0 0 0 0 0 0 2 0 0 2 2 2 2 -24:/home/john/.luarocks/share/lua/5.1/busted/utils.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 5 4 0 1 -214:/home/john/.luarocks/share/lua/5.1/luarocks/loader.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 16 0 16 16 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 9 0 0 0 9 11 10 10 8 8 8 8 0 0 16 8 8 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 9 0 0 0 8 8 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 9 9 1 0 8 8 -258:/home/john/.luarocks/share/lua/5.1/luarocks/path.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 8 8 8 0 0 0 0 0 0 0 0 8 8 8 -154:/home/john/.luarocks/share/lua/5.1/mediator.lua -0 31 0 0 0 31 31 31 0 0 0 0 0 0 31 0 31 31 0 0 0 0 0 19 0 19 19 19 19 0 0 31 31 0 31 0 31 20 20 0 15 0 0 31 0 31 19 0 0 0 0 0 0 0 0 0 0 0 0 19 0 0 0 0 0 0 0 0 19 0 0 38 19 19 0 0 0 19 0 0 82 19 0 0 0 0 0 0 0 0 0 0 0 19 0 0 16 6 0 0 6 0 6 0 6 6 0 0 0 10 6 0 4 0 19 19 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 35 0 98 126 0 0 35 0 0 0 62 0 0 0 0 0 0 0 0 0 0 0 8 -471:/home/john/.luarocks/share/lua/5.1/pl/dir.lua -0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 0 0 1 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 0 0 1 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 -66:/home/john/.luarocks/share/lua/5.1/pl/path.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 1 1 -820:/home/john/.luarocks/share/lua/5.1/pl/tablex.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 1 -83:/home/john/.luarocks/share/lua/5.1/pl/types.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 -441:/home/john/.luarocks/share/lua/5.1/pl/utils.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 3 0 0 3 0 0 0 0 0 0 2 2 -48:/home/john/.luarocks/share/lua/5.1/say/init.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 18 18 0 0 0 0 12 0 12 0 12 0 0 12 12 0 12 0 0 0 12 -39:/home/john/.luarocks/share/lua/5.1/term/colors.lua -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 26 0 0 0 52 0 0 0 26 diff --git a/spec/meta_spec.lua b/spec/meta_spec.lua index 2fcf8fe..9010188 100644 --- a/spec/meta_spec.lua +++ b/spec/meta_spec.lua @@ -1,14 +1,16 @@ --- test metadata read/write - -require 'busted.runner'() +local vips = require "vips" +local ffi = require "ffi" +-- test metadata read/write describe("metadata", function() - vips = require("vips") - --vips.log.enable(true) + local array, im + local tmp_vips_filename = ffi.os == "Windows" and os.getenv("TMP") .. "\\x.v" or "/tmp/x.v" - local array = {1, 2, 3, 4} - local im = vips.Image.new_from_array(array) - local tmp_vips_filename = "/tmp/x.v" + setup(function() + array = { 1, 2, 3, 4 } + im = vips.Image.new_from_array(array) + -- vips.log.enable(true) + end) teardown(function() os.remove(tmp_vips_filename) @@ -21,6 +23,14 @@ describe("metadata", function() assert.are.equal(im2:get("banana"), 12) end) + it("can remove metadata", function() + local im2 = im:copy() + + im2:set_type(vips.gvalue.gint_type, "banana", 12) + im2:remove("banana") + assert.are.equal(im2:get_typeof("banana"), 0) + end) + it("can set/get double", function() local im2 = im:copy() @@ -49,5 +59,4 @@ describe("metadata", function() assert.are.same(im2:format(), "double") end) - end) diff --git a/spec/new_spec.lua b/spec/new_spec.lua index 2d2165b..9a345ad 100644 --- a/spec/new_spec.lua +++ b/spec/new_spec.lua @@ -1,35 +1,19 @@ --- test image new/load/etc. - -require 'busted.runner'() - -say = require("say") -ffi = require("ffi") - -local function almost_equal(state, arguments) - local has_key = false - local threshold = arguments[3] or 0.001 - - if type(arguments[1]) ~= "number" or type(arguments[2]) ~= "number" then - return false - end - - return math.abs(arguments[1] - arguments[2]) < threshold -end - -say:set("assertion.almost_equal.positive", "Expected %s to almost equal %s") -say:set("assertion.almost_equal.negative", "Expected %s to not almost equal %s") -assert:register("assertion", "almost_equal", almost_equal, - "assertion.almost_equal.positive", - "assertion.almost_equal.negative") +local ffi = require("ffi") +ffi.cdef[[ +void* malloc(size_t size); +void free(void *ptr); +]] +local vips = require "vips" +-- test image new/load/etc. describe("test image creation", function() - vips = require("vips") - -- vips.log.enable(true) + setup(function() + -- vips.log.enable(true) + end) describe("test image from array", function() - it("can make an image from a 1D array", function() - local array = {1, 2, 3, 4} + local array = { 1, 2, 3, 4 } local im = vips.Image.new_from_array(array) assert.are.equal(im:width(), 4) @@ -40,7 +24,7 @@ describe("test image creation", function() end) it("can make an image from a 2D array", function() - local array = {{1, 2}, {3, 4}} + local array = { { 1, 2 }, { 3, 4 } } local im = vips.Image.new_from_array(array) assert.are.equal(im:width(), 2) @@ -51,7 +35,7 @@ describe("test image creation", function() end) it("can set scale and offset on an array", function() - local array = {{1, 2}, {3, 4}} + local array = { { 1, 2 }, { 3, 4 } } local im = vips.Image.new_from_array(array, 12, 3) assert.are.equal(im:width(), 2) @@ -60,22 +44,26 @@ describe("test image creation", function() assert.are.equal(im:get("offset"), 3) assert.are.equal(im:avg(), 2.5) end) - end) describe("test image from file", function() - it("can load a jpeg from a file", function() - local im = vips.Image.new_from_file("images/Gugg_coloured.jpg") + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") assert.are.equal(im:width(), 972) assert.are.equal(im:height(), 1296) assert.are.almost_equal(im:avg(), 113.96) end) + it("throws error when file does not exits", function() + assert.has_error(function() + vips.Image.new_from_file("/path/does/not/exist/unknown.jpg") + end, "VipsForeignLoad: file \"/path/does/not/exist/unknown.jpg\" does not exist\n") + end) + it("can subsample a jpeg from a file", function() - local im = vips.Image.new_from_file("images/Gugg_coloured.jpg", - {shrink = 2}) + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg", + { shrink = 2 }) assert.are.equal(im:width(), 486) assert.are.equal(im:height(), 648) @@ -83,20 +71,17 @@ describe("test image creation", function() end) it("can subsample a jpeg from a file, shrink in filename", function() - local im = vips.Image.new_from_file( - "images/Gugg_coloured.jpg[shrink=2]") + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg[shrink=2]") assert.are.equal(im:width(), 486) assert.are.equal(im:height(), 648) assert.are.almost_equal(im:avg(), 113.979) end) - end) describe("test image from buffer", function() - it("can write a jpeg to buffer", function() - local im = vips.Image.new_from_file("images/Gugg_coloured.jpg") + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") local buf = im:write_to_buffer(".jpg") local f = io.open("x.jpg", "w+b") f:write(buf) @@ -113,8 +98,8 @@ describe("test image creation", function() end) it("can load a jpeg from a buffer", function() - local im = vips.Image.new_from_file("images/Gugg_coloured.jpg") - local f = io.open("images/Gugg_coloured.jpg", "rb") + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") + local f = io.open("./spec/images/Gugg_coloured.jpg", "rb") local buf = f:read("*all") f:close() local im2 = vips.Image.new_from_buffer(buf) @@ -126,13 +111,21 @@ describe("test image creation", function() assert.are.equal(im:yres(), im2:yres()) end) + it("throws error when loading from unknown buffer", function() + local buf = "GIF89a" + + assert.error_matches(function() + vips.Image.new_from_buffer(buf) + end, "unable to call VipsForeignLoadNsgifBuffer\ngifload_buffer: .+") + end) + it("can load a jpeg from a buffer, options in a table", function() - local im = vips.Image.new_from_file("images/Gugg_coloured.jpg", - {shrink = 2}) - local f = io.open("images/Gugg_coloured.jpg", "rb") + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg", + { shrink = 2 }) + local f = io.open("./spec/images/Gugg_coloured.jpg", "rb") local buf = f:read("*all") f:close() - local im2 = vips.Image.new_from_buffer(buf, "", {shrink = 2}) + local im2 = vips.Image.new_from_buffer(buf, "", { shrink = 2 }) assert.are.equal(im:width(), im2:width()) assert.are.equal(im:height(), im2:height()) @@ -142,9 +135,9 @@ describe("test image creation", function() end) it("can load a jpeg from a buffer, options in a table", function() - local im = vips.Image.new_from_file("images/Gugg_coloured.jpg", - {shrink = 2}) - local f = io.open("images/Gugg_coloured.jpg", "rb") + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg", + { shrink = 2 }) + local f = io.open("./spec/images/Gugg_coloured.jpg", "rb") local buf = f:read("*all") f:close() local im2 = vips.Image.new_from_buffer(buf, "shrink=2") @@ -155,11 +148,9 @@ describe("test image creation", function() assert.are.equal(im:xres(), im2:xres()) assert.are.equal(im:yres(), im2:yres()) end) - end) describe("test image from memory", function() - it("can make an image from a memory area", function() local width = 64 local height = 32 @@ -172,7 +163,7 @@ describe("test image creation", function() end end - local im = vips.Image.new_from_memory(data, + local im = vips.Image.new_from_memory(data, width, height, 1, "uchar") assert.are.equal(im:width(), width) @@ -182,10 +173,30 @@ describe("test image creation", function() assert.are.equal(im:avg(), 47) end) + it("can make an image from a memory area (pointer)", function() + local width = 64 + local height = 32 + local size = width * height + local data = ffi.gc(ffi.cast("unsigned char*", ffi.C.malloc(size)), ffi.C.free) + + for y = 0, height - 1 do + for x = 0, width - 1 do + data[x + y * width] = x + y + end + end + + local im = vips.Image.new_from_memory_ptr(data, + size, width, height, 1, "uchar") + + assert.are.equal(im:width(), width) + assert.are.equal(im:height(), height) + assert.are.equal(im:bands(), 1) + assert.are.equal(im:format(), "uchar") + assert.are.equal(im:avg(), 47) + end) end) describe("test vips creators", function() - it("can call vips_black()", function() local im = vips.Image.black(1, 1) @@ -193,21 +204,23 @@ describe("test image creation", function() assert.are.equal(im:height(), 1) assert.are.equal(im:bands(), 1) assert.are.equal(im:avg(), 0) - end) it("can call operations with - in option names", function() - local im = vips.Image.perlin(100, 100, {cell_size = 10}) + local im = vips.Image.perlin(100, 100, { cell_size = 10 }) assert.are.equal(im:width(), 100) assert.are.equal(im:height(), 100) assert.are.equal(im:bands(), 1) end) - end) describe("test image from image", function() - local im = vips.Image.new_from_file("images/Gugg_coloured.jpg") + local im + + setup(function() + im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") + end) it("can make a one-band constant image", function() local im2 = im:new_from_image(12) @@ -220,11 +233,10 @@ describe("test image creation", function() assert.are.equal(im:yres(), im2:yres()) assert.are.equal(im2:bands(), 1) assert.are.equal(im2:avg(), 12) - end) it("can make a many-band constant image", function() - local im2 = im:new_from_image({1, 2, 3, 4}) + local im2 = im:new_from_image({ 1, 2, 3, 4 }) assert.are.equal(im:width(), im2:width()) assert.are.equal(im:height(), im2:height()) @@ -234,9 +246,6 @@ describe("test image creation", function() assert.are.equal(im:yres(), im2:yres()) assert.are.equal(im2:bands(), 4) assert.are.equal(im2:avg(), 2.5) - end) - end) - end) diff --git a/spec/overloads_spec.lua b/spec/overloads_spec.lua index 4d5e2f3..398f8ce 100644 --- a/spec/overloads_spec.lua +++ b/spec/overloads_spec.lua @@ -1,27 +1,6 @@ --- test all operator overloads - -require 'busted.runner'() - -say = require("say") - -local function almost_equal(state, arguments) - local has_key = false - local threshold = arguments[3] or 0.001 +local vips = require "vips" - if type(arguments[1]) ~= "number" or type(arguments[2]) ~= "number" then - return false - end - - return math.abs(arguments[1] - arguments[2]) < threshold -end - -say:set("assertion.almost_equal.positive", - "Expected %s to almost equal %s") -say:set("assertion.almost_equal.negative", - "Expected %s to not almost equal %s") -assert:register("assertion", "almost_equal", almost_equal, - "assertion.almost_equal.positive", - "assertion.almost_equal.negative") +-- test all operator overloads -- make a table of x repeated n times local function replicate(x, n) @@ -112,12 +91,16 @@ local function avg(a) end local function test_binary(name, vop, lop) - local array = {1, 2, 3, 4} - local im = vips.Image.new_from_array(array) + local array, im + + setup(function() + array = { 1, 2, 3, 4 } + im = vips.Image.new_from_array(array) + end) describe(name, function() - it("can " .. name .. " image and single constant", function () + it("can " .. name .. " image and single constant", function() local im2 = vop(im, 12) local a2 = map2(lop, array, 12) @@ -127,7 +110,7 @@ local function test_binary(name, vop, lop) assert.are.almost_equal(im2:avg(), avg(a2)) end) - it("can " .. name .. " image and single constant, reversed", function () + it("can " .. name .. " image and single constant, reversed", function() local im2 = vop(12, im) local a2 = map2(lop, 12, array) @@ -138,7 +121,7 @@ local function test_binary(name, vop, lop) end) it("can " .. name .. " an image and an array", function() - local array_constant = {12, 13, 14} + local array_constant = { 12, 13, 14 } local im2 = vop(im, array_constant) local a2 = map2(lop, array, replicate(array_constant, #array)) @@ -149,7 +132,7 @@ local function test_binary(name, vop, lop) end) it("can " .. name .. " an image and an array, reversed", function() - local array_constant = {12, 13, 14} + local array_constant = { 12, 13, 14 } local im2 = vop(array_constant, im) local a2 = map2(lop, replicate(array_constant, #array), array) @@ -168,18 +151,19 @@ local function test_binary(name, vop, lop) assert.are.equal(im2:bands(), 1) assert.are.almost_equal(im2:avg(), avg(a2)) end) - end) - end local function test_binary_noreverse(name, vop, lop) - local array = {1, 2, 3, 4} - local im = vips.Image.new_from_array(array) + local array, im - describe(name, function() + setup(function() + array = { 1, 2, 3, 4 } + im = vips.Image.new_from_array(array) + end) - it("can " .. name .. " image and single constant", function () + describe(name, function() + it("can " .. name .. " image and single constant", function() local im2 = vop(im, 12) local a2 = map2(lop, array, 12) @@ -190,7 +174,7 @@ local function test_binary_noreverse(name, vop, lop) end) it("can " .. name .. " an image and an array", function() - local im2 = vop(im, {12, 13, 14}) + local im2 = vop(im, { 12, 13, 14 }) local a2 = map2(lop, array, 13) assert.are.equal(im2:width(), 4) @@ -208,14 +192,16 @@ local function test_binary_noreverse(name, vop, lop) assert.are.equal(im2:bands(), 1) assert.are.almost_equal(im2:avg(), avg(a2)) end) - end) - end local function test_unary(name, vop, lop) - local array = {1, 2, 3, 4} - local im = vips.Image.new_from_array(array) + local array, im + + setup(function() + array = { 1, 2, 3, 4 } + im = vips.Image.new_from_array(array) + end) describe(name, function() it("can " .. name .. " an image", function() @@ -227,83 +213,76 @@ local function test_unary(name, vop, lop) assert.are.equal(im2:bands(), 1) assert.are.almost_equal(im2:avg(), avg(a2)) end) - end) end describe("test overload", function() - vips = require("vips") - -- vips.log.enable(true) - - test_binary("add", + test_binary("add", function(a, b) return vips.Image.mt.__add(a, b) end, function(a, b) return a + b - end - ) + end) - test_binary("sub", + test_binary("sub", function(a, b) return vips.Image.mt.__sub(a, b) end, function(a, b) return a - b - end - ) + end) - test_binary("mul", + test_binary("mul", function(a, b) return vips.Image.mt.__mul(a, b) end, function(a, b) return a * b - end - ) + end) - test_binary("div", + test_binary("div", function(a, b) return vips.Image.mt.__div(a, b) end, function(a, b) return a / b - end - ) + end) - test_binary_noreverse("mod", + test_binary_noreverse("mod", function(a, b) return vips.Image.mt.__mod(a, b) end, function(a, b) return a % b - end - ) + end) - test_binary("pow", + test_binary("pow", function(a, b) return vips.Image.mt.__pow(a, b) end, function(a, b) return a ^ b - end - ) + end) - test_unary("unm", + test_unary("unm", function(a) return vips.Image.mt.__unm(a) end, function(a) return -a - end - ) + end) describe("band overloads", function() - local array = {1, 2, 3, 4} - local im = vips.Image.new_from_array(array) - local im2 = im:bandjoin({im + 1, im + 2}) + local array, im, im2 - it("can bandjoin with '..'", function () + setup(function() + array = { 1, 2, 3, 4 } + im = vips.Image.new_from_array(array) + im2 = im:bandjoin({ im + 1, im + 2 }) + end) + + it("can bandjoin with '..'", function() local b = im .. im2 assert.are.equal(b:width(), 4) @@ -311,38 +290,23 @@ describe("test overload", function() assert.are.equal(b:bands(), 4) assert.are.equal(b:extract_band(0):avg(), 2.5) end) - - it("can extract bands with '[]'", function () - local b = im2[0] - - assert.are.equal(b:width(), 4) - assert.are.equal(b:height(), 1) - assert.are.equal(b:bands(), 1) - assert.are.equal(b:avg(), 2.5) - end) - - -- works fine with 2.1, fails with 2.0, mysterious! - pending("can count bands with '#' ... needs luajit 2.1", function () - local n = #im2 - - assert.are.equal(n, 3) - end) - end) describe("call overload", function() - local array = {1, 2, 3, 4} - local im = vips.Image.new_from_array(array) - local im2 = im:bandjoin({im + 1, im + 2}) + local array, im, im2 + + setup(function() + array = { 1, 2, 3, 4 } + im = vips.Image.new_from_array(array) + im2 = im:bandjoin({ im + 1, im + 2 }) + end) - it("can extract a pixel with '()'", function () + it("can extract a pixel with '()'", function() local a, b, c = im2(1, 0) assert.are.equal(a, 2) assert.are.equal(b, 3) assert.are.equal(c, 4) end) - end) - end) diff --git a/spec/write_spec.lua b/spec/write_spec.lua index efd7f98..01cbe7d 100644 --- a/spec/write_spec.lua +++ b/spec/write_spec.lua @@ -1,141 +1,123 @@ --- test image writers - -require 'busted.runner'() - -say = require("say") -ffi = require("ffi") - -local function almost_equal(state, arguments) - local has_key = false - local threshold = arguments[3] or 0.001 - - if type(arguments[1]) ~= "number" or type(arguments[2]) ~= "number" then - return false - end - - return math.abs(arguments[1] - arguments[2]) < threshold -end - -say:set("assertion.almost_equal.positive", - "Expected %s to almost equal %s") -say:set("assertion.almost_equal.negative", - "Expected %s to not almost equal %s") -assert:register("assertion", "almost_equal", almost_equal, - "assertion.almost_equal.positive", - "assertion.almost_equal.negative") - -describe("test image write to file", function() - vips = require("vips") - -- vips.log.enable(true) - - local array = {1, 2, 3, 4} - local im = vips.Image.new_from_array(array) - local tmp_png_filename = "/tmp/x.png" - local tmp_jpg_filename = "/tmp/x.jpg" - - teardown(function() - os.remove(tmp_png_filename) - os.remove(tmp_jpg_filename) - end) - - it("can save and then load a png", function() - im:write_to_file(tmp_png_filename) - local im2 = vips.Image.new_from_file(tmp_png_filename) - - assert.are.equal(im:width(), im2:width()) - assert.are.equal(im:height(), im2:height()) - assert.are.equal(im:avg(), im2:avg()) - end) - - it("can save and then load a jpg with an option", function() - im:write_to_file(tmp_jpg_filename, {Q = 90}) - local im2 = vips.Image.new_from_file(tmp_jpg_filename) - - assert.are.equal(im:width(), im2:width()) - assert.are.equal(im:height(), im2:height()) - assert.are.almost_equal(im:avg(), im2:avg()) - end) - -end) +local ffi = require("ffi") +local vips = require "vips" -describe("test image write to buffer", function() - vips = require("vips") - -- vips.log.enable(true) - - it("can write a jpeg to buffer", function() - local im = vips.Image.new_from_file("images/Gugg_coloured.jpg") - local buf = im:write_to_buffer(".jpg") - local f = io.open("x.jpg", "w+b") - f:write(buf) - f:close() - local im2 = vips.Image.new_from_file("x.jpg") - - assert.are.equal(im:width(), im2:width()) - assert.are.equal(im:height(), im2:height()) - assert.are.equal(im:format(), im2:format()) - assert.are.equal(im:xres(), im2:xres()) - assert.are.equal(im:yres(), im2:yres()) - -- remove test file - os.remove("x.jpg") - end) - - it("can write a jpeg to buffer with an option", function() - local im = vips.Image.new_from_file("images/Gugg_coloured.jpg") - local buf = im:write_to_buffer(".jpg") - local buf2 = im:write_to_buffer(".jpg", {Q = 100}) - - assert.is.True(#buf2 > #buf) - end) - -end) - -describe("test image write to buffer", function() - vips = require("vips") - -- vips.log.enable(true) - - it("can write an image to a memory area", function() - local im = vips.Image.new_from_file("images/Gugg_coloured.jpg") - local mem = im:write_to_memory() +-- test image writers +describe("test image write", function() - assert.are.equal(im:width() * im:height() * 3, ffi.sizeof(mem)) + setup(function() + -- vips.log.enable(true) end) - it("can read an image back from a memory area", function() - local im = vips.Image.new_from_file("images/Gugg_coloured.jpg") - local mem = im:write_to_memory() - assert.are.equal(im:width() * im:height() * 3, ffi.sizeof(mem)) - local im2 = vips.Image.new_from_memory(mem, - im:width(), im:height(), im:bands(), im:format()) - - assert.are.equal(im:avg(), im2:avg()) + describe("to file", function() + local array = { 1, 2, 3, 4 } + local im = vips.Image.new_from_array(array) + local tmp_png_filename = ffi.os == "Windows" and os.getenv("TMP") .. "\\x.png" or "/tmp/x.png" + local tmp_jpg_filename = ffi.os == "Windows" and os.getenv("TMP") .. "\\x.jpg" or "/tmp/x.jpg" + + teardown(function() + os.remove(tmp_png_filename) + os.remove(tmp_jpg_filename) + end) + + it("can save and then load a png", function() + im:write_to_file(tmp_png_filename) + local im2 = vips.Image.new_from_file(tmp_png_filename) + + assert.are.equal(im:width(), im2:width()) + assert.are.equal(im:height(), im2:height()) + assert.are.equal(im:avg(), im2:avg()) + end) + + it("can save and then load a jpg with an option", function() + im:write_to_file(tmp_jpg_filename, { Q = 90 }) + local im2 = vips.Image.new_from_file(tmp_jpg_filename) + + assert.are.equal(im:width(), im2:width()) + assert.are.equal(im:height(), im2:height()) + assert.are.almost_equal(im:avg(), im2:avg()) + end) end) -end) - -describe("MODIFY args", function() - vips = require("vips") - -- vips.log.enable(true) - - it("can draw a circle on an image", function() - local im = vips.Image.black(101, 101) - local im2 = im:draw_circle(255, 50, 50, 50, {fill = true}) - - assert.are.equal(im2:width(), 101) - assert.are.equal(im2:height(), 101) - assert.are.almost_equal(im2:avg(), 255 * 3.1415927 / 4, 0.2) - + describe("to buffer", function() + it("can write a jpeg to buffer", function() + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") + local buf = im:write_to_buffer(".jpg") + local f = io.open("x.jpg", "w+b") + f:write(buf) + f:close() + local im2 = vips.Image.new_from_file("x.jpg") + + assert.are.equal(im:width(), im2:width()) + assert.are.equal(im:height(), im2:height()) + assert.are.equal(im:format(), im2:format()) + assert.are.equal(im:xres(), im2:xres()) + assert.are.equal(im:yres(), im2:yres()) + -- remove test file + os.remove("x.jpg") + end) + + it("can write a jpeg to buffer with an option", function() + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") + local buf = im:write_to_buffer(".jpg") + local buf2 = im:write_to_buffer(".jpg", { Q = 100 }) + + assert.is.True(#buf2 > #buf) + end) + + it("can write an image to a memory area", function() + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") + local mem = im:write_to_memory() + + assert.are.equal(im:width() * im:height() * 3, ffi.sizeof(mem)) + end) + + it("can read an image back from a memory area", function() + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") + local mem = im:write_to_memory() + assert.are.equal(im:width() * im:height() * 3, ffi.sizeof(mem)) + local im2 = vips.Image.new_from_memory(mem, + im:width(), im:height(), im:bands(), im:format()) + + assert.are.equal(im:avg(), im2:avg()) + end) + + it("can write an image to a memory area (no copy)", function() + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") + local _, size = im:write_to_memory_ptr() + + assert.are.equal(im:width() * im:height() * 3, size) + end) + + it("can read an image back from a memory area (no copy)", function() + local im = vips.Image.new_from_file("./spec/images/Gugg_coloured.jpg") + local ptr, size = im:write_to_memory_ptr() + assert.are.equal(im:width() * im:height() * 3, size) + local im2 = vips.Image.new_from_memory_ptr(ptr, + size, im:width(), im:height(), im:bands(), im:format()) + + assert.are.equal(im:avg(), im2:avg()) + end) end) - it("each draw op makes a new image", function() - local im = vips.Image.black(101, 101) - local im2 = im:draw_circle(255, 50, 50, 50, {fill = true}) - local im3 = im2:draw_circle(0, 50, 50, 40, {fill = true}) - - assert.are.equal(im2:width(), 101) - assert.are.equal(im2:height(), 101) - assert.are.almost_equal(im2:avg(), 255 * 3.1415927 / 4, 0.2) - assert.is.True(im3:avg() < im2:avg()) - + describe("MODIFY args", function() + it("can draw a circle on an image", function() + local im = vips.Image.black(101, 101) + local im2 = im:draw_circle(255, 50, 50, 50, { fill = true }) + + assert.are.equal(im2:width(), 101) + assert.are.equal(im2:height(), 101) + assert.are.almost_equal(im2:avg(), 255 * 3.1415927 / 4, 0.2) + end) + + it("each draw op makes a new image", function() + local im = vips.Image.black(101, 101) + local im2 = im:draw_circle(255, 50, 50, 50, { fill = true }) + local im3 = im2:draw_circle(0, 50, 50, 40, { fill = true }) + + assert.are.equal(im2:width(), 101) + assert.are.equal(im2:height(), 101) + assert.are.almost_equal(im2:avg(), 255 * 3.1415927 / 4, 0.2) + assert.is.True(im3:avg() < im2:avg()) + end) end) - end) diff --git a/src/vips.lua b/src/vips.lua index 1adefab..ace8231 100644 --- a/src/vips.lua +++ b/src/vips.lua @@ -1,30 +1,75 @@ -- top include for lua-vips -local ffi = require "ffi" +local ffi = require "ffi" -local vips = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") +local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") -ffi.cdef[[ - int vips_init (const char* argv0); +require "vips.cdefs" -]] - -local result = vips.vips_init("") +local result = vips_lib.vips_init("lua-vips") if result ~= 0 then - local errstr = ffi.string(vips.vips_error_buffer()) - vips.vips_error_clear() + local errstr = ffi.string(vips_lib.vips_error_buffer()) + vips_lib.vips_error_clear() error("unable to start up libvips: " .. errstr) end -return { - verror = require "vips/verror", - version = require "vips/version", - log = require "vips/log", - gvalue = require "vips/gvalue", - vobject = require "vips/vobject", - voperation = require "vips/voperation", - vimage = require "vips/vimage", - Image = require "vips/Image", - require "vips/Image_methods", +local vips = { + verror = require "vips.verror", + version = require "vips.version", + log = require "vips.log", + gvalue = require "vips.gvalue", + vobject = require "vips.vobject", + voperation = require "vips.voperation", + Image = require "vips.Image_methods", + Interpolate = require "vips.Interpolate", + Connection = require "vips.Connection", + Source = require "vips.Source", + Target = require "vips.Target", } + +function vips.leak_set(leak) + vips_lib.vips_leak_set(leak) +end + +function vips.cache_set_max(max) + vips_lib.vips_cache_set_max(max) +end + +function vips.cache_get_max() + return vips_lib.vips_cache_get_max() +end + +function vips.cache_set_max_files(max) + vips_lib.vips_cache_set_max_files(max) +end + +function vips.cache_get_max_files() + return vips_lib.vips_cache_get_max_files() +end + +function vips.cache_set_max_mem(max) + vips_lib.vips_cache_set_max_mem(max) +end + +function vips.cache_get_max_mem() + return vips_lib.vips_cache_get_max_mem() +end + +function vips.concurrency_set(concurrency) + return vips_lib.vips_concurrency_set(concurrency) +end + +function vips.concurrency_get() + return vips_lib.vips_concurrency_get() +end + +-- for compat with 1.1-6, when these were misnamed +vips.set_max = vips.cache_set_max +vips.get_max = vips.cache_get_max +vips.set_max_files = vips.cache_set_max_files +vips.get_max_files = vips.cache_get_max_files +vips.set_max_mem = vips.cache_set_max_mem +vips.get_max_mem = vips.cache_get_max_mem + +return vips diff --git a/src/vips/Connection.lua b/src/vips/Connection.lua new file mode 100644 index 0000000..66d7605 --- /dev/null +++ b/src/vips/Connection.lua @@ -0,0 +1,55 @@ +-- abstract base Connection class + +local ffi = require "ffi" + +local vobject = require "vips.vobject" + +local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") + +local Connection_method = {} + +local Connection = { + mt = { + __index = Connection_method, + } +} + +function Connection.mt:__tostring() + return self:filename() or self:nick() or "(nil)" +end + +Connection.new = function(vconnection) + local connection = {} + connection.vconnection = vobject.new(vconnection) + return setmetatable(connection, Connection.mt) +end +function Connection_method:vobject() + return ffi.cast(vobject.typeof, self.vconnection) +end + +function Connection_method:filename() + -- Get the filename asscoiated with a connection. Return nil if there is no associated file. + local so = ffi.cast('VipsConnection *', self.vconnection) + local filename = vips_lib.vips_connection_filename(so) + if filename == ffi.NULL then + return nil + else + return ffi.string(filename) + end +end + +function Connection_method:nick() + -- Make a human-readable name for a connection suitable for error messages. + + local so = ffi.cast('VipsConnection *', self.vconnection) + local nick = vips_lib.vips_connection_nick(so) + if nick == ffi.NULL then + return nil + else + return ffi.string(nick) + end +end + +return ffi.metatype("VipsConnection", { + __index = Connection +}) diff --git a/src/vips/Image.lua b/src/vips/Image.lua index f463ffc..26d4a52 100644 --- a/src/vips/Image.lua +++ b/src/vips/Image.lua @@ -3,9 +3,8 @@ -- we have to split the definition of Image from the methods to avoid -- recursive requires between the methods and voperation -local Image = {} -Image.mt = {} -Image.mt.mt = {} -setmetatable(Image, Image) +local Image = { + mt = {} +} return Image diff --git a/src/vips/Image_methods.lua b/src/vips/Image_methods.lua index 7279a70..3145c6c 100644 --- a/src/vips/Image_methods.lua +++ b/src/vips/Image_methods.lua @@ -2,60 +2,33 @@ local ffi = require "ffi" -local verror = require "vips/verror" -local version = require "vips/version" -local log = require "vips/log" -local gvalue = require "vips/gvalue" -local vobject = require "vips/vobject" -local voperation = require "vips/voperation" -local vimage = require "vips/vimage" -local Image = require "vips/Image" - -local vips -local gobject -local glib +local verror = require "vips.verror" +local version = require "vips.version" +local gvalue = require "vips.gvalue" +local vobject = require "vips.vobject" +local voperation = require "vips.voperation" +local Image = require "vips.Image" + +local type = type +local error = error +local pairs = pairs +local ipairs = ipairs +local unpack = unpack or table.unpack +local rawget = rawget +local setmetatable = setmetatable +local getmetatable = getmetatable + +local vips_lib +local gobject_lib +local glib_lib if ffi.os == "Windows" then - vips = ffi.load("libvips-42.dll") - gobject = ffi.load("libgobject-2.0-0.dll") - glib = ffi.load("libglib-2.0-0.dll") + vips_lib = ffi.load("libvips-42.dll") + gobject_lib = ffi.load("libgobject-2.0-0.dll") + glib_lib = ffi.load("libglib-2.0-0.dll") else - vips = ffi.load("vips") - gobject = vips - glib = vips -end - -ffi.cdef[[ - const char* vips_foreign_find_load (const char* name); - const char* vips_foreign_find_load_buffer (const void* data, size_t size); - const char* vips_foreign_find_save (const char* name); - const char* vips_foreign_find_save_buffer (const char* suffix); - - VipsImage* vips_image_new_matrix_from_array (int width, int height, - const double* array, int size); - - VipsImage* vips_image_new_from_memory (const void *data, size_t size, - int width, int height, int bands, int format); - unsigned char* vips_image_write_to_memory (VipsImage* image, - size_t* size_out); - - VipsImage* vips_image_copy_memory (VipsImage* image); - - GType vips_image_get_typeof (const VipsImage* image, - const char* name); - int vips_image_get (const VipsImage* image, - const char* name, GValue* value_copy); - void vips_image_set (VipsImage* image, const char* name, GValue* value); - int vips_image_remove (VipsImage* image, const char* name); - - char* vips_filename_get_filename (const char* vips_filename); - char* vips_filename_get_options (const char* vips_filename); - -]] - --- either a single number, or a table of numbers -local function is_pixel(value) - return type(value) == "number" or - (type(value) == "table" and not Image.is_Image(value)) + vips_lib = ffi.load("vips") + gobject_lib = vips_lib + glib_lib = vips_lib end -- test for rectangular array of something @@ -99,12 +72,12 @@ end -- either a single number, or a table of numbers local function is_pixel(value) return type(value) == "number" or - (type(value) == "table" and not Image.is_Image(value)) + (type(value) == "table" and not Image.is_Image(value)) end local function call_enum(image, other, base, operation) if type(other) == "number" then - return image[base .. "_const"](image, operation, {other}) + return image[base .. "_const"](image, operation, { other }) elseif is_pixel(other) then return image[base .. "_const"](image, operation, other) else @@ -112,6 +85,13 @@ local function call_enum(image, other, base, operation) end end +-- turn a string from libvips that must be g_free()d into a lua string +local function to_string_copy(vips_string) + local lua_string = ffi.string(vips_string) + glib_lib.g_free(vips_string) + return lua_string +end + -- class methods function Image.is_Image(thing) @@ -135,75 +115,90 @@ end function Image.new(vimage) local image = {} - vobject.new(vimage) - image.vimage = vimage - setmetatable(image, Image.mt) + image.vimage = vobject.new(vimage) - return image + return setmetatable(image, Image.mt) +end + +function Image.find_load(filename) + local name = vips_lib.vips_foreign_find_load(filename) + if name == ffi.NULL then + return nil + else + return ffi.string(name) + end end function Image.new_from_file(vips_filename, ...) - local filename = vips.vips_filename_get_filename(vips_filename) - local options = vips.vips_filename_get_options(vips_filename) - local name = vips.vips_foreign_find_load(filename) + local filename = to_string_copy(vips_lib.vips_filename_get_filename(vips_filename)) + local options = to_string_copy(vips_lib.vips_filename_get_options(vips_filename)) + + local name = Image.find_load(filename) if name == nil then error(verror.get()) end - return voperation.call(ffi.string(name), ffi.string(options), - ffi.string(filename), unpack{...}) + return voperation.call(name, options, filename, unpack { ... }) +end + +function Image.find_load_buffer(data) + local name = vips_lib.vips_foreign_find_load_buffer(data, #data) + if name == ffi.NULL then + return nil + else + return ffi.string(name) + end end function Image.new_from_buffer(data, options, ...) - local name = vips.vips_foreign_find_load_buffer(data, #data) + local name = Image.find_load_buffer(data) if name == nil then error(verror.get()) end - return voperation.call(ffi.string(name), options or "", data, unpack{...}) + return voperation.call(name, options or "", data, unpack { ... }) end -function Image.new_from_memory(data, width, height, bands, format) +function Image.new_from_memory_ptr(data, size, width, height, bands, format) local format_value = gvalue.to_enum(gvalue.band_format_type, format) - local size = ffi.sizeof(data) - - local vimage = vips.vips_image_new_from_memory(data, size, - width, height, bands, format_value) - if vimage == nil then + local vimage = vips_lib.vips_image_new_from_memory(data, size, + width, height, bands, format_value) + if vimage == ffi.NULL then error(verror.get()) end + return Image.new(vimage) +end - local image = Image.new(vimage) - +function Image.new_from_memory(data, width, height, bands, format) + local image = Image.new_from_memory_ptr(data, ffi.sizeof(data), width, height, bands, format) -- libvips is using the memory we passed in: save a pointer to the memory -- block to try to stop it being GCd image._data = data - return image end function Image.new_from_array(array, scale, offset) - local width - local height - if not is_2D(array) then - array = {array} + array = { array } end - width = #array[1] - height = #array + local width = #array[1] + local height = #array local n = width * height - local a = ffi.new(gvalue.pdouble_typeof, n) + local arr = {} for y = 0, height - 1 do for x = 0, width - 1 do - a[x + y * width] = array[y + 1][x + 1] + arr[x + y * width + 1] = array[y + 1][x + 1] end end - local vimage = vips.vips_image_new_matrix_from_array(width, height, a, n) + local a = ffi.new(gvalue.double_arr_typeof, n, arr) + + local vimage = vips_lib.vips_image_new_matrix_from_array(width, + height, a, n) local image = Image.new(vimage) - image:set_type(gvalue.gdouble_type, "scale", scale or 1) - image:set_type(gvalue.gdouble_type, "offset", offset or 0) + image:set_type(gvalue.gdouble_type, "scale", scale or 1.0) + image:set_type(gvalue.gdouble_type, "offset", offset or 0.0) return image end @@ -211,11 +206,11 @@ end function Image.new_from_image(base_image, value) local pixel = (Image.black(1, 1) + value):cast(base_image:format()) local image = pixel:embed(0, 0, base_image:width(), base_image:height(), - {extend = "copy"}) - image = image:copy{ + { extend = "copy" }) + image = image:copy { interpretation = base_image:interpretation(), xres = base_image:xres(), - yres = base_image:yres(), + yres = base_image:yres(), xoffset = base_image:xoffset(), yoffset = base_image:yoffset() } @@ -223,22 +218,23 @@ function Image.new_from_image(base_image, value) return image end --- this is for undefined class methods, like Image.text -function Image.__index(table, name) - return function(...) - return voperation.call(name, "", unpack{...}) +function Image.new_from_source(source, options, ...) + local name = vips_lib.vips_foreign_find_load_source(source.vconnection) + if name == ffi.NULL then + error("Unable to load from source") end -end + return voperation.call(ffi.string(name), options, source.vconnection, unpack { ... }) +end -- overloads function Image.mt.__add(a, b) a, b = swap_Image_left(a, b) if type(b) == "number" then - return a:linear({1}, {b}) + return a:linear({ 1 }, { b }) elseif is_pixel(b) then - return a:linear({1}, b) + return a:linear({ 1 }, b) else return a:add(b) end @@ -247,19 +243,21 @@ end function Image.mt.__sub(a, b) if Image.is_Image(a) then if type(b) == "number" then - return a:linear({1}, {-b}) + return a:linear({ 1 }, { -b }) elseif is_pixel(b) then - return a:linear({1}, map(function(x) return -x end, b)) + return a:linear({ 1 }, map(function(x) + return -x + end, b)) else return a:subtract(b) end else -- therefore a is a constant and b is an image if type(a) == "number" then - return (b * -1):linear({1}, {a}) + return (b * -1):linear({ 1 }, { a }) else -- assume a is a pixel - return (b * -1):linear({1}, a) + return (b * -1):linear({ 1 }, a) end end end @@ -268,9 +266,9 @@ function Image.mt.__mul(a, b) a, b = swap_Image_left(a, b) if type(b) == "number" then - return a:linear({b}, {0}) + return a:linear({ b }, { 0 }) elseif is_pixel(b) then - return a:linear(b, {0}) + return a:linear(b, { 0 }) else return a:multiply(b) end @@ -279,19 +277,21 @@ end function Image.mt.__div(a, b) if Image.is_Image(a) then if type(b) == "number" then - return a:linear({1 / b}, {0}) + return a:linear({ 1 / b }, { 0 }) elseif is_pixel(b) then - return a:linear(map(function(x) return x ^ -1 end, b), {0}) + return a:linear(map(function(x) + return x ^ -1 + end, b), { 0 }) else return a:divide(b) end else -- therefore a is a constant and b is an image if type(a) == "number" then - return (b ^ -1):linear({a}, {0}) + return (b ^ -1):linear({ a }, { 0 }) else -- assume a is a pixel - return (b ^ -1):linear(a, {0}) + return (b ^ -1):linear(a, { 0 }) end end end @@ -302,7 +302,7 @@ function Image.mt.__mod(a, b) end if type(b) == "number" then - return a:remainder_const({b}) + return a:remainder_const({ b }) elseif is_pixel(b) then return a:remainder_const(b) else @@ -320,18 +320,17 @@ function Image.mt.__pow(a, b) else return b:wop(a) end - end -- unfortunately, lua does not let you return non-bools from <, >, <=, >=, ==, -- ~=, so there's no point overloading these ... call :more(2) etc. instead -function Image.mt.__tostring(self) +function Image.mt:__tostring() local result = (self:filename() or "(nil)") .. ": " .. - self:width() .. "x" .. self:height() .. " " .. - self:format() .. ", " .. - self:bands() .. " bands, " .. - self:interpretation() + self:width() .. "x" .. self:height() .. " " .. + self:format() .. ", " .. + self:bands() .. " bands, " .. + self:interpretation() if self:get_typeof("vips-loader") ~= 0 then result = result .. ", " .. self:get("vips-loader") @@ -340,462 +339,503 @@ function Image.mt.__tostring(self) return result end -function Image.mt.__call(self, x, y) +function Image.mt:__call(x, y) -- getpoint() will return a table for a pixel return unpack(self:getpoint(x, y)) end -function Image.mt.__concat(self, other) +function Image.mt:__concat(other) return self:bandjoin(other) end -function Image.mt.__len(self) - return self:bands() +-- instance methods + +local Image_method = {} + +function Image_method:vobject() + -- TODO: Could we use `self.vimage.parent_instance` here? + return ffi.cast(vobject.typeof, self.vimage) end -local instance_methods = { - -- utility methods +-- handy to have as instance methods too - vobject = function(self) - return ffi.cast(vobject.typeof, self.vimage) - end, +function Image_method:imageize(value) + return Image.imageize(self, value) +end - -- handy to have as instance methods too +function Image_method:new_from_image(value) + return Image.new_from_image(self, value) +end - imageize = function(self, value) - return Image.imageize(self, value) - end, +function Image_method:copy_memory() + local vimage = vips_lib.vips_image_copy_memory(self.vimage) + if vimage == ffi.NULL then + error(verror.get()) + end + return Image.new(vimage) +end - new_from_image = function(self, value) - return Image.new_from_image(self, value) - end, +-- writers - copy_memory = function(self) - local vimage = vips.vips_image_copy_memory(self.vimage) - if vimage == nil then - error(verror.get()) - end - return Image.new(vimage) - end, +function Image_method:write_to_file(vips_filename, ...) + collectgarbage("stop") + local filename = to_string_copy(vips_lib.vips_filename_get_filename(vips_filename)) + local options = to_string_copy(vips_lib.vips_filename_get_options(vips_filename)) + local name = vips_lib.vips_foreign_find_save(filename) + collectgarbage("restart") + if name == ffi.NULL then + error(verror.get()) + end + return voperation.call(ffi.string(name), options, + self, filename, unpack { ... }) +end - -- writers +function Image_method:write_to_buffer(format_string, ...) + collectgarbage("stop") + local options = to_string_copy(vips_lib.vips_filename_get_options(format_string)) + local name = vips_lib.vips_foreign_find_save_buffer(format_string) + collectgarbage("restart") + if name == ffi.NULL then + error(verror.get()) + end - write_to_file = function(self, vips_filename, ...) - local filename = vips.vips_filename_get_filename(vips_filename) - local options = vips.vips_filename_get_options(vips_filename) - local name = vips.vips_foreign_find_save(filename) - if name == nil then - error(verror.get()) - end + return voperation.call(ffi.string(name), options, self, unpack { ... }) +end - return voperation.call(ffi.string(name), ffi.string(options), - self, filename, unpack{...}) - end, +function Image_method:write_to_memory() + local psize = ffi.new(gvalue.psize_typeof, 1) + local vips_memory = vips_lib.vips_image_write_to_memory(self.vimage, psize) + local size = tonumber(psize[0]) - write_to_buffer = function(self, format_string, ...) - local options = vips.vips_filename_get_options(format_string) - local name = vips.vips_foreign_find_save_buffer(format_string) - if name == nil then - error(verror.get()) - end + local lua_memory = ffi.new(gvalue.mem_typeof, size) + ffi.copy(lua_memory, vips_memory, size) + glib_lib.g_free(vips_memory) - return voperation.call(ffi.string(name), ffi.string(options), - self, unpack{...}) - end, - - write_to_memory = function(self) - local psize = ffi.new(gvalue.psize_typeof, 1) - local vips_memory = vips.vips_image_write_to_memory(self.vimage, psize) - local size = psize[0] - -- FIXME can we avoid the copy somehow? - local lua_memory = ffi.new(gvalue.mem_typeof, size) - ffi.copy(lua_memory, vips_memory, size) - glib.g_free(vips_memory) - - return lua_memory - end, - - -- get/set metadata - - get_typeof = function(self, name) - -- on libvips 8.4 and earlier, we need to fetch the type via - -- our superclass get_typeof(), since vips_image_get_typeof() returned - -- enum properties as ints - if not version.at_least(8, 5) then - local gtype = self:vobject():get_typeof(name) - if gtype ~= 0 then - return gtype - end - - -- we must clear the error buffer after vobject typeof fails - verror.get() - end + return lua_memory +end + +function Image_method:write_to_memory_ptr() + local psize = ffi.new(gvalue.psize_typeof, 1) + local vips_memory = vips_lib.vips_image_write_to_memory(self.vimage, psize) + + return ffi.gc(vips_memory, glib_lib.g_free), tonumber(psize[0]) +end - return vips.vips_image_get_typeof(self.vimage, name) - end, - - get = function(self, name) - -- on libvips 8.4 and earlier, we need to fetch gobject properties via - -- our superclass get(), since vips_image_get() returned enum properties - -- as ints - if not version.at_least(8, 5) then - local gtype = self:vobject():get_typeof(name) - if gtype ~= 0 then - return self:vobject():get(name) - end - - -- we must clear the error buffer after vobject typeof fails - verror.get() +function Image_method:write_to_target(target, format_string, ...) + collectgarbage("stop") + local options = to_string_copy(vips_lib.vips_filename_get_options(format_string)) + local name = vips_lib.vips_foreign_find_save_target(format_string) + collectgarbage("restart") + if name == ffi.NULL then + error(verror.get()) + end + + return voperation.call(ffi.string(name), options, self, target.vconnection, unpack { ... }) +end +-- get/set metadata + +function Image_method:get_typeof(name) + -- on libvips 8.4 and earlier, we need to fetch the type via + -- our superclass get_typeof(), since vips_image_get_typeof() returned + -- enum properties as ints + if not version.at_least(8, 5) then + local vob = self:vobject() + local gtype = vob:get_typeof(name) + if gtype ~= 0 then + return vob:get_type(name, gtype) end - local pgv = gvalue.newp() + -- we must clear the error buffer after vobject typeof fails + verror.get() + end - local result = vips.vips_image_get(self.vimage, name, pgv) - if result ~= 0 then - error("unable to get " .. name) + return vips_lib.vips_image_get_typeof(self.vimage, name) +end + +function Image_method:get(name) + -- on libvips 8.4 and earlier, we need to fetch gobject properties via + -- our superclass get(), since vips_image_get() returned enum properties + -- as ints + if not version.at_least(8, 5) then + local vo = self:vobject() + local gtype = vo:get_typeof(name) + if gtype ~= 0 then + return vo:get(name) end - local result = pgv[0]:get() + -- we must clear the error buffer after vobject typeof fails + verror.get() + end - gobject.g_value_unset(pgv[0]) + local pgv = gvalue(true) - return result - end, + local result = vips_lib.vips_image_get(self.vimage, name, pgv) + if result ~= 0 then + error("unable to get " .. name) + end - set_type = function(self, gtype, name, value) - local gv = gvalue.new() - gv:init(gtype) - gv:set(value) - vips.vips_image_set(self.vimage, name, gv) - end, + result = pgv[0]:get() + gobject_lib.g_value_unset(pgv[0]) - set = function(self, name, value) - local gtype = self:get_typeof(name) - self:set_type(gtype, name, value) - end, + return result +end - remove = function(self, name) - return vips.vips_image_remove(self, name) ~= 0 - end, +function Image_method:set_type(gtype, name, value) + local pgv = gvalue(true) + pgv[0]:init(gtype) + pgv[0]:set(value) + vips_lib.vips_image_set(self.vimage, name, pgv) + gobject_lib.g_value_unset(pgv[0]) +end - -- standard header fields +function Image_method:set(name, value) + local gtype = self:get_typeof(name) + self:set_type(gtype, name, value) +end - width = function(self) - return self:get("width") - end, +function Image_method:remove(name) + return vips_lib.vips_image_remove(self.vimage, name) ~= 0 +end - height = function(self) - return self:get("height") - end, +-- standard header fields - size = function(self) - return self:width(), self:height() - end, +function Image_method:width() + return self:get("width") +end - bands = function(self) - return self:get("bands") - end, +function Image_method:height() + return self:get("height") +end - format = function(self) - return self:get("format") - end, +function Image_method:size() + return self:width(), self:height() +end - interpretation = function(self) - return self:get("interpretation") - end, +function Image_method:bands() + return self:get("bands") +end - xres = function(self) - return self:get("xres") - end, +function Image_method:format() + return self:get("format") +end - yres = function(self) - return self:get("yres") - end, +function Image_method:interpretation() + return self:get("interpretation") +end - xoffset = function(self) - return self:get("xoffset") - end, +function Image_method:xres() + return self:get("xres") +end - yoffset = function(self) - return self:get("yoffset") - end, +function Image_method:yres() + return self:get("yres") +end - filename = function(self) - return self:get("filename") - end, +function Image_method:xoffset() + return self:get("xoffset") +end - -- many-image input operations - -- - -- these don't wrap well automatically, since self is held separately +function Image_method:yoffset() + return self:get("yoffset") +end - bandjoin = function(self, other, options) - -- allow a single untable arg as well - if type(other) == "number" or Image.is_Image(other) then - other = {other} - end +function Image_method:filename() + return self:get("filename") +end - -- if other is all constants, we can use bandjoin_const - local all_constant = true - for i = 1, #other do - if type(other[i]) ~= "number" then - all_constant = false - break - end - end +-- many-image input operations +-- +-- these don't wrap well automatically, since self is held separately - if all_constant then - return voperation.call("bandjoin_const", "", self, other) - else - return voperation.call("bandjoin", "", {self, unpack(other)}) - end - end, +function Image_method:bandjoin(other, options) + -- allow a single untable arg as well + if type(other) == "number" or Image.is_Image(other) then + other = { other } + end - bandrank = function(self, other, options) - if type(other) ~= "table" then - other = {other} + -- if other is all constants, we can use bandjoin_const + local all_constant = true + for i = 1, #other do + if type(other[i]) ~= "number" then + all_constant = false + break end + end - return voperation.call("bandrank", "", {self, unpack(other)}) - end, + if all_constant then + return voperation.call("bandjoin_const", "", self, other, options) + else + return voperation.call("bandjoin", "", { self, unpack(other) }, options) + end +end - composite = function(self, other, mode, options) - -- allow a single untable arg as well - if type(other) == "number" or Image.is_Image(other) then - other = {other} - end - if type(mode) ~= "table" then - mode = {mode} - end +function Image_method:bandrank(other, options) + if type(other) ~= "table" then + other = { other } + end - -- need to map str -> int by hand, since the mode arg is actually - -- arrayint - for i = 1, #mode do - mode[i] = gvalue.to_enum(gvalue.blend_mode_type, mode[i]) - end + return voperation.call("bandrank", "", { self, unpack(other) }, options) +end + +function Image_method:composite(other, mode, options) + -- allow a single untable arg as well + if type(other) == "number" or Image.is_Image(other) then + other = { other } + end + if type(mode) ~= "table" then + mode = { mode } + end - return voperation.call("composite", "", - {self, unpack(other)}, mode, options) - end, + -- need to map str -> int by hand, since the mode arg is actually + -- arrayint + for i = 1, #mode do + mode[i] = gvalue.to_enum(gvalue.blend_mode_type, mode[i]) + end - -- convenience functions + return voperation.call("composite", "", { self, unpack(other) }, mode, options) +end - bandsplit = function(self) - local result +-- convenience functions + +function Image_method:hasalpha() + return vips_lib.vips_image_hasalpha(self.vimage) ~= 0 +end - result = {} - for i = 0, self:bands() - 1 do - result[i + 1] = self:extract_band(i) +-- addalpha was made a VipsOperation in vips 8.16; earlier versions need this polyfill +if not version.at_least(8, 16) then + function Image_method:addalpha() + local max_alpha + if self:interpretation() == "rgb16" or self:interpretation() == "grey16" then + max_alpha = 65535 + elseif self:interpretation() == "scrgb" then + max_alpha = 1.0 + else + max_alpha = 255 end - return result - end, + return self:bandjoin(max_alpha) + end +end - -- special behaviour wrappers +function Image_method:bandsplit() + local result - ifthenelse = function(self, then_value, else_value, options) - -- We need different imageize rules for this. We need then_value - -- and else_value to match each other first, and only if they - -- are both constants do we match to self. + result = {} + for i = 0, self:bands() - 1 do + result[i + 1] = self:extract_band(i) + end - local match_image + return result +end - for i, v in pairs({then_value, else_value, self}) do - if Image.is_Image(v) then - match_image = v - break - end - end +-- special behaviour wrappers - if not Image.is_Image(then_value) then - then_value = match_image:imageize(then_value) - end +function Image_method:ifthenelse(then_value, else_value, options) + -- We need different imageize rules for this. We need then_value + -- and else_value to match each other first, and only if they + -- are both constants do we match to self. + + local match_image - if not Image.is_Image(else_value) then - else_value = match_image:imageize(else_value) + for _, v in pairs({ then_value, else_value, self }) do + if Image.is_Image(v) then + match_image = v + break end + end - return voperation.call("ifthenelse", "", - self, then_value, else_value, options) - end, + if not Image.is_Image(then_value) then + then_value = match_image:imageize(then_value) + end - -- enum expansions + if not Image.is_Image(else_value) then + else_value = match_image:imageize(else_value) + end - pow = function(self, other) - return call_enum(self, other, "math2", "pow") - end, + return voperation.call("ifthenelse", "", self, then_value, else_value, options) +end - wop = function(self, other) - return call_enum(self, other, "math2", "wop") - end, +-- enum expansions - lshift = function(self, other) - return call_enum(self, other, "boolean", "lshift") - end, +function Image_method:pow(other) + return call_enum(self, other, "math2", "pow") +end - rshift = function(self, other) - return call_enum(self, other, "boolean", "rshift") - end, +function Image_method:wop(other) + return call_enum(self, other, "math2", "wop") +end - andimage = function(self, other) - return call_enum(self, other, "boolean", "and") - end, +function Image_method:lshift(other) + return call_enum(self, other, "boolean", "lshift") +end - orimage = function(self, other) - return call_enum(self, other, "boolean", "or") - end, +function Image_method:rshift(other) + return call_enum(self, other, "boolean", "rshift") +end - eorimage = function(self, other) - return call_enum(self, other, "boolean", "eor") - end, +function Image_method:andimage(other) + return call_enum(self, other, "boolean", "and") +end - less = function(self, other) - return call_enum(self, other, "relational", "less") - end, +function Image_method:orimage(other) + return call_enum(self, other, "boolean", "or") +end - lesseq = function(self, other) - return call_enum(self, other, "relational", "lesseq") - end, +function Image_method:eorimage(other) + return call_enum(self, other, "boolean", "eor") +end - more = function(self, other) - return call_enum(self, other, "relational", "more") - end, +function Image_method:less(other) + return call_enum(self, other, "relational", "less") +end - moreeq = function(self, other) - return call_enum(self, other, "relational", "moreeq") - end, +function Image_method:lesseq(other) + return call_enum(self, other, "relational", "lesseq") +end - equal = function(self, other) - return call_enum(self, other, "relational", "equal") - end, +function Image_method:more(other) + return call_enum(self, other, "relational", "more") +end - noteq = function(self, other) - return call_enum(self, other, "relational", "noteq") - end, +function Image_method:moreeq(other) + return call_enum(self, other, "relational", "moreeq") +end - floor = function(self) - return self:round("floor") - end, +function Image_method:equal(other) + return call_enum(self, other, "relational", "equal") +end - ceil = function(self) - return self:round("ceil") - end, +function Image_method:noteq(other) + return call_enum(self, other, "relational", "noteq") +end - rint = function(self) - return self:round("rint") - end, +function Image_method:floor() + return self:round("floor") +end - bandand = function(self) - return self:bandbool("and") - end, +function Image_method:ceil() + return self:round("ceil") +end - bandor = function(self) - return self:bandbool("or") - end, +function Image_method:rint() + return self:round("rint") +end - bandeor = function(self) - return self:bandbool("eor") - end, +function Image_method:bandand() + return self:bandbool("and") +end - real = function(self) - return self:complexget("real") - end, +function Image_method:bandor() + return self:bandbool("or") +end - imag = function(self) - return self:complexget("imag") - end, +function Image_method:bandeor() + return self:bandbool("eor") +end - polar = function(self) - return self:complex("polar") - end, +function Image_method:real() + return self:complexget("real") +end - rect = function(self) - return self:complex("rect") - end, +function Image_method:imag() + return self:complexget("imag") +end - conj = function(self) - return self:complex("conj") - end, +function Image_method:polar() + return self:complex("polar") +end - sin = function(self) - return self:math("sin") - end, +function Image_method:rect() + return self:complex("rect") +end - cos = function(self) - return self:math("cos") - end, +function Image_method:conj() + return self:complex("conj") +end - tan = function(self) - return self:math("tan") - end, +function Image_method:sin() + return self:math("sin") +end - asin = function(self) - return self:math("asin") - end, +function Image_method:cos() + return self:math("cos") +end - acos = function(self) - return self:math("acos") - end, +function Image_method:tan() + return self:math("tan") +end - atan = function(self) - return self:math("atan") - end, +function Image_method:asin() + return self:math("asin") +end - exp = function(self) - return self:math("exp") - end, +function Image_method:acos() + return self:math("acos") +end - exp10 = function(self) - return self:math("exp10") - end, +function Image_method:atan() + return self:math("atan") +end - log = function(self) - return self:math("log") - end, +function Image_method:exp() + return self:math("exp") +end - log10 = function(self) - return self:math("log10") - end, +function Image_method:exp10() + return self:math("exp10") +end - erode = function(self, mask) - return self:morph(mask, "erode") - end, +function Image_method:log() + return self:math("log") +end - dilate = function(self, mask) - return self:morph(mask, "dilate") - end, +function Image_method:log10() + return self:math("log10") +end - fliphor = function(self) - return self:flip("horizontal") - end, +function Image_method:erode(mask) + return self:morph(mask, "erode") +end - flipver = function(self) - return self:flip("vertical") - end, +function Image_method:dilate(mask) + return self:morph(mask, "dilate") +end - rot90 = function(self) - return self:rot("d90") - end, +function Image_method:fliphor() + return self:flip("horizontal") +end - rot180 = function(self) - return self:rot("d180") - end, +function Image_method:flipver() + return self:flip("vertical") +end - rot270 = function(self) - return self:rot("d270") - end -} +function Image_method:rot90() + return self:rot("d90") +end -function Image.mt.__index(table, index) - if type(index) == "number" then - return table:extract_band(index) - elseif instance_methods[index] then - return instance_methods[index] - else - return - function(...) - return voperation.call(index, "", unpack{...}) - end +function Image_method:rot180() + return self:rot("d180") +end + +function Image_method:rot270() + return self:rot("d270") +end + +-- this is for undefined class / instance methods, like Image.text or image:avg +local fall_back = function(name) + return function(...) + return voperation.call(name, "", unpack { ... }) end end -return Image +function Image.mt.__index(_, name) + -- try to get instance method otherwise fallback to voperation + return rawget(Image_method, name) or fall_back(name) +end + +return setmetatable(Image, { + __index = function(_, name) + -- undefined class methods + return fall_back(name) + end +}) diff --git a/src/vips/Interpolate.lua b/src/vips/Interpolate.lua new file mode 100644 index 0000000..5502159 --- /dev/null +++ b/src/vips/Interpolate.lua @@ -0,0 +1,30 @@ +-- make image interpolators, see affine + +local ffi = require "ffi" + +local verror = require "vips.verror" +local vobject = require "vips.vobject" + +local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") + +local Interpolate = {} + +Interpolate.vobject = function(self) + return ffi.cast(vobject.typeof, self) +end + +Interpolate.new = function(name) + -- there could potentially be other params here, but ignore that for now + local interpolate = vips_lib.vips_interpolate_new(name) + if interpolate == nil then + error("no such interpolator\n" .. verror.get()) + end + + return vobject.new(interpolate) +end + +return ffi.metatype("VipsInterpolate", { + __index = Interpolate +}) + + diff --git a/src/vips/Source.lua b/src/vips/Source.lua new file mode 100644 index 0000000..a642f65 --- /dev/null +++ b/src/vips/Source.lua @@ -0,0 +1,41 @@ +-- An output connection + +local ffi = require "ffi" + +local verror = require "vips.verror" +local Connection = require "vips.Connection" + +local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") + +local Source = {} + +Source.new_from_descriptor = function(descriptor) + local source = vips_lib.vips_source_new_from_descriptor(descriptor) + if source == ffi.NULL then + error("Can't create source from descriptor " .. descriptor .. "\n" .. verror.get()) + end + + return Connection.new(source) +end + +Source.new_from_file = function(filename) + local source = vips_lib.vips_source_new_from_file(filename) + if source == ffi.NULL then + error("Can't create source from filename " .. filename .. "\n" .. verror.get()) + end + + return Connection.new(source) +end + +Source.new_from_memory = function(data) -- data is an FFI memory array containing the image data + local source = vips_lib.vips_source_new_from_memory(data, ffi.sizeof(data)) + if source == ffi.NULL then + error("Can't create input source from memory \n" .. verror.get()) + end + + return Connection.new(source) +end + +return ffi.metatype("VipsSource", { + __index = Source +}) diff --git a/src/vips/Target.lua b/src/vips/Target.lua new file mode 100644 index 0000000..acb0579 --- /dev/null +++ b/src/vips/Target.lua @@ -0,0 +1,46 @@ +-- An input connection + +local ffi = require "ffi" + +local Connection = require "vips.Connection" + +local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") + +local Target = {} + +Target.new_to_descriptor = function(descriptor) + collectgarbage("stop") + local target = vips_lib.vips_target_new_to_descriptor(descriptor) + collectgarbage("restart") + if target == ffi.NULL then + error("can't create output target from descriptor " .. descriptor) + else + return Connection.new(target) + end +end + +Target.new_to_file = function(filename) + collectgarbage("stop") + local target = vips_lib.vips_target_new_to_file(filename) + collectgarbage("restart") + if target == ffi.NULL then + error("can't create output target from filename " .. filename) + else + return Connection.new(target) + end +end + +Target.new_to_memory = function() + collectgarbage("stop") + local target = vips_lib.vips_target_new_to_memory() + collectgarbage("restart") + if target == ffi.NULL then + error("can't create output target from memory") + else + return Connection.new(target) + end +end + +return ffi.metatype("VipsTarget", { + __index = Target +}) diff --git a/src/vips/cdefs.lua b/src/vips/cdefs.lua new file mode 100644 index 0000000..30d5bd1 --- /dev/null +++ b/src/vips/cdefs.lua @@ -0,0 +1,307 @@ +-- all the C declarations for lua-vips + +local ffi = require "ffi" + +-- GType is an int the size of a pointer ... I don't think we can just use +-- size_t, sadly +if ffi.arch == "x64" or ffi.arch == "arm64" then + ffi.cdef [[ + typedef uint64_t GType; + ]] +else + ffi.cdef [[ + typedef uint32_t GType; + ]] +end + +ffi.cdef [[ + typedef struct _GValue { + GType gtype; + uint64_t data[2]; + } GValue; + + void *g_malloc (size_t size); + void g_free (void *data); + + void g_object_ref (void *object); + void g_object_unref (void *object); + + void g_value_init (GValue *value, GType gtype); + void g_value_unset (GValue *value); + const char *g_type_name (GType gtype); + GType g_type_from_name (const char *name); + GType g_type_fundamental (GType gtype); + + GType vips_blend_mode_get_type (void); + GType vips_band_format_get_type (void); + + int vips_enum_from_nick (const char *domain, + GType gtype, const char *str); + const char *vips_enum_nick (GType gtype, int value); + + void g_value_set_boolean (GValue *value, int v_boolean); + void g_value_set_int (GValue *value, int i); + void g_value_set_double (GValue *value, double d); + void g_value_set_enum (GValue *value, int e); + void g_value_set_flags (GValue *value, unsigned int f); + void g_value_set_string (GValue *value, const char *str); + void vips_value_set_ref_string (GValue *value, const char *str); + void g_value_set_object (GValue *value, void *object); + void vips_value_set_array_double (GValue *value, + const double *array, int n); + void vips_value_set_array_int (GValue *value, + const int *array, int n); + void vips_value_set_array_image (GValue *value, int n); + void vips_value_set_blob (GValue *value, + void (*free_fn)(void *data), void *data, size_t length); + void vips_value_set_blob_free (GValue *value, + void *data, size_t length); + + int g_value_get_boolean (const GValue *value); + int g_value_get_int (const GValue *value); + double g_value_get_double (const GValue *value); + int g_value_get_enum (const GValue *value); + unsigned int g_value_get_flags (const GValue *value); + const char *g_value_get_string (const GValue *value); + const char *vips_value_get_ref_string (const GValue *value, size_t *length); + void *g_value_get_object (const GValue *value); + double *vips_value_get_array_double (const GValue *value, int *n); + int *vips_value_get_array_int (const GValue *value, int *n); + void *vips_value_get_blob (const GValue *value, size_t *length); + + typedef struct _GObject { + void *g_type_instance; + unsigned int ref_count; + void *qdata; + } GObject; + + typedef struct _VipsObject { + GObject parent_instance; + bool constructed; + bool static_object; + void *argument_table; + char *nickname; + char *description; + bool preclose; + bool close; + bool postclose; + size_t local_memory; + } VipsObject; + + typedef struct _VipsObjectClass { + // opaque + } VipsObjectClass; + + typedef enum { + G_PARAM_READABLE = 1, + G_PARAM_WRITABLE = 2, + G_PARAM_CONSTRUCT = 4, + G_PARAM_CONSTRUCT_ONLY = 8, + G_PARAM_LAX_VALIDATION = 16, + G_PARAM_STATIC_NAME = 32, + G_PARAM_PRIVATE = G_PARAM_STATIC_NAME, + G_PARAM_STATIC_NICK = 64, + G_PARAM_STATIC_BLURB = 128 + } GParamFlags; + + typedef struct _GParamSpec { + void *g_type_instance; + + const char *name; + GParamFlags flags; + GType value_type; + GType owner_type; + + // rest opaque + } GParamSpec; + + typedef struct _VipsArgument { + GParamSpec *pspec; + } VipsArgument; + + typedef struct _VipsArgumentInstance { + VipsArgument parent; + + // opaque + } VipsArgumentInstance; + + typedef enum _VipsArgumentFlags { + VIPS_ARGUMENT_NONE = 0, + VIPS_ARGUMENT_REQUIRED = 1, + VIPS_ARGUMENT_CONSTRUCT = 2, + VIPS_ARGUMENT_SET_ONCE = 4, + VIPS_ARGUMENT_SET_ALWAYS = 8, + VIPS_ARGUMENT_INPUT = 16, + VIPS_ARGUMENT_OUTPUT = 32, + VIPS_ARGUMENT_DEPRECATED = 64, + VIPS_ARGUMENT_MODIFY = 128 + } VipsArgumentFlags; + + typedef struct _VipsArgumentClass { + VipsArgument parent; + + VipsObjectClass *object_class; + VipsArgumentFlags flags; + int priority; + uint64_t offset; + } VipsArgumentClass; + + int vips_object_get_argument (VipsObject *object, + const char *name, GParamSpec **pspec, + VipsArgumentClass **argument_class, + VipsArgumentInstance **argument_instance); + + void g_object_set_property (VipsObject *object, + const char *name, const GValue *value); + void g_object_get_property (VipsObject *object, + const char *name, GValue *value); + + void vips_object_print_all (void); + + int vips_object_set_from_string (VipsObject *object, const char *options); + + typedef struct _VipsImage { + VipsObject parent_instance; + + // opaque + } VipsImage; + + typedef struct _VipsConnection { + VipsObject parent_instance; + + // opaque + } VipsConnection; + + const char *vips_connection_filename (VipsConnection *connection); + const char *vips_connection_nick (VipsConnection *connection); + + typedef struct _VipsSource { + VipsConnection parent_instance; + + // opaque + } VipsSource; + + typedef struct _VipsTarget { + VipsConnection parent_instance; + + // opaque + } VipsTarget; + + VipsSource *vips_source_new_from_descriptor (int descriptor); + VipsSource *vips_source_new_from_file (const char *filename); + // VipsSource *vips_source_new_from_blob (VipsBlob *blob); + // VipsSource *vips_source_new_from_target (VipsTarget *target); + VipsSource *vips_source_new_from_memory (const void *data, size_t size); + // VipsSource *vips_source_new_from_options (const char *options); + // void vips_source_minimise (VipsSource *source); + // int vips_source_decode (VipsSource *source); + // gint64 vips_source_read (VipsSource *source, void *data, size_t length); + // gboolean vips_source_is_mappable (VipsSource *source); + // gboolean vips_source_is_file (VipsSource *source); + // const void *vips_source_map (VipsSource *source, size_t *length); + // VipsBlob *vips_source_map_blob (VipsSource *source); + // gint64 vips_source_seek (VipsSource *source, gint64 offset, int whence); + // int vips_source_rewind (VipsSource *source); + // gint64 vips_source_sniff_at_most (VipsSource *source, unsigned char **data, size_t length); + // unsigned char *vips_source_sniff (VipsSource *source, size_t length); + // gint64 vips_source_length (VipsSource *source); + // VipsSourceCustom *vips_source_custom_new (void); + // GInputStream *vips_g_input_stream_new_from_source (VipsSource *source); + // VipsSourceGInputStream *vips_source_g_input_stream_new (GInputStream *stream); + + VipsTarget *vips_target_new_to_descriptor (int descriptor); + VipsTarget *vips_target_new_to_file (const char *filename); + VipsTarget *vips_target_new_to_memory (void); + // VipsTarget *vips_target_new_temp (VipsTarget *target); + // int vips_target_write (VipsTarget *target, const void *data, size_t length); + // gint64 vips_target_read (VipsTarget *target, void *buffer, size_t length); + // gint64 vips_target_seek (VipsTarget *target, gint64 offset, int whence); + // int vips_target_end (VipsTarget *target); + // unsigned char *vips_target_steal (VipsTarget *target, size_t *length); + // char *vips_target_steal_text (VipsTarget *target); + // int vips_target_putc (VipsTarget *target, int ch); + // int vips_target_writes (VipsTarget *target, const char *str); + // int vips_target_writef (VipsTarget *target, const char *fmt, ...); + // int vips_target_write_amp (VipsTarget *target, const char *str); + // VipsTargetCustom *vips_target_custom_new (void); + + const char *vips_foreign_find_load (const char *name); + const char *vips_foreign_find_load_buffer (const void *data, size_t size); + const char *vips_foreign_find_save (const char *name); + const char *vips_foreign_find_save_buffer (const char *suffix); + const char* vips_foreign_find_load_source (VipsSource *source); + const char* vips_foreign_find_save_target (const char* suffix); + + VipsImage *vips_image_new_matrix_from_array (int width, int height, + const double *array, int size); + + VipsImage *vips_image_new_from_memory (const void *data, size_t size, + int width, int height, int bands, int format); + unsigned char *vips_image_write_to_memory (VipsImage *image, + size_t *size_out); + + VipsImage *vips_image_copy_memory (VipsImage *image); + + VipsImage **vips_value_get_array_image (const GValue *value, int *n); + + GType vips_image_get_typeof (const VipsImage *image, const char *name); + int vips_image_get (const VipsImage *image, const char *name, + GValue *value_copy); + void vips_image_set (VipsImage *image, const char *name, GValue *value); + int vips_image_remove (VipsImage *image, const char *name); + + int vips_image_hasalpha(VipsImage *image); + + char *vips_filename_get_filename (const char *vips_filename); + char *vips_filename_get_options (const char *vips_filename); + + typedef struct _VipsOperation { + VipsObject parent_instance; + + // opaque + } VipsOperation; + + typedef struct _VipsInterpolate { + VipsObject parent_instance; + + // opaque + } VipsInterpolate; + + VipsInterpolate *vips_interpolate_new (const char *name); + + VipsOperation *vips_operation_new (const char *name); + + typedef void * (*VipsArgumentMapFn) (VipsOperation *object, + GParamSpec *pspec, + VipsArgumentClass *argument_class, + VipsArgumentInstance *argument_instance, + void *a, void *b); + + void *vips_argument_map (VipsOperation *object, + VipsArgumentMapFn fn, void *a, void *b); + + void vips_object_get_args (VipsOperation *object, + const char ***names, int **flags, int *n_args); + + VipsOperation *vips_cache_operation_build (VipsOperation *operation); + void vips_object_unref_outputs (VipsOperation *operation); + + void vips_leak_set (int leak); + void vips_cache_set_max (int max); + void vips_cache_set_max (int max); + int vips_cache_get_max (void); + void vips_cache_set_max_mem (size_t max_mem); + size_t vips_cache_get_max_mem (void); + void vips_cache_set_max_files (int max_files); + int vips_cache_get_max_files (void); + + void vips_concurrency_set (int concurrency); + int vips_concurrency_get (); + + int vips_init (const char *argv0); + int vips_version (int flag); + + const char *vips_error_buffer (void); + void vips_error_clear (void); + +]] diff --git a/src/vips/gvalue.lua b/src/vips/gvalue.lua index 22ecfe4..5e8ba13 100644 --- a/src/vips/gvalue.lua +++ b/src/vips/gvalue.lua @@ -1,349 +1,300 @@ -- manipulate GValue objects from lua -- pull in gobject via the vips library -local ffi = require "ffi" +local ffi = require "ffi" -local log = require "vips/log" +local verror = require "vips.verror" +local version = require "vips.version" +local Image = require "vips.Image" -local verror = require "vips/verror" -local version = require "vips/version" -local Image = require "vips/Image" +local type = type +local error = error -local vips -local gobject -local glib +local vips_lib +local gobject_lib +local glib_lib if ffi.os == "Windows" then - vips = ffi.load("libvips-42.dll") - gobject = ffi.load("libgobject-2.0-0.dll") - glib = ffi.load("libglib-2.0-0.dll") + vips_lib = ffi.load("libvips-42.dll") + gobject_lib = ffi.load("libgobject-2.0-0.dll") + glib_lib = ffi.load("libglib-2.0-0.dll") else - vips = ffi.load("vips") - gobject = vips - glib = vips + vips_lib = ffi.load("vips") + gobject_lib = vips_lib + glib_lib = vips_lib end --- GType is an int the size of a pointer ... I don't think we can just use --- size_t, sadly -if ffi.arch == "x64" then - ffi.cdef[[ - typedef uint64_t GType; - ]] -else - ffi.cdef[[ - typedef uint32_t GType; - ]] +if version.at_least(8, 6) then + vips_lib.vips_blend_mode_get_type() end +vips_lib.vips_band_format_get_type() -ffi.cdef[[ - typedef struct _GValue { - GType gtype; - uint64_t data[2]; - } GValue; - - typedef struct _VipsImage VipsImage; - - void* g_malloc(size_t size); - void g_free(void* data); - - void g_object_ref (void* object); - void g_object_unref (void* object); - - void g_value_init (GValue* value, GType gtype); - void g_value_unset (GValue* value); - const char* g_type_name (GType gtype); - GType g_type_from_name (const char* name); - GType g_type_fundamental (GType gtype); - - GType vips_blend_mode_get_type (void); - GType vips_band_format_get_type (void); - - int vips_enum_from_nick (const char* domain, - GType gtype, const char* str); - const char *vips_enum_nick (GType gtype, int value); - - void g_value_set_boolean (GValue* value, int v_boolean); - void g_value_set_int (GValue* value, int i); - void g_value_set_double (GValue* value, double d); - void g_value_set_enum (GValue* value, int e); - void g_value_set_flags (GValue* value, unsigned int f); - void g_value_set_string (GValue* value, const char *str); - void g_value_set_object (GValue* value, void* object); - void vips_value_set_array_double (GValue* value, - const double* array, int n ); - void vips_value_set_array_int (GValue* value, - const int* array, int n ); - void vips_value_set_array_image (GValue *value, int n); - void vips_value_set_blob (GValue* value, - void (*free_fn)(void* data), void* data, size_t length); - - int g_value_get_boolean (const GValue* value); - int g_value_get_int (GValue* value); - double g_value_get_double (GValue* value); - int g_value_get_enum (GValue* value); - unsigned int g_value_get_flags (GValue* value); - const char* g_value_get_string (GValue* value); - const char* vips_value_get_ref_string (const GValue* value, size_t* length); - void* g_value_get_object (GValue* value); - double* vips_value_get_array_double (const GValue* value, int* n); - int* vips_value_get_array_int (const GValue* value, int* n); - VipsImage** vips_value_get_array_image (const GValue* value, int* n); - void* vips_value_get_blob (const GValue* value, size_t* length); - - void vips_object_print_all (void); - -]] +local gvalue = {} -if version.at_least(8, 6) then - vips.vips_blend_mode_get_type() +-- make ffi constructors we can reuse +gvalue.gv_typeof = ffi.typeof("GValue") +gvalue.pgv_typeof = ffi.typeof("GValue[1]") +gvalue.image_typeof = ffi.typeof("VipsImage*") +gvalue.pint_typeof = ffi.typeof("int[?]") +gvalue.int_arr_typeof = ffi.typeof("const int[?]") +gvalue.double_arr_typeof = ffi.typeof("const double[?]") +gvalue.psize_typeof = ffi.typeof("size_t[?]") +gvalue.mem_typeof = ffi.typeof("unsigned char[?]") +gvalue.interpolate_typeof = ffi.typeof("VipsInterpolate*") +gvalue.connection_typeof = ffi.typeof("VipsConnection*") +gvalue.source_typeof = ffi.typeof("VipsSource*") +gvalue.target_typeof = ffi.typeof("VipsTarget*") + +-- look up some common gtypes at init for speed +gvalue.gbool_type = gobject_lib.g_type_from_name("gboolean") +gvalue.gint_type = gobject_lib.g_type_from_name("gint") +gvalue.gdouble_type = gobject_lib.g_type_from_name("gdouble") +gvalue.gstr_type = gobject_lib.g_type_from_name("gchararray") +gvalue.genum_type = gobject_lib.g_type_from_name("GEnum") +gvalue.gflags_type = gobject_lib.g_type_from_name("GFlags") +gvalue.image_type = gobject_lib.g_type_from_name("VipsImage") +gvalue.array_int_type = gobject_lib.g_type_from_name("VipsArrayInt") +gvalue.array_double_type = gobject_lib.g_type_from_name("VipsArrayDouble") +gvalue.array_image_type = gobject_lib.g_type_from_name("VipsArrayImage") +gvalue.refstr_type = gobject_lib.g_type_from_name("VipsRefString") +gvalue.blob_type = gobject_lib.g_type_from_name("VipsBlob") +gvalue.band_format_type = gobject_lib.g_type_from_name("VipsBandFormat") +gvalue.blend_mode_type = version.at_least(8, 6) and gobject_lib.g_type_from_name("VipsBlendMode") or 0 +gvalue.interpolate_type = gobject_lib.g_type_from_name("VipsInterpolate") +gvalue.connection_type = gobject_lib.g_type_from_name("VipsConnection") +gvalue.source_type = gobject_lib.g_type_from_name("VipsSource") +gvalue.target_type = gobject_lib.g_type_from_name("VipsTarget") + +-- gvalue.*_type can be of type cdata or number depending on the OS and Lua version +-- gtypes as returned by vips_lib can also be of type cdata or number +-- cdata and number are not comparable with Standard Lua (using luaffi-tkl) +gvalue.comparable_type = type(gvalue.gdouble_type) == "number" and + function(gtype) return tonumber(gtype) end or + function(gtype) return gtype end +gvalue.to_enum = function(gtype, value) + -- turn a string into an int, ready to be passed into libvips + local enum_value + + if type(value) == "string" then + enum_value = vips_lib.vips_enum_from_nick("lua-vips", + gtype, value) + + if enum_value < 0 then + error("no such enum " .. value .. "\n" .. verror.get()) + end + else + enum_value = value + end + + return enum_value end -vips.vips_band_format_get_type() -local function print_all(msg) - collectgarbage() - print(msg) - vips.vips_object_print_all() - print() +gvalue.type_name = function(gtype) + return ffi.string(gobject_lib.g_type_name(gtype)) end -local gvalue = {} -local gvalue_mt = { +gvalue.init = function(gv, gtype) + gobject_lib.g_value_init(gv, gtype) +end + +gvalue.set = function(gv, value) + local gtype = gv.gtype + local gtype_comp = gvalue.comparable_type(gtype) + local fundamental = gobject_lib.g_type_fundamental(gtype) + + if gtype_comp == gvalue.gbool_type then + gobject_lib.g_value_set_boolean(gv, value) + elseif gtype_comp == gvalue.gint_type then + gobject_lib.g_value_set_int(gv, value) + elseif gtype_comp == gvalue.gdouble_type then + gobject_lib.g_value_set_double(gv, value) + elseif fundamental == gvalue.genum_type then + gobject_lib.g_value_set_enum(gv, gvalue.to_enum(gtype, value)) + elseif fundamental == gvalue.gflags_type then + gobject_lib.g_value_set_flags(gv, value) + elseif gtype_comp == gvalue.gstr_type then + gobject_lib.g_value_set_string(gv, value) + elseif gtype_comp == gvalue.refstr_type then + gobject_lib.vips_value_set_ref_string(gv, value) + elseif gtype_comp == gvalue.image_type then + gobject_lib.g_value_set_object(gv, value.vimage) + elseif gtype_comp == gvalue.array_int_type then + if type(value) == "number" then + value = { value } + end + + local n = #value + local a = ffi.new(gvalue.int_arr_typeof, n, value) + + vips_lib.vips_value_set_array_int(gv, a, n) + elseif gtype_comp == gvalue.array_double_type then + if type(value) == "number" then + value = { value } + end + + local n = #value + local a = ffi.new(gvalue.double_arr_typeof, n, value) + + vips_lib.vips_value_set_array_double(gv, a, n) + elseif gtype_comp == gvalue.array_image_type then + if Image.is_Image(value) then + value = { value } + end + + local n = #value + + vips_lib.vips_value_set_array_image(gv, n) + local a = vips_lib.vips_value_get_array_image(gv, nil) + + for i = 0, n - 1 do + a[i] = value[i + 1].vimage + -- the gvalue needs a set of refs to own + gobject_lib.g_object_ref(a[i]) + end + elseif gtype_comp == gvalue.blob_type then + -- we need to set the blob to a copy of the lua string that vips + -- can own + local n = #value + + local buf = glib_lib.g_malloc(n) + ffi.copy(buf, value, n) + + if version.at_least(8, 6) then + vips_lib.vips_value_set_blob_free(gv, buf, n) + else + vips_lib.vips_value_set_blob(gv, glib_lib.g_free, buf, n) + end + elseif gtype_comp == gvalue.interpolate_type then + gobject_lib.g_value_set_object(gv, value) + elseif gtype_comp == gvalue.connection_type then + gobject_lib.g_value_set_object(gv, value) + elseif gtype_comp == gvalue.source_type then + gobject_lib.g_value_set_object(gv, value) + elseif gtype_comp == gvalue.target_type then + gobject_lib.g_value_set_object(gv, value) + else + error("unsupported gtype for set " .. gvalue.type_name(gtype)) + end +end + +gvalue.get = function(gv) + local gtype = gv.gtype + local gtype_comp = gvalue.comparable_type(gtype) + local fundamental = gobject_lib.g_type_fundamental(gtype) + + local result + + if gtype_comp == gvalue.gbool_type then + result = gobject_lib.g_value_get_boolean(gv) + elseif gtype_comp == gvalue.gint_type then + result = gobject_lib.g_value_get_int(gv) + elseif gtype_comp == gvalue.gdouble_type then + result = gobject_lib.g_value_get_double(gv) + elseif fundamental == gvalue.genum_type then + local enum_value = gobject_lib.g_value_get_enum(gv) + + local cstr = vips_lib.vips_enum_nick(gtype, enum_value) + + if cstr == ffi.NULL then + error("value not in enum") + end + + result = ffi.string(cstr) + elseif fundamental == gvalue.gflags_type then + result = gobject_lib.g_value_get_flags(gv) + elseif gtype_comp == gvalue.gstr_type then + local cstr = gobject_lib.g_value_get_string(gv) + + if cstr ~= ffi.NULL then + result = ffi.string(cstr) + else + result = nil + end + elseif gtype_comp == gvalue.refstr_type then + local psize = ffi.new(gvalue.psize_typeof, 1) + + local cstr = vips_lib.vips_value_get_ref_string(gv, psize) + + result = ffi.string(cstr, tonumber(psize[0])) + elseif gtype_comp == gvalue.image_type then + -- g_value_get_object() will not add a ref ... that is + -- held by the gvalue + local vo = gobject_lib.g_value_get_object(gv) + local vimage = ffi.cast(gvalue.image_typeof, vo) + + -- we want a ref that will last with the life of the vimage: + -- this ref is matched by the unref that's attached to finalize + -- by Image.new() + gobject_lib.g_object_ref(vimage) + + result = Image.new(vimage) + elseif gtype_comp == gvalue.array_int_type then + local pint = ffi.new(gvalue.pint_typeof, 1) + + local array = vips_lib.vips_value_get_array_int(gv, pint) + result = {} + for i = 0, pint[0] - 1 do + result[i + 1] = array[i] + end + + elseif gtype_comp == gvalue.array_double_type then + local pint = ffi.new(gvalue.pint_typeof, 1) + + local array = vips_lib.vips_value_get_array_double(gv, pint) + result = {} + for i = 0, pint[0] - 1 do + result[i + 1] = array[i] + end + elseif gtype_comp == gvalue.array_image_type then + local pint = ffi.new(gvalue.pint_typeof, 1) + + local array = vips_lib.vips_value_get_array_image(gv, pint) + result = {} + for i = 0, pint[0] - 1 do + -- this will make a new cdata object + local vimage = array[i] + + -- vips_value_get_array_image() adds a ref for each image in + -- the array ... we must remember to drop them + gobject_lib.g_object_ref(vimage) + + result[i + 1] = Image.new(vimage) + end + elseif gtype_comp == gvalue.blob_type then + local psize = ffi.new(gvalue.psize_typeof, 1) + + local array = vips_lib.vips_value_get_blob(gv, psize) + + result = ffi.string(array, tonumber(psize[0])) + elseif gtype_comp == gvalue.interpolate_type then + local vo = gobject_lib.g_value_get_object(gv) + result = ffi.cast(gvalue.interpolate_typeof, vo) + elseif gtype_comp == gvalue.connection_type then + local vo = gobject_lib.g_value_get_object(gv) + result = ffi.cast(gvalue.connection_typeof, vo) + elseif gtype_comp == gvalue.source_type then + local vo = gobject_lib.g_value_get_object(gv) + result = ffi.cast(gvalue.source_typeof, vo) + elseif gtype_comp == gvalue.target_type then + local vo = gobject_lib.g_value_get_object(gv) + result = ffi.cast(gvalue.target_typeof, vo) + else + error("unsupported gtype for get " .. gvalue.type_name(gtype)) + end + + return result +end + +return ffi.metatype("GValue", { + __new = function(ct, pt) + -- if pt equals to true you'll need to + -- g_value_unset() yourself after calling it, + -- it won't unset() automatically! + return pt and ffi.new(gvalue.pgv_typeof) or ffi.new(ct) + end, + __gc = function(gv) - gobject.g_value_unset(gv) + gobject_lib.g_value_unset(gv) end, - __index = { - -- make ffi constructors we can reuse - gv_typeof = ffi.typeof("GValue"), - pgv_typeof = ffi.typeof("GValue[1]"), - image_typeof = ffi.typeof("VipsImage*"), - pimage_typeof = ffi.typeof("VipsImage*[?]"), - pint_typeof = ffi.typeof("int[?]"), - pdouble_typeof = ffi.typeof("double[?]"), - psize_typeof = ffi.typeof("size_t[?]"), - pstr_typeof = ffi.typeof("char*[?]"), - mem_typeof = ffi.typeof("unsigned char[?]"), - - -- look up some common gtypes at init for speed - gbool_type = gobject.g_type_from_name("gboolean"), - gint_type = gobject.g_type_from_name("gint"), - gdouble_type = gobject.g_type_from_name("gdouble"), - gstr_type = gobject.g_type_from_name("gchararray"), - genum_type = gobject.g_type_from_name("GEnum"), - gflags_type = gobject.g_type_from_name("GFlags"), - image_type = gobject.g_type_from_name("VipsImage"), - array_int_type = gobject.g_type_from_name("VipsArrayInt"), - array_double_type = gobject.g_type_from_name("VipsArrayDouble"), - array_image_type = gobject.g_type_from_name("VipsArrayImage"), - refstr_type = gobject.g_type_from_name("VipsRefString"), - blob_type = gobject.g_type_from_name("VipsBlob"), - band_format_type = gobject.g_type_from_name("VipsBandFormat"), - blend_mode_type = version.at_least(8, 6) and - gobject.g_type_from_name("VipsBlendMode") or 0, - - new = function() - -- with no init, this will initialize with 0, which is what we need - -- for a blank GValue - return ffi.new(gvalue.gv_typeof) - end, - - to_enum = function(gtype, value) - -- turn a string into an int, ready to be passed into libvips - local enum_value - - if type(value) == "string" then - enum_value = vips.vips_enum_from_nick("lua-vips", gtype, value) - - if enum_value < 0 then - error("no such enum " .. value .. "\n" .. verror.get()) - end - else - enum_value = value - end - - return enum_value - end, - - -- this won't be unset() automatically! you need to - -- g_value_unset() yourself after calling - newp = function() - return ffi.new(gvalue.pgv_typeof) - end, - - type_name = function(gtype) - return(ffi.string(gobject.g_type_name(gtype))) - end, - - init = function(gv, gtype) - gobject.g_value_init(gv, gtype) - end, - - set = function(gv, value) - local gtype = gv.gtype - local fundamental = gobject.g_type_fundamental(gtype) - - if gtype == gvalue.gbool_type then - gobject.g_value_set_boolean(gv, value) - elseif gtype == gvalue.gint_type then - gobject.g_value_set_int(gv, value) - elseif gtype == gvalue.gdouble_type then - gobject.g_value_set_double(gv, value) - elseif fundamental == gvalue.genum_type then - gobject.g_value_set_enum(gv, gvalue.to_enum(gtype, value)) - elseif fundamental == gvalue.gflags_type then - gobject.g_value_set_flags(gv, value) - elseif gtype == gvalue.gstr_type or gtype == gvalue.refstr_type then - gobject.g_value_set_string(gv, value) - elseif gtype == gvalue.image_type then - gobject.g_value_set_object(gv, value.vimage) - elseif gtype == gvalue.array_int_type then - if type(value) == "number" then - value = {value} - end - - local n = #value - local a = ffi.new(gvalue.pint_typeof, n, value) - - vips.vips_value_set_array_int(gv, a, n) - - elseif gtype == gvalue.array_double_type then - if type(value) == "number" then - value = {value} - end - - local n = #value - local a = ffi.new(gvalue.pdouble_typeof, n, value) - - vips.vips_value_set_array_double(gv, a, n) - - elseif gtype == gvalue.array_image_type then - if Image.is_Image(value) then - value = {value} - end - - local n = #value - - vips.vips_value_set_array_image(gv, n) - local a = vips.vips_value_get_array_image(gv, nil) - - for i = 0, n - 1 do - a[i] = value[i + 1].vimage - -- the gvalue needs a set of refs to own - gobject.g_object_ref(a[i]) - end - - elseif gtype == gvalue.blob_type then - -- we need to set the blob to a copy of the lua string that vips - -- can own - local n = #value - - local buf = glib.g_malloc(n) - ffi.copy(buf, value, n) - vips.vips_value_set_blob(gv, glib.g_free, buf, n) - else - error("unsupported gtype for set " .. gvalue.type_name(gtype)) - end - end, - - get = function(gv) - local gtype = gv.gtype - local fundamental = gobject.g_type_fundamental(gtype) - - local result - - if gtype == gvalue.gbool_type then - result = gobject.g_value_get_boolean(gv) - elseif gtype == gvalue.gint_type then - result = gobject.g_value_get_int(gv) - elseif gtype == gvalue.gdouble_type then - result = gobject.g_value_get_double(gv) - elseif fundamental == gvalue.genum_type then - local enum_value = gobject.g_value_get_enum(gv) - - local cstr = vips.vips_enum_nick(gtype, enum_value) - - if cstr == nil then - error("value not in enum") - end - - result = ffi.string(cstr) - elseif fundamental == gvalue.gflags_type then - result = gobject.g_value_get_flags(gv) - elseif gtype == gvalue.gstr_type then - local cstr = gobject.g_value_get_string(gv) - - if cstr ~= nil then - result = ffi.string(cstr) - else - result = nil - end - elseif gtype == gvalue.refstr_type then - local psize = ffi.new(gvalue.psize_typeof, 1) - - local cstr = vips.vips_value_get_ref_string(gv, psize) - - result = ffi.string(cstr, psize[0]) - elseif gtype == gvalue.image_type then - -- g_value_get_object() will not add a ref ... that is - -- held by the gvalue - local vo = gobject.g_value_get_object(gv) - local vimage = ffi.cast(gvalue.image_typeof, vo) - - -- we want a ref that will last with the life of the vimage: - -- this ref is matched by the unref that's attached to finalize - -- by Image.new() - gobject.g_object_ref(vimage) - - result = Image.new(vimage) - - elseif gtype == gvalue.array_int_type then - local pint = ffi.new(gvalue.pint_typeof, 1) - - local array = vips.vips_value_get_array_int(gv, pint) - result = {} - for i = 0, pint[0] - 1 do - result[i + 1] = array[i] - end - - elseif gtype == gvalue.array_double_type then - local pint = ffi.new(gvalue.pint_typeof, 1) - - local array = vips.vips_value_get_array_double(gv, pint) - result = {} - for i = 0, pint[0] - 1 do - result[i + 1] = array[i] - end - - elseif gtype == gvalue.array_image_type then - local pint = ffi.new(gvalue.pint_typeof, 1) - - local array = vips.vips_value_get_array_image(gv, pint) - result = {} - for i = 0, pint[0] - 1 do - -- this will make a new cdata object - local vimage = array[i] - - -- vips_value_get_array_image() adds a ref for each image in - -- the array ... we must remember to drop them - vobject.new(vimage) - - result[i + 1] = Image.new(vimage) - end - - elseif gtype == gvalue.blob_type then - local psize = ffi.new(gvalue.psize_typeof, 1) - - local array = vips.vips_value_get_blob(gv, psize) - - result = ffi.string(array, psize[0]) - else - error("unsupported gtype for get " .. gvalue.type_name(gtype)) - end - - return result - end, - - } -} - -gvalue = ffi.metatype("GValue", gvalue_mt) -return gvalue + __index = gvalue +}) diff --git a/src/vips/log.lua b/src/vips/log.lua index 338d86c..fee04df 100644 --- a/src/vips/log.lua +++ b/src/vips/log.lua @@ -1,20 +1,27 @@ -- simple logging -local logging_on = false +local logging_enabled = false + +local type = type +local print = print +local pairs = pairs +local unpack = unpack or table.unpack +local tostring = tostring +local str_rep = string.rep local log = {} log = { enable = function(on) - logging_on = on + logging_enabled = on end, msg = function(...) - if logging_on then - print(unpack{...}) + if logging_enabled then + print(unpack { ... }) end end, - prettyprint_table = function(p, t) + prettyprint_table = function(p, table) local p_r_cache = {} local function sub_p_r(t, indent) if (p_r_cache[tostring(t)]) then @@ -24,18 +31,17 @@ log = { if type(t) == "table" then for pos, val in pairs(t) do if type(val) == "table" then - p(indent .. - "[" .. pos .. "] => " .. tostring(t) .. " {") - sub_p_r(val, indent .. - string.rep(" ", string.len(pos) + 8)) - p(indent .. - string.rep(" ", string.len(pos) + 6) .. "}") + p(indent .. + "[" .. pos .. "] => " .. tostring(t) .. " {") + local length = type(pos) == "string" and #pos or pos + sub_p_r(val, indent .. str_rep(" ", length + 8)) + p(indent .. str_rep(" ", length + 6) .. "}") elseif type(val) == "string" then - p(indent .. "[".. pos .. '] => "' .. - val .. '"') + p(indent .. "[" .. pos .. '] => "' .. + val .. '"') else - p(indent .. "[" .. pos .. "] => " .. - tostring(val)) + p(indent .. "[" .. pos .. "] => " .. + tostring(val)) end end else @@ -43,25 +49,28 @@ log = { end end end - if type(t) == "table" then - p(tostring(t) .. " {") - sub_p_r(t, " ") + + if type(table) == "table" then + p(tostring(table) .. " {") + sub_p_r(table, " ") p("}") else - sub_p_r(t, " ") + sub_p_r(table, " ") end p() end, msg_r = function(t) - log.prettyprint_table(log.msg, t) + if logging_enabled then + log.prettyprint_table(log.msg, t) + end end, print_r = function(t) - log.prettyprint_table(print, t) - end, - + if logging_enabled then + log.prettyprint_table(print, t) + end + end } return log - diff --git a/src/vips/verror.lua b/src/vips/verror.lua index 60c1bd4..00163fc 100644 --- a/src/vips/verror.lua +++ b/src/vips/verror.lua @@ -1,27 +1,17 @@ -- handle the libvips error buffer -local ffi = require "ffi" +local ffi = require "ffi" -local vips = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") +local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") -ffi.cdef[[ - const char* vips_error_buffer (void); - void vips_error_clear (void); - -]] - -local verror = {} - -verror = { - -- get and clear the error buffer +local verror = { + -- get and clear the error buffer get = function() - local errstr = ffi.string(vips.vips_error_buffer()) - vips.vips_error_clear() + local errstr = ffi.string(vips_lib.vips_error_buffer()) + vips_lib.vips_error_clear() return errstr - end, - + end } return verror - diff --git a/src/vips/version.lua b/src/vips/version.lua index fe9aa0c..cd40bce 100644 --- a/src/vips/version.lua +++ b/src/vips/version.lua @@ -1,28 +1,21 @@ -- detect and test libvips version -local ffi = require "ffi" +local ffi = require "ffi" -local vips = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") - -ffi.cdef[[ - int vips_version (int flag); - -]] +local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") local version = {} version = { - major = vips.vips_version(0), - minor = vips.vips_version(1), - micro = vips.vips_version(2), + major = vips_lib.vips_version(0), + minor = vips_lib.vips_version(1), + micro = vips_lib.vips_version(2), - -- test for libvips version is better than x.y .. we use this to turn on + -- test for libvips version is better than x.y .. we use this to turn on -- various workarounds for older libvips at_least = function(x, y) return version.major > x or (version.major == x and version.minor >= y) - end, - + end } return version - diff --git a/src/vips/vimage.lua b/src/vips/vimage.lua deleted file mode 100644 index e9d65dc..0000000 --- a/src/vips/vimage.lua +++ /dev/null @@ -1,14 +0,0 @@ --- manage the cdata VipsImage value - -local ffi = require "ffi" - -local vips = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") - -ffi.cdef[[ - typedef struct _VipsImage { - VipsObject parent_instance; - - // opaque - } VipsImage; - -]] diff --git a/src/vips/vobject.lua b/src/vips/vobject.lua index 112aa56..a59c8c6 100644 --- a/src/vips/vobject.lua +++ b/src/vips/vobject.lua @@ -3,186 +3,128 @@ local ffi = require "ffi" -local verror = require "vips/verror" -local log = require "vips/log" -local gvalue = require "vips/gvalue" +local verror = require "vips.verror" +local log = require "vips.log" +local gvalue = require "vips.gvalue" -local vips -local gobject +local print = print +local error = error +local collectgarbage = collectgarbage + +local vips_lib +local gobject_lib if ffi.os == "Windows" then - vips = ffi.load("libvips-42.dll") - gobject = ffi.load("libgobject-2.0-0.dll") + vips_lib = ffi.load("libvips-42.dll") + gobject_lib = ffi.load("libgobject-2.0-0.dll") else - vips = ffi.load("vips") - gobject = vips + vips_lib = ffi.load("vips") + gobject_lib = vips_lib end -ffi.cdef[[ - typedef struct _GObject { - void *g_type_instance; - unsigned int ref_count; - void *qdata; - } GObject; - - typedef struct _VipsObject { - GObject parent_object; - bool constructed; - bool static_object; - void *argument_table; - char *nickname; - char *description; - bool preclose; - bool close; - bool postclose; - size_t local_memory; - } VipsObject; - - typedef struct _VipsObjectClass { - // opaque - } VipsObjectClass; - - typedef struct _GParamSpec { - void* g_type_instance; - - const char* name; - unsigned int flags; - GType value_type; - GType owner_type; - - // rest opaque - } GParamSpec; - - typedef struct _VipsArgument { - GParamSpec *pspec; - } VipsArgument; - - typedef struct _VipsArgumentInstance { - VipsArgument parent; - - // opaque - } VipsArgumentInstance; - - typedef enum _VipsArgumentFlags { - VIPS_ARGUMENT_NONE = 0, - VIPS_ARGUMENT_REQUIRED = 1, - VIPS_ARGUMENT_CONSTRUCT = 2, - VIPS_ARGUMENT_SET_ONCE = 4, - VIPS_ARGUMENT_SET_ALWAYS = 8, - VIPS_ARGUMENT_INPUT = 16, - VIPS_ARGUMENT_OUTPUT = 32, - VIPS_ARGUMENT_DEPRECATED = 64, - VIPS_ARGUMENT_MODIFY = 128 - } VipsArgumentFlags; - - typedef struct _VipsArgumentClass { - VipsArgument parent; - - VipsObjectClass *object_class; - VipsArgumentFlags flags; - int priority; - uint64_t offset; - } VipsArgumentClass; - - int vips_object_get_argument (VipsObject* object, - const char *name, GParamSpec** pspec, - VipsArgumentClass** argument_class, - VipsArgumentInstance** argument_instance); - - void g_object_set_property (VipsObject* object, - const char *name, GValue* value); - void g_object_get_property (VipsObject* object, - const char* name, GValue* value); - - void vips_object_print_all (void); - -]] - local vobject = {} -local vobject_mt = { + +-- types to get ref back from vips_object_get_argument() +vobject.typeof = ffi.typeof("VipsObject*") +vobject.pspec_typeof = ffi.typeof("GParamSpec*[1]") +vobject.argument_class_typeof = ffi.typeof("VipsArgumentClass*[1]") +vobject.argument_instance_typeof = ffi.typeof("VipsArgumentInstance*[1]") + +vobject.print_all = function(msg) + collectgarbage() + print(msg) + vips_lib.vips_object_print_all() + print() +end + +vobject.new = function(pt) + return ffi.gc(pt, gobject_lib.g_object_unref) +end + +-- return 0 for not found and leave the error in the error log +vobject.get_typeof = function(self, name) + local pspec = vobject.pspec_typeof() + local argument_class = vobject.argument_class_typeof() + local argument_instance = vobject.argument_instance_typeof() + local result = vips_lib.vips_object_get_argument(self, name, + pspec, argument_class, argument_instance) + + if result ~= 0 then + return 0 + end + + return pspec[0].value_type +end + +vobject.get_type = function(self, name, gtype) + log.msg("vobject.get_type") + log.msg(" name =", name) + + if gtype == 0 then + return false + end + + local pgv = gvalue(true) + pgv[0]:init(gtype) + + -- this will add a ref for GObject properties, that ref will be + -- unreffed when the gvalue is finalized + gobject_lib.g_object_get_property(self, name, pgv) + + local result = pgv[0]:get() + gobject_lib.g_value_unset(pgv[0]) + + return result +end + +vobject.set_type = function(self, name, value, gtype) + log.msg("vobject.set_type") + log.msg(" name =", name) + log.msg(" value =", value) + + if gtype == 0 then + return false + end + + local pgv = gvalue(true) + pgv[0]:init(gtype) + pgv[0]:set(value) + gobject_lib.g_object_set_property(self, name, pgv) + gobject_lib.g_value_unset(pgv[0]) + + return true +end + +vobject.get = function(self, name) + log.msg("vobject.get") + log.msg(" name =", name) + + local gtype = self:get_typeof(name) + if gtype == 0 then + error(verror.get()) + end + + return vobject.get_type(self, name, gtype) +end + +vobject.set = function(self, name, value) + log.msg("vobject.set") + log.msg(" name =", name) + log.msg(" value =", value) + + local gtype = self:get_typeof(name) + if gtype == 0 then + error(verror.get()) + end + + vobject.set_type(self, name, value, gtype) + + return true +end + +return ffi.metatype("VipsObject", { + -- no __gc method, we don't build these things ourselves, just wrap the -- pointer, so we use ffi.gc() instead - __index = { - -- types to get ref back from vips_object_get_argument() - typeof = ffi.typeof("VipsObject*"), - pspec_typeof = ffi.typeof("GParamSpec*[1]"), - argument_class_typeof = ffi.typeof("VipsArgumentClass*[1]"), - argument_instance_typeof = ffi.typeof("VipsArgumentInstance*[1]"), - - print_all = function(msg) - collectgarbage() - print(msg) - vips.vips_object_print_all() - print() - end, - - new = function(self) - ffi.gc(self, - function(x) - gobject.g_object_unref(x) - end - ) - return self - end, - - -- return 0 for not found and leave the error in the error log - get_typeof = function(self, name) - local pspec = vobject.pspec_typeof() - local argument_class = vobject.argument_class_typeof() - local argument_instance = vobject.argument_instance_typeof() - local result = vips.vips_object_get_argument(self, name, - pspec, argument_class, argument_instance) - - if result ~= 0 then - return 0 - end - - return pspec[0].value_type - end, - - get = function(self, name) - log.msg("vobject.get") - log.msg(" self =", self) - log.msg(" name =", name) - - local gtype = self:get_typeof(name) - if gtype == 0 then - error(verror.get()) - end - - local pgv = gvalue.newp() - pgv[0]:init(gtype) - -- this will add a ref for GObject properties, that ref will be - -- unreffed when the gvalue is finalized - gobject.g_object_get_property(self, name, pgv) - - local result = pgv[0]:get() - gobject.g_value_unset(pgv[0]) - - return result - end, - - set = function(self, name, value) - log.msg("vobject.set") - log.msg(" self =", self) - log.msg(" name =", name) - log.msg(" value =", value) - - local gtype = self:get_typeof(name) - if gtype == 0 then - error(verror.get()) - end - - local gv = gvalue.new() - gv:init(gtype) - gv:set(value) - gobject.g_object_set_property(self, name, gv) - - return true - end, - - } -} - -vobject = ffi.metatype("VipsObject", vobject_mt) -return vobject + __index = vobject +}) diff --git a/src/vips/voperation.lua b/src/vips/voperation.lua index 3756301..cdc2f77 100644 --- a/src/vips/voperation.lua +++ b/src/vips/voperation.lua @@ -2,70 +2,47 @@ -- lookup and call operations local ffi = require "ffi" -local bit = require "bit" -local band = bit.band -local verror = require "vips/verror" -local log = require "vips/log" -local gvalue = require "vips/gvalue" -local vobject = require "vips/vobject" -local Image = require "vips/Image" +local verror = require "vips.verror" +local version = require "vips.version" +local log = require "vips.log" +local gvalue = require "vips.gvalue" +local vobject = require "vips.vobject" +local Image = require "vips.Image" -local vips = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") +local type = type +local error = error +local pairs = pairs +local unpack = unpack or table.unpack +local tonumber = tonumber +local str_gsub = string.gsub -ffi.cdef[[ - typedef struct _VipsOperation { - VipsObject parent_instance; - - // opaque - } VipsOperation; - - VipsOperation* vips_operation_new (const char* name); - - typedef void *(*VipsArgumentMapFn) (VipsOperation* object, - GParamSpec* pspec, - VipsArgumentClass* argument_class, - VipsArgumentInstance* argument_instance, - void* a, void* b); - - void* vips_argument_map (VipsOperation* object, - VipsArgumentMapFn fn, void* a, void* b); - - VipsOperation* vips_cache_operation_build (VipsOperation* operation); - void vips_object_unref_outputs (VipsOperation *operation); - - int vips_object_set_from_string (VipsObject* object, const char* options); - -]] +local vips_lib = ffi.load(ffi.os == "Windows" and "libvips-42.dll" or "vips") local REQUIRED = 1 -local CONSTRUCT = 2 -local SET_ONCE = 4 -local SET_ALWAYS = 8 +local CONSTRUCT = 2 -- luacheck: ignore +local SET_ONCE = 4 -- luacheck: ignore +local SET_ALWAYS = 8 -- luacheck: ignore local INPUT = 16 local OUTPUT = 32 local DEPRECATED = 64 local MODIFY = 128 -local function map(fn, array) - local new_array = {} - - for i, v in ipairs(array) do - new_array[i] = fn(v) - end - - return new_array +local function isbitset(a, b) + return ( (a - (a % b)) / b ) % 2 == 1 end --- find in order, and recurse -local function find_order(fn, array) - for i = 1, #array do - if fn(array[i]) then +-- find the first image, and recurse +local function find_first_image(array, length) + length = length or #array + + for i = 1, length do + if Image.is_Image(array[i]) then return array[i] elseif type(array[i]) == "table" then - local result = find_order(fn, array[i]) + local result = find_first_image(array[i]) - if result then + if result then return result end end @@ -75,233 +52,250 @@ local function find_order(fn, array) end local voperation = {} -local voperation_mt = { - __index = { - argumentmap_typeof = ffi.typeof("VipsArgumentMapFn"), - - -- cast to a vobject ... this will create a new cdata object, but won't - -- change any VipsObject reference counts, nor add a finalizer - vobject = function(self) - return ffi.cast(vobject.typeof, self) - end, - - -- but for new() we can't do self:vobject():new() since that would - -- attach the unref callback to the cdata object made by the vobject() - -- cast, not to this voperation - new = function(self) - return vobject.new(self) - end, - - set = function(self, name, flags, match_image, value) - -- if the object wants an image and we have a constant, imageize it - -- - -- if the object wants an image array, imageize any constants in the - -- array - if match_image then - local gtype = self:vobject():get_typeof(name) - - if gtype == gvalue.image_type then - value = match_image:imageize(value) - elseif gtype == gvalue.array_image_type then - value = map( - function(x) - return match_image:imageize(x) - end, - value) - end - end - -- MODIFY args need to be copied before they are set - if band(flags, MODIFY) ~= 0 then - log.msg("copying MODIFY arg", name) - -- make sure we have a unique copy - value = value:copy():copy_memory() +voperation.argumentmap_typeof = ffi.typeof("VipsArgumentMapFn") +voperation.pstring_array_typeof = ffi.typeof("const char**[1]") +voperation.pint_array_typeof = ffi.typeof("int*[1]") +voperation.pint_typeof = ffi.typeof("int[1]") + +-- cast to a vobject ... this will create a new cdata object, but won't +-- change any VipsObject reference counts, nor add a finalizer +-- TODO: Could we use `self.parent_instance` here? +voperation.vobject = function(self) + return ffi.cast(vobject.typeof, self) +end + +-- but for new() we can't do self:vobject():new() since that would +-- attach the unref callback to the cdata object made by the vobject() +-- cast, not to this voperation +voperation.new = function(self) + return vobject.new(self) +end + +voperation.set = function(self, name, flags, match_image, value) + local vob = self:vobject() + local gtype = vob:get_typeof(name) + local gtype_comp = gvalue.comparable_type(gtype) + + -- if the object wants an image and we have a constant, imageize it + -- + -- if the object wants an image array, imageize any constants in the + -- array + if match_image then + if gtype_comp == gvalue.image_type then + value = match_image:imageize(value) + elseif gtype_comp == gvalue.array_image_type then + for i = 1, #value do + value[i] = match_image:imageize(value[i]) end + end + end + + -- MODIFY args need to be copied before they are set + if isbitset(flags, MODIFY) then + log.msg("copying MODIFY arg", name) + -- make sure we have a unique copy + value = value:copy():copy_memory() + end + + return vob:set_type(name, value, gtype) +end + +-- this is slow ... call as little as possible +voperation.getargs = function(self) + local names = {} + local flags = {} + local n_args = 0 - return self:vobject():set(name, value) - end, + if version.at_least(8, 7) then + local p_names = ffi.new(voperation.pstring_array_typeof) + local p_flags = ffi.new(voperation.pint_array_typeof) + local p_n_args = ffi.new(voperation.pint_typeof) - -- this is slow ... call as little as possible - getargs = function(self) - local args = {} - local cb = ffi.cast(voperation.argumentmap_typeof, - function(self, pspec, argument_class, argument_instance, a, b) - local name = ffi.string(pspec.name) + vips_lib.vips_object_get_args(self, p_names, p_flags, p_n_args) + + p_names = p_names[0] + p_flags = p_flags[0] + n_args = p_n_args[0] + + -- C-array is numbered from zero + for i = 0, n_args - 1 do + names[i + 1] = str_gsub(ffi.string(p_names[i]), "-", "_") + flags[i + 1] = p_flags[i] + end + else + local cb = ffi.cast(voperation.argumentmap_typeof, + function(_, pspec, argument_class, _, _, _) + n_args = n_args + 1 -- libvips uses "-" to separate parts of arg names, but we -- need "_" for lua - name = string.gsub(name, "-", "_") - - table.insert(args, - {name = name, - flags = tonumber(argument_class.flags) - } - ) - end - ) - vips.vips_argument_map(self, cb, nil, nil ) - cb:free() - - return args - end, - - -- string_options is any optional args coded as a string, perhaps - -- "[strip,tile=true]" - call = function(name, string_options, ...) - local call_args = {...} - - local vop = vips.vips_operation_new(name) - if vop == nil then - error("no such operation\n" .. verror.get()) - end - vop:new() + names[n_args] = str_gsub(ffi.string(pspec.name), "-", "_") + flags[n_args] = tonumber(argument_class.flags) + end) + vips_lib.vips_argument_map(self, cb, nil, nil) + cb:free() + end - local arguments = vop:getargs() + return names, flags, n_args +end - log.msg("calling operation:", name) - log.msg("passed:") - log.msg_r(call_args) +-- string_options is any optional args coded as a string, perhaps +-- "[strip,tile=true]" +voperation.call = function(name, string_options, ...) + local call_args = { ... } - -- count required input args - local n_required = 0 - for i = 1, #arguments do - local flags = arguments[i].flags + local vop = vips_lib.vips_operation_new(name) + if vop == ffi.NULL then + error("no such operation\n" .. verror.get()) + end + vop = vop:new() - if band(flags, INPUT) ~= 0 and - band(flags, REQUIRED) ~= 0 and - band(flags, DEPRECATED) == 0 then - n_required = n_required + 1 - end - end + local names, flags, arguments_length = vop:getargs() - -- so we should have been passed n_required, or n_required + 1 if - -- there's a table of options at the end - local last_arg - if #call_args == n_required then - last_arg = nil - elseif #call_args == n_required + 1 then - last_arg = call_args[#call_args] - if type(last_arg) ~= "table" then - error("unable to call " .. name .. ": " .. #call_args .. - " arguments given, " .. n_required .. - ", but final argument is not a table") - end - else - error("unable to call " .. name .. ": " .. #call_args .. - " arguments given, but " .. n_required .. " required") - end + -- cache the call args length + local call_args_length = #call_args - -- the first image argument is the thing we expand constants to - -- match ... look inside tables for images, since we may be passing - -- an array of image as a single param - local match_image = find_order( - function(x) - if Image.is_Image(x) then - return x - else - return nil - end - end, - call_args - ) - - -- set any string options before any args so they can't be - -- overridden - if vips.vips_object_set_from_string(vop:vobject(), - string_options) ~= 0 then - error("unable to call " .. name .. "\n" .. verror.get()) - end + log.msg("calling operation:", name) + log.msg("passed:") + log.msg_r(call_args) + + -- make a thing to quickly get flags from an arg name + local flags_from_name = {} - local n = 0 - for i = 1, #arguments do - local flags = arguments[i].flags + -- count required input args + local n_required = 0 + for i = 1, arguments_length do + local flag = flags[i] + flags_from_name[names[i]] = flag - if band(flags, INPUT) ~= 0 and - band(flags, REQUIRED) ~= 0 and - band(flags, DEPRECATED) == 0 then - n = n + 1 + if isbitset(flag, INPUT) and + isbitset(flag, REQUIRED) and + not isbitset(flag, DEPRECATED) then + n_required = n_required + 1 + end + end - if not vop:set(arguments[i].name, flags, - match_image, call_args[n]) then - error("unable to call " .. name .. "\n" .. verror.get()) - end - end + -- so we should have been passed n_required, or n_required + 1 if + -- there's a table of options at the end + local last_arg + if call_args_length == n_required then + last_arg = nil + elseif call_args_length == n_required + 1 then + last_arg = call_args[#call_args] + if type(last_arg) ~= "table" then + error("unable to call " .. name .. ": " .. call_args_length .. + " arguments given, " .. n_required .. + ", but final argument is not a table") + end + else + error("unable to call " .. name .. ": " .. call_args_length .. + " arguments given, but " .. n_required .. " required") + end + + -- the first image argument is the thing we expand constants to + -- match ... look inside tables for images, since we may be passing + -- an array of image as a single param + local match_image = find_first_image(call_args, call_args_length) + + -- set any string options before any args so they can't be + -- overridden + if vips_lib.vips_object_set_from_string(vop:vobject(), + string_options) ~= 0 then + error("unable to call " .. name .. "\n" .. verror.get()) + end + + local n = 0 + for i = 1, arguments_length do + local flag = flags[i] + + if isbitset(flag, INPUT) and + isbitset(flag, REQUIRED) and + not isbitset(flag, DEPRECATED) then + n = n + 1 + + if not vop:set(names[i], flag, + match_image, call_args[n]) then + error("unable to call " .. name .. "\n" .. verror.get()) end + end + end - if last_arg then - local args_by_name = {} - for i = 1, #arguments do - args_by_name[arguments[i].name] = arguments[i].flags - end - - for k, v in pairs(last_arg) do - if not vop:set(k, args_by_name[k], match_image, v) then - error("unable to call " .. name .. "\n" .. - verror.get()) - end - end + if last_arg then + for k, v in pairs(last_arg) do + local flag = flags_from_name[k] + if not flag then + error("unable to call " .. name .. ": invalid flag '" .. + tostring(k) .. "'") end - local vop2 = vips.vips_cache_operation_build(vop) - if vop2 == nil then + if not vop:set(k, flag, match_image, v) then error("unable to call " .. name .. "\n" .. verror.get()) end - vop2:new() - vop = vop2 - vop2 = nil - - local result = {} - local vob = vop:vobject() - n = 1 - for i = 1, #arguments do - local flags = arguments[i].flags - - if band(flags, OUTPUT) ~= 0 and - band(flags, REQUIRED) ~= 0 and - band(flags, DEPRECATED) == 0 then - result[n] = vob:get(arguments[i].name) - n = n + 1 - end - - -- MODIFY input args are returned .. this will get the copy we - -- made above - if band(flags, INPUT) ~= 0 and - band(flags, MODIFY) ~= 0 then - result[n] = vob:get(arguments[i].name) - n = n + 1 - end - end + end + end - for i = 1, #arguments do - local flags = arguments[i].flags + local vop2 = vips_lib.vips_cache_operation_build(vop) + if vop2 == ffi.NULL then + error("unable to call " .. name .. "\n" .. verror.get()) + end + vop = vop2:new() - if band(flags, OUTPUT) ~= 0 and - band(flags, REQUIRED) == 0 and - band(flags, DEPRECATED) == 0 then - result[n] = vob:get(arguments[i].name) - n = n + 1 - end - end + local result = {} + local vob = vop:vobject() - for i = 1, #arguments do - local flags = arguments[i].flags + -- fetch required output args, plus modified input images + n = 1 + for i = 1, arguments_length do + local flag = flags[i] - if band(flags, OUTPUT) ~= 0 and - band(flags, DEPRECATED) ~= 0 then - result[n] = vob:get(arguments[i].name) - n = n + 1 - end - end + if isbitset(flag, OUTPUT) and + isbitset(flag, REQUIRED) and + not isbitset(flag, DEPRECATED) then + result[n] = vob:get(names[i]) + n = n + 1 + end - vips.vips_object_unref_outputs(vop) - vop = nil + -- MODIFY input args are returned .. this will get the copy we + -- made above + if isbitset(flag, INPUT) and + isbitset(flag, MODIFY) then + result[n] = vob:get(names[i]) + n = n + 1 + end + end - return unpack(result) - end, + -- fetch optional output args + for i = 1, arguments_length do + local flag = flags[i] - } -} + if isbitset(flag, OUTPUT) and + not isbitset(flag, REQUIRED) and + not isbitset(flag, DEPRECATED) then + result[n] = vob:get(names[i]) + n = n + 1 + end + end -voperation = ffi.metatype("VipsOperation", voperation_mt) -return voperation + -- garbage collection during vips_object_unref_outputs leads to crashes on Lua 5.3 + collectgarbage("stop") + vips_lib.vips_object_unref_outputs(vop) + collectgarbage("restart") + + -- this strange if expression is because unpack + -- has not yet been implemented in the JIT compiler + -- of LuaJIT, see: http://wiki.luajit.org/NYI. + if n == 1 then + return nil + elseif n == 2 then + return result[1] + else + -- we could extend this if expression even more, + -- but usually one item is returned. + return unpack(result) + end +end +return ffi.metatype("VipsOperation", { + __index = voperation +})