add howitworks.md

This commit is contained in:
John Cupitt 2017-03-31 10:57:04 +01:00
parent 60e661614f
commit 30829ef003
13 changed files with 339 additions and 300 deletions

View File

@ -1,5 +1,6 @@
25/3/17 started 8.5.2
- better behaviour for truncated PNG files, thanks Yury
- move some docs from the wiki and blog into core libvips docs
25/3/17 started 8.5.1
- init more classes earlier, thanks David

1
doc/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
How-it-works.xml

316
doc/How-it-works.md Normal file
View File

@ -0,0 +1,316 @@
Compared to most image processing libraries, VIPS needs little RAM and runs
quickly, especially on machines with more than one CPU. VIPS achieves this
improvement by only keeping the pixels currently being processed in RAM
and by having an efficient, threaded image IO system. This page explains
how these features are implemented.
### Images
VIPS images have three dimensions: width, height and bands. Bands usually
(though not always) represent colour. These three dimensions can be any
size up to 2 ** 31 elements. Every band element in an image has to have the
same format. A format is an 8-, 16- or 32-bit int, signed or unsigned, 32-
or 64-bit float, and 64- or 128-bit complex.
### Regions
An image can be very large, much larger than the available memory, so you
can't just access pixels with a pointer \*.
Instead, you read pixels from an image with a region. This is a rectangular
sub-area of an image. In C, the API looks like:
```c
VipsImage *image = vips_image_new_from_file( filename, NULL );
VipsRegion *region = vips_region_new( image );
// ask for a 100x100 pixel region at 0x0 (top left)
VipsRect r = { left: 0, top: 0, width: 100, height: 100 };
if( vips_region_prepare( region, &r ) )
vips_error( ... );
// get a pointer to the pixel at x, y, where x, y must
// be within the region
// as long as you stay within the valid area for the region,
// you can address pixels with regular pointer arithmetic
// compile with -DDEBUG and the macro will check bounds for you
// add VIPS_REGION_LSKIP() to move down a line
VipsPel *pixel = VIPS_REGION_ADDR( region, x, y );
// you can call vips_region_prepare() many times
// everything in libvips is a GObject ... when you're done,
// just free with
g_object_unref( region );
```
The action that `vips_region_prepare()` takes varies with the type of
image. If the image is a file on disc, for example, then VIPS will arrange
for a section of the file to be read in.
(\* there is an image access mode where you can just use a pointer, but
it's rarely used)
### Partial images
A partial image is one where, instead of storing a value for each pixel, VIPS
stores a function which can make any rectangular area of pixels on demand.
If you use `vips_region_prepare()` on a region created on a partial image,
VIPS will allocate enough memory to hold the pixels you asked for and use
the stored function to calculate values for just those pixels \*.
The stored function comes in three parts: a start function, a generate
function and a stop function. The start function creates a state, the
generate function uses the state plus a requested area to calculate pixel
values and the stop function frees the state again. Breaking the stored
function into three parts is good for SMP scaling: resource allocation and
synchronisation mostly happens in start functions, so generate functions
can run without having to talk to each other.
VIPS makes a set of guarantees about parallelism that make this simple to
program. Start and stop functions are mutually exclusive and a state is
never used by more than one generate. In other words, a start / generate /
generate / stop sequence works like a thread.
![](Sequence.png)
(\* in fact VIPS keeps a cache of calculated pixel buffers and will return
a pointer to a previously-calculated buffer if it can)
### Operations
VIPS operations read input images and write output images, performing some
transformation on the pixels. When an operation writes to an image the
action it takes depends upon the image type. For example, if the image is a
file on disc then VIPS will start a data sink to stream pixels to the file,
or if the image is a partial one then it will just attach start / generate /
stop functions.
Like most threaded image processing systems, all VIPS operations have to
be free of side-effects. In other words, operations cannot modify images,
they can only create new images. This could result in a lot of copying if
an operation is only making a small change to a large image so VIPS has a
set of mechanisms to copy image areas by just adjusting pointers. Most of
the time no actual copying is necessary and you can perform operations on
large images at low cost.
### Run-time code generation
VIPS uses [Orc](http://code.entropywave.com/orc/), a run-time compiler, to
generate code for some operations. For example, to compute a convolution
on an 8-bit image, VIPS will examine the convolution matrix and the source
image and generate a tiny program to calculate the convolution. This program
is then "compiled" to the vector instruction set for your CPU, for example
SSE3 on most x86 processors.
Run-time vector code generation typically speeds operations up by a factor
of three or four.
### Joining operations together
The region create / prepare / prepare / free calls you use to get pixels
from an image are an exact parallel to the start / generate / generate /
stop calls that images use to create pixels. In fact, they are the same:
a region on a partial image holds the state created by that image for the
generate function that will fill the region with pixels.
![](Combine.png)
VIPS joins image processing operations together by linking the output of one
operation (the start / generate / stop sequence) to the input of the next
(the region it uses to get pixels for processing). This link is a single
function call, and very fast. Additionally, because of the the split between
allocation and processing, once a pipeline of operations has been set up,
VIPS is able to run without allocating and freeing memory.
This graph (generated by `vipsprofile`, the vips profiler) shows memory use
over time for a vips pipeline running on a large image. The bottom trace
shows total memory, the upper traces show threads calculating useful results
(green), threads blocked on synchronisation (red) and memory allocations
(white ticks).
![](Memtrace.png)
Because the intermediate image is just a small region in memory, a pipeline
of operations running together needs very little RAM. In fact, intermediates
are small enough that they can fit in L2 cache on most machines, so an
entire pipeline can run without touching main memory. And finally, because
each thread runs a very cheap copy of just the writeable state of the
entire pipeline, threads can run with few locks. VIPS needs just four lock
operations per output tile, regardless of the pipeline length or complexity.
### Data sources
VIPS has data sources which can supply pixels for processing from a variety
of sources. VIPS can stream images from files in VIPS native format, from
tiled TIFF files, from binary PPM/PGM/PBM/PFM, from Radiance (HDR) files,
from FITS images and from tiled OpenEXR images. VIPS will automatically
unpack other formats to temporary disc files for you but this can
obviously generate a lot of disc traffic. It also has a special
sequential mode for streaming operations on non-random-access
formats. A post on the libvips blog [explains how libvips opens a
file](http://libvips.blogspot.co.uk/2012/06/how-libvips-opens-file.html). One
of the sources uses the [ImageMagick](http://www.imagemagick.org) (or
optionally [GraphicsMagick](http://www.graphicsmagick.org)) library, so
VIPS can read any image format that these libraries can read.
VIPS images are held on disc as a 64-byte header containing basic image
information like width, height, bands and format, then the image data as
a single large block of pixels, left-to-right and top-to-bottom, then an
XML extension block holding all the image metadata, such as ICC profiles
and EXIF blocks.
When reading from a large VIPS image (or any other format with the same
structure on disc, such as binary PPM), VIPS keeps a set of small rolling
windows into the file, some small number of scanlines in size. As pixels
are demanded by different threads VIPS will move these windows up and down
the file. As a result, VIPS can process images much larger than RAM, even
on 32-bit machines.
### Data sinks
In a demand-driven system, something has to do the demanding. VIPS has a
variety of data sinks that you can use to pull image data though a pipeline
in various situations. There are sinks that will build a complete image
in memory, sinks to draw to a display, sinks to loop over an image (useful
for statistical operations, for example) and sinks to stream an image to disc.
The disc sink looks something like this:
![](Sink.png)
The sink keeps two buffers\*, each as wide as the image. It starts threads
as rapidly as it can up to the concurrency limit, filling each buffer with
tiles\*\* of calculated pixels, each thread calculating one tile at once. A
separate background thread watches each buffer and, as soon as the last tile
in a buffer finishes, writes that complete set of scanlines to disc using
whatever image write library is appropriate. VIPS can write with libjpeg,
libtiff, libpng and others. It then wipes the buffer and repositions it
further down the image, ready for the next set of tiles to stream in.
These features in combination mean that, once a pipeline of image processing
operations has been built, VIPS can run almost lock-free. This is very
important for SMP scaling: you don't want the synchronization overhead to
scale with either the number of threads or the complexity of the pipeline
of operations being performed. As a result, VIPS scales almost linearly
with increasing numbers of threads:
![](Vips-smp.png)
Number of CPUs is on the horizontal axis, speedup is on the vertical
axis. Taken from the [[Benchmarks]] page.
(\* there can actually be more than one, it allocate enough buffers to
ensure that there are at least two tiles for every thread)
(\*\* tiles can be any shape and size, VIPS has a tile hint system that
operations use to tell sinks what tile geometry they prefer)
### Operation cache
Because VIPS operations are free of side-effects\*, you can cache them. Every
time you call an operation, VIPS searches the cache for a previous call to
the same operation with the same arguments. If it finds a match, you get
the previous result again. This can give a huge speedup.
By default, VIPS caches the last 1,000 operation calls. You can also control
the cache size by memory use or by files opened.
(\* Some vips operations DO have side effects, for example,
`vips_draw_circle()` will draw a circle on an image. These operations emit an
"invalidate" signal on the image they are called on and this signal makes
all downstream operations and caches drop their contents.)
### Operation database and APIs
VIPS has around 300 image processing operations written in this style. Each
operation is a GObject class. You can use the standard GObject calls to walk
the class hierarchy and discover operations, and libvips adds a small amount
of extra introspection metadata to handle things like optional arguments.
The [C API](using-from-c.html) is a set of simple wrappers which create
class instances for you. The [C++ API](using-from-cpp.html) is a little
fancier and adds things like automatic object lifetime management. The
[command-line interface](using-cli.html) uses introspection to run any vips
operation in the class hierarchy.
The [Python API](using-from-python.html) is built on top of
gobject-introspection. It is written in Python, so as long as you can get
gobject-introspection working, you should be able to use vips. It supports
python2 and python3 and works on Linux, OS X and Windows.
### Snip
The VIPS GUI, nip2, has its own scripting language called Snip. Snip is a
lazy, higher-order, purely functional, object oriented language. Almost all
of nip2's menus are implemented in it, and nip2 workspaces are Snip programs.
VIPS operations listed in the operation database appear as Snip functions. For
example, `abs` can be used from Snip as:
```
// absolute value of image b
a = vips_call "abs" [b] [];
```
However, `abs` won't work on anything except the primitive vips image type. It
can't be used on any class, or list or number. Definitions in `_stdenv.dev`
wrap each VIPS operation as a higher level Snip operation. For example:
```
abs x
= oo_unary_function abs_op x, is_class x
= vips_call "abs" [x] [], is_image x
= abs_cmplx x, is_complex x
= abs_num x, is_real x
= abs_list x, is_real_list x
= abs_list (map abs_list x), is_matrix x
= error (_ "bad arguments to " ++ "abs")
{
abs_op = Operator "abs" abs Operator_type.COMPOUND false;
abs_list l = (sum (map square l)) ** 0.5;
abs_num n
= n, n >= 0
= -n;
abs_cmplx c = ((re c)**2 + (im c)**2) ** 0.5;
}
```
This defines the behaviour of `abs` for the base Snip types (number, list,
matrix, image and so on), then classes will use that to define operator
behaviour on higher-level objects.
Now you can use:
```
// absolute value of anything
a = abs b;
```
and you ought to get sane behaviour for any object, including things like
the `Matrix` class.
You can write Snip classes which present functions to the user as menu
items. For example, `Math.def` has this:
```
Math_arithmetic_item = class
Menupullright "_Arithmetic" "basic arithmetic for objects" {
Absolute_value_item = class
Menuaction "A_bsolute Value" "absolute value of x" {
action x = map_unary abs x;
}
}
```
Now the user can select an object and click `Math / Abs` to find the absolute
value of that object.

View File

@ -126,13 +126,26 @@ IGNORE_HFILES = $(IGNORE_VIPS_INCLUDE) $(IGNORE_VIPS_C)
# Images to copy into HTML directory.
# e.g. HTML_IMAGES=$(top_srcdir)/gtk/stock-icons/stock_about_24.png
HTML_IMAGES = \
$(top_srcdir)/doc/images/interconvert.png
$(top_srcdir)/doc/images/interconvert.png \
$(top_srcdir)/doc/images/Combine.png \
$(top_srcdir)/doc/images/Memtrace.png \
$(top_srcdir)/doc/images/Sequence.png \
$(top_srcdir)/doc/images/Sink.png \
$(top_srcdir)/doc/images/Vips-smp.png
# we have some files in markdown ... convert to docbook for gtk-doc
# pandoc makes sect1 headers, we want refsect3 for gtk-doc
.md.xml:
pandoc -s -S --template="$(realpath pandoc-docbook-template.docbook)" --wrap=none -V title="$<" -f markdown -t docbook -o $@ $<
sed -e s/sect1/refsect3/g < $@ > x && mv x $@
# Our markdown source files
markdown_content_files = \
How-it-works.md
# converted to xml in this dir by pandoc
markdown_content_files_docbook = $(markdown_content_files:.md=.xml)
# Extra SGML files that are included by $(DOC_MAIN_SGML_FILE).
# e.g. content_files=running.sgml building.sgml changes-2.0.sgml
content_files = \
@ -144,7 +157,7 @@ content_files = \
extending.xml \
function-list.xml \
file-format.xml \
whatsnew-8.5.xml \
${markdown_content_files_docbook} \
binding.xml
# SGML files where gtk-doc abbrevations (#GtkWidget) are expanded
@ -159,7 +172,7 @@ expand_content_files = \
extending.xml \
function-list.xml \
file-format.xml \
whatsnew-8.5.xml \
${markdown_content_files_docbook} \
binding.xml
# CFLAGS and LDFLAGS for compiling gtkdoc-scangobj with your library.
@ -176,13 +189,14 @@ include gtk-doc.make
# Other files to distribute
# e.g. EXTRA_DIST += version.xml.in
EXTRA_DIST += \
${markdown_content_files} \
images \
gen-function-list.py
# Files not to distribute
# for --rebuild-types in $(SCAN_OPTIONS), e.g. $(DOC_MODULE).types
# for --rebuild-sections in $(SCAN_OPTIONS) e.g. $(DOC_MODULE)-sections.txt
DISTCLEANFILES = libvips.types
DISTCLEANFILES = libvips.types
# Comment this out if you want 'make check' to test you doc status
# and run some sanity checks

View File

@ -1,109 +0,0 @@
#!/bin/sh
#
usage() {
cat <<EOF
gtkdoc-mkhtml version 1.25 - generate documentation in html format
--verbose Print extra output while processing
--path=SEARCH_PATH Extra source directories
MODULE Name of the doc module being parsed
DRIVER_FILE File containing the toplevel DocBook file.
--version Print the version of this program
--help Print this help
EOF
}
# echo "args $*\n";
# parse options, ignore unknown options for future extensions
verbose="0"
searchpath=
uninstalled=no
while true; do
case "X$1" in
X--version) echo "1.25"; exit 0;;
X--help) usage; exit 0;;
X--uninstalled) uninstalled=yes; shift;;
X--verbose) verbose="1"; shift;;
X--path=*) searchpath=`echo "$1" | sed s/.*=//`; shift;;
X--*) shift;;
X*) break;;
esac
done
if test $# -lt 2; then
usage 1>&2
exit 1
fi
module="$1"
shift
document="$1"
shift
quiet="1"
if test $verbose = "1"; then
quiet="0"
fi
if test $uninstalled = yes; then
# this does not work from buiddir!=srcdir
gtkdocdir=`dirname $0`
# traditional Bourne shells may not support -e here, use -f
if test ! -f $gtkdocdir/gtk-doc.xsl; then
# try to src dir (set from makefiles) too
if test -f $ABS_TOP_SRCDIR/gtk-doc.xsl; then
gtkdocdir=$ABS_TOP_SRCDIR
fi
fi
styledir=$gtkdocdir/style
#echo "uninstalled, gtkdocdir=$gtkdocdir, cwd=$PWD"
else
# the first two are needed to resolve datadir
prefix=/usr
datarootdir=${prefix}/share
gtkdocdir=${datarootdir}/gtk-doc/data
styledir=$gtkdocdir
fi
# we could do "$path_option $PWD "
# to avoid needing rewriting entities that are copied from the header
# into docs under xml
if test "X$searchpath" = "X"; then
path_arg=
else
path_arg="--path $searchpath"
fi
# profiling
profile_args=""
if test "$GTKDOC_PROFILE" != ""; then
profile_args="--profile"
fi
#echo /usr/bin/xsltproc $path_arg --nonet --xinclude \
# --stringparam gtkdoc.bookname $module \
# --stringparam gtkdoc.version "1.25" \
# "$@" $gtkdocdir/gtk-doc.xsl "$document"
/usr/bin/xsltproc 2>profile.txt $profile_args $path_arg --nonet --xinclude \
--stringparam gtkdoc.bookname $module \
--stringparam gtkdoc.version "1.25" \
--stringparam chunk.quietly $quiet \
--stringparam chunker.output.quiet $quiet \
"$@" $gtkdocdir/gtk-doc.xsl "$document" || exit $?
# profiling
if test "$GTKDOC_PROFILE" != ""; then
cat profile.txt | gprof2dot.py -e 0.01 -n 0.01 | dot -Tpng -o profile.png
else
rm profile.txt
fi
# copy navigation images and stylesheets to html directory ...
cp -f $styledir/*.png $styledir/*.css ./
echo "timestamp" > ../html.stamp

BIN
doc/images/Combine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
doc/images/Memtrace.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
doc/images/Sequence.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
doc/images/Sink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
doc/images/Vips-smp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -39,7 +39,7 @@
<xi:include href="xml/function-list.xml"/>
<xi:include href="xml/file-format.xml"/>
<xi:include href="xml/using-threads.xml"/>
<xi:include href="xml/whatsnew-8.5.xml"/>
<xi:include href="xml/How-it-works.xml"/>
</chapter>
<chapter>

View File

@ -11,11 +11,11 @@ $endif$
<refmeta>
<refentrytitle>$title$</refentrytitle>
<manvolnum>3</manvolnum>
<refmiscinfo>VIPS Library</refmiscinfo>
<refmiscinfo>libvips</refmiscinfo>
</refmeta>
<refnamediv>
<refname>VIPS</refname>
<refname>libvips</refname>
<refpurpose>$title$</refpurpose>
</refnamediv>

View File

@ -1,184 +0,0 @@
libvips 8.5 should be out by the end of March 2017. This page introduces the
main features.
## New operators
Almost all of the logic from the `vipsthumbnail` program is now in a pair of
new operators, `vips_thumbnail()` and `vips_thumbnail_buffer()`. These are very
handy for the various scripting languages with vips bindings: you can now make
a high-quality, high-speed thumbnail in PHP (for example) with just:
```php
$filename = ...;
$image = Vips\Image::thumbnail($filename, 200, ["height" => 200]);
$image.writeToFile("my-thumbnail.jpg");
```
The new thumbnail operator has also picked up some useful features:
* **Smart crop** A new cropping mode called `attention` searches the image for
edges, skin tones and areas of saturated colour, and attempts to position the
crop box over the most significant feature. There's a `vips_smartcrop()`
operator as well.
* **Crop constraints** Thanks to tomasc, libvips has crop constraints. You
can set it to only thumbnail if the image is larger or smaller than the target
(the `<` and `>` modifiers in imagemagick), and to crop to a width or height.
* **Buffer sources** `vips_thumbnail_buffer()` will thumbnail an image held as
a formatted block of data in memory. This is useful for cloud services, where
the filesystem is often rather slow.
CLAHE, or Contrast-Limited Adaptive Histogram Equalisation, is a simple way to
make local histogram equalisation more useful.
Plain local equalization removes
all global brightness variation and can make images hard to understand.
The `hist_local` operator now has a `max-slope` parameter you can use to limit
how much equalisation can alter your image. A value of 3 generally works well.
## Toilet roll images
libvips used to let you pick single pages out of multi-page images, such
as PDFs, but had little support for processing entire documents.
libvips 8.5 now has good support for toilet roll images. You can load a
multipage image as a very tall, thin strip, process the whole thing, and write
back to another multi-page file. The extra feature is an `n` parameter which
gives the number of pages to load, or -1 to load all pages.
For example, (OME-
TIFF)[https://www.openmicroscopy.org/site/support/ome-model/ome-tiff]
is a standard for microscopy data that stores volumetric images as multi-page
TIFFs. They have some (sample
data)[https://www.openmicroscopy.org/site/support/ome-model/ome-tiff/data.html]
including a 4D image of an embryo.
Each TIFF contains 10 slices. Normally you just see page 0:
```
$ vipsheader tubhiswt_C0_TP13.ome.tif
tubhiswt_C0_TP13.ome.tif: 512x512 uchar, 1 band, b-w, tiffload
```
Use `n=-1` and you see all the pages as a very tall strip:
```
$ vipsheader tubhiswt_C0_TP13.ome.tif[n=-1]
tubhiswt_C0_TP13.ome.tif: 512x5120 uchar, 1 band, b-w, tiffload
```
You can work with PDF, TIFF, GIF and all imagemagick-supported formats in
this way.
You can write this tall strip to another file, and it will be broken up into
pages:
```
$ vips copy tubhiswt_C0_TP13.ome.tif[n=-1] x.tif
$ vipsheader x.tif
x.tif: 512x512 uchar, 1 band, b-w, tiffload
$ vipsheader x.tif[n=-1]
x.tif: 512x5120 uchar, 1 band, b-w, tiffload
```
The extra magic is a `page-height` property that images carry around that says
how long each sheet of toilet paper is.
There are clearly some restrictions with this style of multi-page document
handling: all pages must have identical width, height and colour depth; and image
processing operators have no idea they are dealing with a multi-page document,
so if you do something like `resize`, you'll need to update `page-height`.
You'll also need to be careful about edge effects if you're using spatial
filters.
## Computation reordering
Thanks to the developer of
(PhotoFlow)[https://github.com/aferrero2707/PhotoFlow], a non-destructive image
editor with a libvips backend, libvips can now reorder computations to reduce
recalculation. This can (sometimes) produce a dramatic speedup.
This has been (discussed on the libvips
blog)[http://libvips.blogspot.co.uk/2017/01/automatic-computation-reordering.html],
but briefly, the order in which operator arguments are evaluated can have a
big effect on runtime due to the way libvips tries to cache and reuse results
behind the scenes.
The blog post has some examples and some graphs.
## New sequential mode
libvips sequential mode has been around for a while. This is the thing libvips
uses to stream pixels through your computer, from input file to output file,
without having to have the whole image in memory all at the same time. When it
works, it give a nice performance boost and a large drop in memory use.
There are some more complex cases where it didn't work. Consider this Python
program:
```python
#!/usr/bin/python
import sys
import random
import gi
gi.require_version('Vips', '8.0')
from gi.repository import Vips
composite = Vips.Image.black(10000, 10000)
for filename in sys.argv[2:]:
tile = Vips.Image.new_from_file(filename, access = Vips.Access.SEQUENTIAL)
x = random.randint(0, composite.width - tile.width)
y = random.randint(0, composite.height - tile.height)
composite = composite.insert(tile, x, y)
composite.write_to_file(sys.argv[1])
```
It makes a large 10,000 x 10,000 pixel image, then inserts all of the images
you list at random positions, then writes the result.
You'd think this could work with sequential mode, but sadly with earlier
libvipses it will sometimes fail. The problem is that images can cover each
other, so while writing, libvips can discover that it only needs the bottom few
pixels of one of the input images. The image loaders used to track the current
read position, and if a request came in for some pixels way down the image,
they'd assume one of the evaluation threads had run ahead of the rest and
needed to be stalled. Once stalled, it was only restarted on a long timeout,
causing performance to drop through the floor.
libvips 8.5 has a new implementation of sequential mode that changes the way
threads are kept together as images are processed. Rather than trying to add
constraints to load operations, instead it puts the constraints into operations
that can cause threads to become spread out, such as vertical shrink.
As a result of this change, many more things can run in sequential mode, and
out of order reads should be impossible.
## `libxml2` swapped out for `expat`
libvips has used libxml2 as its XML parser since dinosaurs roamed the Earth.
Now libvips is based on gobject, the XML parser selected by glib, expat, makes
more sense, since it will already be linked.
It's nice to be able to remove a required dependency for a change.
## File format support
As usual, there are a range of improvements to file format read and write.
* Thanks to a push from Felix Bünemann, TIFF now supports load and save to and
from memory buffers.
* `dzsave` can write to memory (as a zip file) as well.
* Again, thanks to pushing from Felix, libvips now supports ICC, XMP and IPCT
metadata for WebP images.
* FITS images support `bzero` and `bscale`.
* `tiffload` memory use is now much lower for images with large strips.
## Other
Many small bug fixes, improvements to the C++ binding. As usual, the
ChangeLog has more detail, if you're interested.