This marks the end of work on nip4’s language. Next month will focus on the menu redesign.
For some background, there’s an older introduction to nip4’s programming language post.
Left-drag on the workspace background to scroll is very handy, but left-drag also moved rows around. I found myself often dragging rows accidentally while navigating large workspaces, and then putting the rows back in the right place was annoying.
You now have to hold down CTRL to drag rows around. Hopefully this will stop accidental row moves!
I’ve rewritten lambda expressions so they now support patterns and deconstruction. For example:
main = map (\[x, y] x + y) (zip2 [1..10] [11..20]);
So the lambda (the backslash character) expects a two element list with
elements named as x and y and adds them together. The zip2 makes
a list like [[1, 11], [2, 12], ..], so therefore main will have the
value [12, 14, 16, ..].
This means that (finally!) you can use patterns and argument deconstruction everywhere. For example, you could write that lambda expression as:
main = [x + y :: [x, y] <- zip2 [1..10] [11..20]];
I’ve also rewritten list comprehensions so they have much better scoping behaviour.
The compiler is now lazy – everything is parsed during load, but code generation only happens when functions are evaluated. This improves startup time.
And finally the programming language is officially renamed as snip, and
the old nip4-batch program is now installed as snip. You can use it to
write scripts with a shebang, perhaps:
#!/usr/bin/env snip
main = [x + y :: [x, y] <- zip2 [1..10] [11..20]];
You can run this and see:
$ ./try.def
[12, 14, 16, 18, 20, 22, 24, 26, 28, 30]
$
… though that’s not quite correct, for now you need a print in there too,
but this will be going away in the next version.
The headline features are support for UltraHDR, camera RAW images, and Oklab colourspace.
UltraHDR is a way of embedding a gainmap plus some extra metadata inside an ordinary SDR image. An SDR display can just show the regular SDR image, but an HDR display can extract the gainmap and use it to reconstruct a full HDR image. Having a single image file that can display well on both SDR and HDR devices is very valuable.
libvips 8.18 uses Google’s libultrahdr for UltraHDR load and save. The current version of this library only supports UltraHDR JPEG images; the next version is expected to add support for a wider range of image formats.
There are two main paths for UltraHDR images in libvips: as an SDR image with a separate gainmap, and as a full HDR image. The separate gainmap path is relatively fast but you will sometimes need to update the gainmap during processing. The full HDR path does not require gainmap updates, but can be slower, and will usually lose the original image’s tone mapping.
A new chapter in the libvips documentation introduces
this feature and explains how to use it. As an example, you can use
vipsthumbnail to resize UltraHDR images. This command:
$ vipsthumbnail ultra-hdr.jpg --size 1024
Makes this image:

If you view that image on an HDR display and with a web browser that supports UltraHDR images, the rocket exhaust should look very bright. It should also look nicely tonemapped on an SDR display.
We’d like to thank nlnet for their generous support while developing this feature.
Thanks to @lxsameer, libvips 8.18 now uses libraw
to add support for most camera RAW formats. The new
vips_dcrawload() operator will
be used to automatically import images, for example:
$ vipsthumbnail IMG_3260.CR2 --size 1024
Makes this image

Most time is spent in dcraw, so performance isn’t that much better than the previous solution with imagemagick:
$ /usr/bin/time -f %M:%e convert IMG_3260.CR2 -resize 500x x.jpg
442076:1.56
$ /usr/bin/time -f %M:%e vipsthumbnail IMG_3260.CR2 --size 500
359336:1.12
But it’s convenient to have it all in one thing.
Oklab and Oklch are new colourspaces that are more linear than CIELAB ‘76, faster to compute, and support HDR imaging. They have been added to CSS4 and are now implemented by all major web browsers.
libvips 8.18 supports them like any other colourspace, so you can use Oklab coordinates in image processing. For example, you could render a watermark in an Oklab colour like this:
#!/usr/bin/env python3
import sys
import pyvips
im = pyvips.Image.new_from_file(sys.argv[1], access="sequential")
# make the watermark
text = pyvips.Image \
.text(sys.argv[3], width=500, dpi=600, align="centre") \
.rotate(45)
colour = text \
.cast("float") \
.new_from_image([float(value) for value in sys.argv[4:]]) \
.copy(interpretation="oklab") \
.colourspace("srgb")
# use the text as the alpha, scaled down to make it semi-transparent
text = colour \
.bandjoin((text * 0.8).cast("uchar")) \
.copy_memory()
# replicate many times to cover the image
overlay = text \
.replicate(1 + im.width / text.width, 1 + im.height / text.height) \
.crop(0, 0, im.width, im.height)
# composite on top of the image
im = im.composite(overlay, "over")
im.write_to_file(sys.argv[2])
I can run it like this:
$ ./watermark-oklab.py ~/pics/theo.jpg x.jpg "in the beginning was the word" 0.7 0.2 -0.2
To generate:

A watermarked image, with the watermark colour specified in Oklab coordinates.
The libvips core has seen some useful improvements, mostly driven by interactive use:
The mmap window size hadn’t been reviewed for a long time, and I think had been previously set after benchmarking on a 32-bit machine with limited VMEM. For 64-bit machines this is now much larger, improving random access speeds for many file formats.
vips_system() has a new "cache"
argument which adds the command to the libvips operation cache. This makes
nip4 much, much faster at issuing ImageMagick commands.
A new system for forcing the exit of worker threads makes threadpoool shutdown dramatically faster, greatly improving interactive performance.
Tiled image formats now set metadata to hint their cache size to downstream operations. This can help prevent retiling, again improving interactive performance.
vipsthumbnail has a new "path"
argument. This gives you much more flexibility in how the output filename is
constructed. The old -o option is still supported, but is deprecated.
As well as the RAW support above, the other file format operations have seen some improvements:
vips_pdfload() has a new "page-box"
argument which lets you control which of the various media boxes you’d like
to load.
vips_jxlload() has a new "bitdepth"
argument which sets the depth at which the image should be loaded.
vips_webpsave() has a new
"exact" argument which forces the RGB in RGBA to always be saved, even if
the pixel is transparent. This can be important if you are using WebP to
store data.
vips_heifsave() has a new
"tune" parameter that lets you pass detailed options to the encoder. Handy
for tuning output.
For some background, there’s an older introduction to nip4 post.
nip4 is an image processing spreadsheet and, like all spreadsheets, you can type equations into cells.
The equations you can type are in fact a tiny pure functional programming language, and this language is used to implement all of nip4’s menus and workspace widgets. You can use it to add things to nip4 yourself.
One way to experiment with it is with the Workspace definitions pane. If you right-click on the workspace background and pick Workspace definitions from the menu, a pane will slide in from the right:

This pane holds definitions local to this workspace tab. They are saved in the workspace file, so they are a great way to add small extra things that are useful for whatever you are working on.
Try entering:
fred = 99;
Pressing the ▶ (play) button will make nip4 parse and compile your
definition. Back in the workspace, try entering fred at the bottom of
column A. You should see:

If you go back to the Workspace definitions pane, edit it to fred =
"banana";, and press ▶ again, A1 will update. Definitions are all live
and if you change one, everything that uses it will also update.
If you right-click on the workspace background and select Edit toolkits you’ll get something more like a tiny development environment for the language, but Workspace definitions is handier for little experiments.
The big new feature in this new version is multiple definitions. You can define a function many times and during execution the first one which matches the arguments becomes the result of the function.
For example, you could write this to define a factorial function:
factorial 1 = 1;
factorial n = n * factorial (n - 1);
Now factorial 6 in a column will evaluate to 720.
This is not a great way to write a factorial function! But it does show the syntax. Something like:
factorial n = product [1..n];
would be better.
A parallel new feature is function argument pattern matching and deconstruction.
Function arguments don’t have to be simple variables — they can be complex structure declarations. For example:
sum [] = 0;
sum a:x = a + sum x;
Square brackets mean lists and the colon (:) operator is infix CONS. The
first definition of sum will match if the argument is the empty list, and
the second for non-empty lists (lists which can be deconstructed with CONS),
with a being bound to the head of the list and x to the tail.
Back in the workspace, sum [1..10] should be 55. The [1..10] is a list
generator, it evaluates to [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].
This is a pretty bad way to define sum, a better solution is:
sum = foldr add 0
Where foldr is the standard right-associative list fold operator. You can define it very conveniently as:
foldr fn st [] = st;
foldr fn st x:xs = fn x (foldr fn st xs);
Patterns can be simple constants, complex numbers, list constants, : (cons),
classes and argument names. You can nest these in any way you like, so:
conj (x, y) = (x, -y);
Finds the complex conjugate, or:
banana [a, (x, y), c] = "a three element list, with a complex number as the middle element";
Or:
test (Image x) = "it's an image!";
This matches any x which is an instance of the Image class.
The next step is to update the nip4 menus, hopefully making them more consistent and easier to use. We’ll see!
]]>We were using gtk-doc for our documentation system. We’ve switched to its newer replacement, gi-docgen, and it should be a lot better.

The most interesting improvements are:
A much better search system – try typing in the search box at the top left!
Better and more consistent display of optional arguments, see for example
embed.
The class overview page includes a useful one-line description of each operator.
Revised and restructured Additional documentation.
Support for light and dark appearance.
More consistent markup should make it easier to automatically generate documentation for downstream projects.
To help compatibility, the old vips7 matrix multiply
function is now available as a vips8 operator,
matrixmultiply.
We rewrote the old vips7 remosaic function for vips8 years
ago, but stupidly forgot to link it. We’ve fixed this, and
remosaic
is now proudly available. Similarly,
quadratic
has been there for years, but never worked properly. It’s been revised and
should now (finally) be useful.
The so-called magic kernel is now supported for image resize.
ICC import and transform now has an
auto option
for rendering intent.
File format support has been improved (again). Highlights this time are:
gifsave
has a new keep_duplicate_frames option
svgload
has a new stylesheet option for custom CSS, and a high_bitdepth option
for scRGB output.
heifload
has a new unlimited flag to remove all load limits, has better alpha
channel detection, and better detection of truncated files.
jp2kload
has a new oneshot flag which can improve compatibility for older
jp2k files.
jxlsave
has much lower memory use for large images.
openslideload
now shares and reuses connections to the underlying file format library,
improving speed.
ppmload
has a new buffer variant for convenience.
TIFF load and save has better error handling
A new “convert for save” system fixes a large number of minor bugs and inconsistencies in file format support.
There have been some smaller libvips additions and improvements too.
The
hough_line
operator has better scaling to Hough space.
shrink
is noticeably faster
The operation cache is a lot more reliable.
sink_screen
has better scheduling and should render thumbnails much more quickly.
The ICC operators are better at detecting and rejecting corrupt profiles.
Plus some even more minor bugfixes and improvements.
]]>If you used nip2, there’s a nip4 for nip2 users post which runs though the main differences.
I’ll write a “nip4 for nerds” post next week introducing nip4’s programming language.
nip4 is a free spreadsheet-like GUI for the libvips image processing library with binaries for Linux, Windows and Mac. You can use it to build image processing pipelines and then execute them on large datasets. These pipelines can get quite complex — I’ve made systems with over 10,000 operations chained together.

This workspace analyses pulmonary PET-CT scans. It fits a two compartment model to each lung voxel to estimate a rate constant for FDG uptake (a proxy for inflammation).
Here’s the workspace developed in the Charisma project.

This workspace standardises and automates technical imaging in museums. You can load visible, infrared, ultraviolet, UV-induced visible luminescence and visible-induced IR luminescence images, and it computes a set of derivatives. This screenshot shows calibrated visible and calibrated UV-induced visible luminescence with stray-light removal and Kubelka–Munk modelling of scatter.
Because the underlying image processing engine is libvips, it’s fast and does not need a lot of memory. The Charisma workspace above, for example, loads in about 5s on this PC, manipulates almost 100GB of images, but runs in under 1GB of RAM:

Everything is live. You can open an image view window on any of the cells in the workspace and watch pixels change as you edit the formula.
The whole systems is lazy and demand-driven. You can load enormous images (many 100s of gigabytes) and manipulate them interactively, since only the pixels needed to update the screen actually get processed. When you do a final save operation, it will take a while, of course.
nip4 comes with a separate program called snip. This is a batch-mode
processor that can load nip4 workspace files and execute them on a set of
inputs. You can use to apply a workspace you’ve developed in nip4 to a big
collection of images.
When you start nip4 it looks something like this. I’ve loaded a test image
(drag one in, use the folder button at the top left, or start nip4 from the
command-line with nip4 my-great-image.jpg):

A is the current column, A1 is a row for the image you loaded, this is all
in tab1. The thing down the left is the set of loaded toolkits.
The toolkit menu contains around 300 useful operations, and you can make more yourself. You can click to move in and out of toolkits, or you can click in the magnifying glass at the top and search for tools by keyword.
If you select Filter > Photographic Negative you’ll see:

Most tools take one argument, and they are applied to the bottom row in the current column. If you want to apply a tool to a row other than the bottom one, select it first by clicking on the row label.
If you open up A2 by clicking on the V down button next to the label,
you’ll see the cell formula:

So the function Filter_negative_item.action has been applied to the row A1.
You can edit the formula — click on it, enter 255 - A1, and you should see:

Which computes almost the same result.
Rows can contain many types of object. Click at the top of the toolkit
bar to get back to the start position, then click Widgets > Scale to add a
scale widget called A3 to the workspace. Now edit the formula to be
A3 - A1 and try dragging the slider.
It looks a bit awkward with the result row A2 positioned before the scale.
You can reorder columns by holding down Ctrl and dragging on the row label.

nip4 does not have an undo operation, instead it has fast and easy duplicate,
merge and delete. If you make a copy of column A before you start changing
it, you can’t lose any of your current work.
Duplicate column A by right-clicking on the column title and selecting
Duplicate from the menu. Right-click on B3 and select Delete, so you have:

Now in the text box at the bottom of column B, enter this formula for
Solarization:
if B1 < B2 then 255 * B1 / B2 else 255 * (255 - B1) / (255 - B2)
And try dragging scale B2. Hopefully you’ll see a solarized image.
A version of this operation is also in the standard toolkits, next to
Photographic Negative.
If you double-click on an image thumbnail, you’ll open an image view window. These are all live, so as you drag scales, they’ll all update. You can zoom in and watch the values of individual pixels change as you edit formula.

The main window has some other useful features. You can pan large workspaces by dragging on the background, the burger menu at the top-right has some useful options, there’s a context menu on the workspace tab, and another one on the workspace background.
The burger menu includes Recover After Crash, which lets you get any workspace back if something bad happens (I hope it doesn’t).
The nip4 image view window has a lot of useful shortcuts.
If you select View > Display Control Bar, some widgets appear at the bottom. They let you flip pages, move to animation frames, set a scale and offset for each pixel (handy for scientific images), and a burger menu gives a set of useful visualisation options such as false colour, log scale, and colour management.

You can also mark features on images. If you hold down Ctrl and drag down and right, you’ll create a rectangular region, if you drag up and left you’ll mark an arrow, if you just Ctrl-click you’ll mark a point.
If you drag down and left you’ll mark a horizontal guide, if you drag up and right you’ll mark a vertical guide. Regions, arrows and points snap to guides, so they are handy for lining up groups of annotations.

All compound row objects can be edited. Back in the main window, try pressing the down arrow next to the region a few times. You should see something like this:

You can edit any of these member values. For example, try setting top to
be a fixed value, like 900. Now go back to the image window and try dragging
B8 — you’ll find it can only be dragged left-right, because the top edge
is now fixed in place.
You can use any formula you like. Try setting height to be the formula
width * 2 ** 0.5, that is, width times root two. Now you’ll find B8 can’t
be sized vertically, but if you resize horizontally, the height will also
change to keep the region in the “A” paper aspect ratio.
In that last example, three conflicting things were competing to set the
value of B8.
First, at the bottom of B8, you’ll see the original
equation, created when you initially dragged out the shape:
Region B7 1534 964 2880 2283
Next, as you moved the region with the mouse, the left, top, width and
height members were updated. Finally, you edited top by hand to fix a
value in place.
nip4 decides which of these ultimately sets the value of a row with two rules:
Graphical edits (eg. dragging a region, or moving a scale) override formula.
Inner edits override outer edits.
You can reset all changes to a row by right-clicking on a row and picking Reset from the menu. It will revert to the state of the top-most formula.
Most operations will work on rows of any type and will produce a result of the expected type.
For example, if you make a new column, select Widgets > Scale twice to make
two scale widgets, then select Math > Arithmetic > Add (or enter C1 + C2
in the formula box), you’ll get a third scale. Try dragging either of the top
two scales and the third will update.
If you open up the new scale widget, you’ll see that from and to have
been set appropriately for the possible range:

Just as with regions, you can also set the formula for any of these members.
Each tab can have a set of private definitions in nip4’s programming language. Right-click on the workspace background and select Workspace Definitions.
All the tools and toolkits are implemented in this language too. Right-click on the workspace background and select Edit Toolkits to change them. You can write your own tools and toolkits.
You can select many rows and group them. When you perform an operation on a group, it’ll automatically operate on every row in the group.
Once you’ve set up a pipeline, you can open the image view window of the first
image and use the < and > buttons in the titlebar to step though all the
other images in the same directory.
If you’ve not used nip2, there’s another post with a general introduction to nip4.
nip2 was mostly finished around 2000, with the final big feature going in in 2005 (I think). It still works fine, but it was becoming difficult to maintain since the major libraries it depends on (libvips-7 for image processing, gtk-2 for the user interface widgets, and goffice-0.8 for plotting) have long fallen out of maintenance.
It was also starting to look rather crude and old-fashioned! Something of a trip back in time, with brightly coloured elements and hard edges and corners everywhere. And nip2 is a “kitchen sink” program — it has lots of features which seemed like a good idea at the time, but which I never ended up using much.

nip4 is a rewrite of nip2 with the following aims:
can load and process all nip2 workspaces
add some missing language features
use the modern gtk-4 user-interface toolkit
a cleaner, simpler interface
use libvips-8 for image processing
use kplot for graphing
simple build and distribution process for Linux, macOS and Windows
And here is nip4, running the same very complex workspace:

One of the biggest changes is a completely new image view window. It understands most pyramidal image formats, it does smooth zooming, it runs on your GPU, it handles alpha, it supports animation and multipage images, and it should be a lot quicker.
The big changes from a user-interface point of view are:
Most other things are the same, so number keys for zoom levels, i and o for zoom in and out, Ctrl-drag to mark regions, cursor keys to pan, and so on.
There are two new region create gestures: hold down Ctrl and drag down and left to great a horizontal guide, and drag up and right to make a vertical guide. Regions, arrows and points snap to guides, so they are handy for lining up groups of annotations.
The big change in the main window is the new toolkit bar down the left, replacing the old Toolkits menu.

It slides left and right as you select items or go back, and it stays open after clicking on a tool, so you can use several related tools together, which can be convenient.
It also supports keyword search. Click on the magnifying glass at the top-left, then type something related to what you want. Entering “lab”, for example, will show all the tools related to CIELAB colourspace, for example:

Searching is fuzzy, so you don’t need to be exact.
The main workspace view has fancy animations for column and row dragging, which can help make it easier to see what’s going on.
You now need to hold down Ctrl before dragging a row, to make it harder to make accidental changes. You can now (finally!) drag rows between columns.
You can also left-drag on the workspace background to pan, phew.
In nip2 there was a right-click menu with an Edit option on most objects. You couldf select this to edit basic object properties.
This is gone in nip4, you’re supposed to change properties by opening the row a few times and editing the members:

nip4 has a much better recover after crash system. Select Recover after crash … from the main burger menu and you see a table of recent workspace auto-backups, sorted by the process ID and time. Doubleclick one of them to restore it.

The graphing window has been rewritten and now uses kplot.

It no longer supports bar plots, but everything else works, and it’s very fast.
You can now range-select rows, then right-click on a row name and act on them all. This is useful for deleting or duplicating sets of rows.
If you right-click on the workspace background, the workspace menu includes Workspace definitions and Edit toolkits ….
The toolkit editor is a bit basic, but does work.
The paintbox is probably the biggest missing feature, this might come back.
The github repository has a large TODO file with notes on bugs, ideas and other missing features.
]]>Here’s the per-voxel Patlak workspace I made for analyzing pulmonary FDG-PET scans:

The About nip4 menu item shows some stats:

So this workspace has:
top, it’s running in about 2.5 GB of ramHere’s the Charisma workspace:

This thing is for museum imaging: you give it images taken under visible, infrared, and UV light with various filters and it aligns them and computes a range of useful derivatives, such as Kubelka–Munk-modelled UV-induced visible fluorescence with stray visible light removal.
Looking at About nip4 for this one, it’s only 300 rows, but has
14,000 images and over 120 GB of image data. It runs in about 1.2 GB of ram,
according to top.
I’ve implemented some other features:
nip4 saves your work after every change, and keeps the last 10 saves from each workspace. These save files are normally deleted automatically on exit, but if there’s a crash (sadly inevitable in alpha software) they’ll still be there when it restarts. Click on Recover after crash and you get this window:

You can see the saves from each nip4 workspace and select one to recover it. The Delete all backups button wipes the temp area if it’s getting too big.
You can now drag-drop and copy-paste images and workspaces from your desktop and file manager. This is very handy for things like screen snapshots. You can even set nip4 as the image handler for your desktop (!!!).
Each tab in each workspace can have private definitions. Right click on the workspace background and select Workspace definitions and a panel opens on the right with all the local definitions. You can edit them and press the Play button to process your changes and update everything.

Right-click on the workspace background, pick Edit toolkits and you get the programming window:

You can edit and compile any of the built-in toolkits, though it’s a bit barebones for now.
nip4 now tracks which thumbnails are visible and starts and stops rendering as you move around a workspace. This saves a lot of memory and CPU!
]]>We were using goffice to render plots, but sadly goffice is still gtk3, with no progress towards a port. I did a bit of hacking and got it working with gtk4, but I don’t think that’s a very maintainable way forward. Instead, I’ve switched to kplot:
https://github.com/kristapsdz/kplot
This is a very simple library, but it can do everything nip4 needs, more or less. I made a fork and added better axis labeling and picking.
I’ve also mostly finished the new toolkit browser. I found the nip2 Toolkit menu difficult to use, so it’s now a scrolling bar down the left of the window. It also supports searching, so you can find things by name as well as by category.
There are some obvious missing features, and it’s still a bit crashy, but it’s now just about possible to use nip4 for work. I’ll do a bit more polishing, then try to make a first alpha release.
]]>nip2 was written between about 1997 and 2003 and depends on a lot of frameworks from the period. These are becoming extremely elderly now, and nip2 is getting difficult to maintain. It’s also looking increasingly old-fashioned.

nip4 is hoping to fix this. It has the following goals:
Update from the gtk2 GUI toolkit to gtk4, the current version. This
is a lot of work – the drawing model is completely different, and there
are many, many changes in best practice. As a test, I updated vipsdisp
to gtk4. The image display widget in
this viewer is the thing that will display images in nip4.
Switch to the vips8 API. We rewrote libvips to make vips8 between about 2010 and 2015, but nip2 was obviously stuck on the old vips7 API. nip4 has removed all the old vips7 code and redesigned for vips8. nip2 was the last large-scale user of vips7, as far as I know, so completing nip4 will let us build libvips with deprecated code removed by default.
Complete backwards compatibility. nip4 aims to be able to run the whole of the nip2 test suite unmodified.
Prettier, simpler, faster, modernised.
nip4 is now loading the first 5 test nip2 workspaces correctly. The big change from the UI point of view is full animation for interactions. Everything slides around as you work:
Next steps:
Get all the old nip2 test workspaces loading. This will involve implementing a few more workspace widgets. Plotting is probably the most work.
Finish drag/drop and copy/paste. It’s only half-there right now.
Add the toolkit menu and browser. This will probably be a bar down the left.
Perhaps implement a cut down and polished version of the programming and debugging interface from nip2.
UI for per-workspace definitions needs to go in.
The image processing menus are all currently designed around vips7. A new set of vips8 menus could be a useful simplification.
Do windows and mac builds. We have win and mac builds for vipsdisp done already.
And I think that would be enough for a release, hopefully in the first half of 2025.
]]>libvips has a new
vips_sdf()
operator. This can efficiently generate a range of basic shapes as Signed
Distance Fields – these are images where each pixel contains a signed value
giving the distance to the closest edge. For example:
$ vips sdf x.v 512 512 circle --r=200 --a="256 256"
Makes a 512 x 512 pixel float image of a circle with radius 200, centered on 256 x 256. As you move out and away from the edge, values become increasingly positive, as you move within the circle, values become negative.

The great thing about SDFs is that they are quick to make and very easy to combine to make more complex shapes. For example, you could write:
#!/usr/bin/env python3
import sys
import pyvips
box = pyvips.Image.sdf(1000, 1000, "rounded-box",
a=[300, 400],
b=[700, 600],
corners=[100, 0, 0, 0])
circle = pyvips.Image.sdf(1000, 1000, "circle",
a=[500, 300],
r=100)
line = pyvips.Image.sdf(1000, 1000, "line",
a=[500, 500],
b=[600, 900])
# union
sdf = box.minpair(circle).minpair(line)
# make annular
sdf = sdf.abs() - 15
# render as an antialiased image
sdf.clamp().linear(-255, 255, uchar=True).write_to_file(sys.argv[1])
To make:

Hmmm, possibly a person rowing a boat. This uses three
other new operators: vips_minpair() and vips_maxpair(),
which given a pair of images find the
pixel-wise max and min, and vips_clamp(), which constrains pixels
to a range.
SDFs fit really well with libvips on-demand-evaluation. These things never really exist, they are just chains of delayed computation, so you can make them any size, and compute them in parallel.
Up until now we’ve used SVG rendering to generate masks for large images. SDFs are a lot faster and need much less memory – as long as you only need simple shapes, they should be a great replacement.
File format support has been improved (again). Highlights this time are:
JXL load and save now supports exif, xmp, and animation.
webpsave now has target_size parameter to set desired size in bytes and a
passes parameter to set number of passes to achieve desired target size,
plus a smart_deblock option for better edge rendering.
tiffload supports old-style JPEG compression.
tiffsave now lets you change the deflate compression level.
All paletteised images now have a palette metadata item.
PFM load and save now uses scRGB colourspace (ie. linear 0-1).
rawsave gets streaming support with
vips_rawsave_target() and
vips_rawsave_buffer().
There have been some smaller libvips additions and improvements too.
libvips used to limit image dimensions to 10 million pixels. This is now
configurable, see vips_max_coord_get().
There’s a new (trivial) vips_addalpha() operation.
vips_getpoint() has a new unpack_complex option.
vipsheader supports multiple -f field arguments.
libvips now includes basic g_auto support, making C programming slightly
more convenient.
Plus some even more minor bugfixes and improvements.
]]>