Image
Image is a fast, memory-efficient image processing library for Elixir. It is a high-level wrapper around Vix, the Elixir bindings for the libvips C library, and provides an idiomatic functional API for image manipulation, drawing, text rendering, EXIF/XMP metadata, video frame extraction (via Xav/FFmpeg), QR code encoding and decoding (via eVision), blurhash, perceptual hashing, and many other image-related operations.
Machine-learning features (object detection, image classification, image generation) live in the companion image_detection library, which depends on :image and pulls in Bumblebee and Nx as its own optional dependencies.
In a simple resize benchmark, Image is approximately 2 to 3 times faster than Mogrify and uses about 5 times less memory.
Documentation can be found at https://hexdocs.pm/image.
Features
Image processing — open, write, resize, thumbnail, crop, embed, rotate, flip, flatten, trim, replace colour, chroma key, warp perspective, distort, blur (Gaussian, box, bilateral), sharpen, modulate, vibrance, tone map, local contrast, equalize, blend, composite, mask, dilate / erode, edge detect.
Drawing —
Image.Drawprovides points, rectangles, circles, lines, masks, flood fill, image overlay, smudge.Text rendering —
Image.Textproduces antialiased text overlays with full Pango markup support, font selection, alignment, background fills, stroke, and per-character control.Colour management — colour arguments accept atoms, hex strings, CSS named colours, hex shorthand,
#RRGGBBAA,Color.*structs, CSS Color 4 / 5 functions (rgb(),hsl(),lab(),oklch(),color-mix(), relative colour syntax viafrom,nonekeyword,calc()), and are converted to the target image's interpretation viaImage.Pixel.to_pixel/3. The same colour string draws correctly on sRGB, Lab, scRGB, CMYK, 16-bit, and greyscale images.Colour spaces —
Image.colorspace/1,Image.to_colorspace/2, and full conversion between sRGB / scRGB / Lab / LCh / CMYK / HSV / XYZ / B&W / 16-bit RGB.Dominant colour and palette extraction —
Image.dominant_color/2with two methods: a fast 3D-histogram (default) and an imagequant-backed perceptual quantiser. Seeguides/performance.mdfor benchmarks.K-means clustering —
Image.k_means/2(when:scholaris available) returns the dominant colour palette extracted by unsupervised clustering.Histogram operations —
Image.histogram/1,Image.equalize/2, per-band statistics, percentile, mean, median.Metadata —
Image.exif/1for EXIF,Image.Xmp.extract_xmp/1for XMP, plusImage.minimize_metadata/1to strip metadata while retaining the artist and copyright fields.ICC colour profiles —
Image.ICCProfilefor libvips' built-in profiles (:srgb,:cmyk,:p3) and arbitrary.iccfiles.Image streaming — open and write directly from
File.Streams, PlugConns, in-memory binaries, and S3 sources.Optional ML integrations — each is compiled only when its optional dependency is present:
Image.Video(frame extraction, seek, webcam) via Xav, an Elixir wrapper around FFmpeg. Requires FFmpeg ≥ 6.0 on the system.Image.QRcode(encode + decode) via eVision.Image.k_meansvia Scholar.Image.to_nx/2/Image.from_nx/1via Nx.Object detection, image classification, and image generation live in the separate
:image_detectionpackage. Add it alongside:imagein yourmix.exsto getImage.Detection,Image.Classification, andImage.Generation(which depend on:axon_onnxand Bumblebee respectively).
Hashing — perceptual difference hash (
Image.dhash/2), blurhash encode/decode (Image.Blurhash), Hamming distance.YUV interop —
Image.YUVfor raw YUV file/binary I/O in C420/C422/C444 chroma subsampling and BT.601/BT.709 colour spaces.Kino integration —
Image.Kinorenders images in Livebook without manual conversion.Social media presets —
Image.Socialwith the standard image sizes for Twitter, Facebook, Instagram, LinkedIn, Pinterest, YouTube, Snapchat, and TikTok.Bundled fonts — ships the Impact font for meme rendering so
Image.meme/3works out of the box.Structured errors — every fallible function returns
{:ok, value}or{:error, %Image.Error{}}. The error struct carries:reason(atom or{atom, value}),:operation,:path,:value, and a derived:message. Bang variants raise the same struct.
Supported Elixir and OTP releases
Image is tested and supported on the following matrix:
| Elixir | OTP |
|---|---|
| 1.17 | 26, 27 |
| 1.18 | 26, 27 |
| 1.19 | 26, 27, 28 |
| 1.20-rc | 27, 28 |
Quick start
Add :image to your dependencies:
def deps do
[
{:image, "~> 0.64"}
]
endlibvips is bundled by default via :vix, so you don't need to
install it system-wide. See the "Installing Libvips" section below
if you want to bring your own libvips for additional format
support.
Open, transform, write
{:ok, image} = Image.open("photo.jpg")
{:ok, thumb} = Image.thumbnail(image, 256)
:ok = Image.write(thumb, "thumb.jpg", quality: 85)Resize, crop, rotate
image
|> Image.resize!(scale: 0.5)
|> Image.crop!(0, 0, 400, 400)
|> Image.rotate!(15)
|> Image.write!("derived.png")Compose and draw
{:ok, base} = Image.new(800, 600, color: :white)
{:ok, with_circle} = Image.Draw.circle(base, 400, 300, 100, color: "#ff0000")
{:ok, with_text} = Image.Text.text("Hello world", font_size: 64)
{:ok, composed} = Image.compose(with_circle, with_text, x: :center, y: :middle)Colour-aware operations
Colour arguments work in any colour space:
# Draws actual Lab red, not [255, 0, 0] reinterpreted as Lab
{:ok, lab_image} = Image.to_colorspace(image, :lab)
{:ok, _} = Image.Draw.rect(lab_image, 0, 0, 100, 100, color: :red)
# CSS Color 5 syntax everywhere
{:ok, _} = Image.Draw.rect(image, 0, 0, 100, 100,
color: "color-mix(in oklch, red 40%, blue)")
# Relative colour syntax
{:ok, _} = Image.Draw.circle(image, 50, 50, 25,
color: "oklch(from teal calc(l + 0.1) c h)")Dominant colour
{:ok, [r, g, b]} = Image.dominant_color(image)
{:ok, palette} = Image.dominant_color(image, method: :imagequant, top_n: 8)
# => [{124, 30, 4}, {200, 88, 12}, ...]EXIF metadata
{:ok, image} = Image.open("photo.jpg")
{:ok, exif} = Image.exif(image)
exif[:make]
# => "FUJIFILM"Streaming
"photo.jpg"
|> File.stream!([], 64_000)
|> Image.open!()
|> Image.thumbnail!(256)
|> Image.write!(File.stream!("thumb.jpg"))QR codes
{:ok, qrcode} = Image.QRcode.encode("Hello world", size: 256)
{:ok, "Hello world"} = Image.QRcode.decode(qrcode)
(Requires the optional :evision dependency.)
Pattern-matching errors
case Image.open(path) do
{:ok, image} -> use_image(image)
{:error, %Image.Error{reason: :enoent}} -> not_found(path)
{:error, %Image.Error{reason: :unsupported_format}} -> wrong_format(path)
{:error, %Image.Error{} = error} -> raise error
endInstalling Libvips
Starting from Vix v0.16.0, libvips can be either bundled
(default) or platform-provided. The default uses precompiled NIF
binaries built from the sharp-libvips
project — no system dependencies required, ideal for Livebook and
Heroku-style deploys.
For additional format support (HEIF compression options, JPEG XL, specialised codecs) you can use the platform's libvips:
# macOS
brew install libvips
# Debian / Ubuntu
apt install libvips-dev
# Fedora / RHEL
dnf install vips-devel
Then set VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS at compile
time and at runtime. See the Vix documentation
for the full list.
Installing FFmpeg (for Image.Video)
Image.Video is powered by Xav, which wraps the FFmpeg C libraries as a NIF. FFmpeg itself is not bundled — you need to install the FFmpeg development packages (version 4.x – 7.x) on the system where :image is compiled and where it runs.
Image.Video and the :xav optional dependency only compile when these libraries are present. Projects that don't use video don't need to install anything here.
# macOS (Apple Silicon)
brew install pkg-config ffmpeg
# macOS (Intel)
brew install ffmpeg
# Debian / Ubuntu
apt install libavcodec-dev libavformat-dev libavutil-dev \
libswscale-dev libavdevice-dev
# Fedora / RHEL
dnf install pkg-config ffmpeg-devel ffmpeg-libs
Note: Fedora's default repositories don't ship FFmpeg. Enable
RPM Fusion first with
dnf install https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
before installing ffmpeg-devel.
Windows is not currently supported by Xav. See the Xav installation guide for the upstream source of these commands and any updates.
Optional dependencies
Image is small and self-contained at its core. The following
optional dependencies enable specific features:
| Dependency | Enables |
|---|---|
:nx | Image.to_nx/2, Image.from_nx/1, tensor interop |
:scholar | Image.k_means/2 |
:xav | Image.Video (FFmpeg-backed frame extraction) |
:evision | Image.QRcode, Image.to_evision/2, Image.from_evision/1 |
:image_detection | Image.Detection, Image.Classification, Image.Generation (object detection, classification, image generation — pulls Bumblebee, Nx, Axon as its own transitive deps) |
:plug |
streaming via Plug.Conn |
:req | streaming over HTTP |
:kino | Image.Kino (Livebook integration) |
Each is detected at compile time; the corresponding Image module
is conditionally compiled. Add only the deps you actually use.
Configuring libvips
libvips exposes several environment variables that control
debugging, concurrency, memory leak detection, and security. Each
has a sensible default; the most commonly tuned ones:
VIPS_BLOCK_UNTRUSTED=TRUE(set automatically when the:imageapplication starts) prevents libvips from loading untrusted format loaders.VIPS_CONCURRENCY=Ncaps the libvips thread pool. Default is the system core count. Lower it if image processing is competing with other workloads.VIPS_LEAK=trueenables libvips' memory leak reporter.G_DEBUG=fatal-criticalsaborts on the first GLib critical.
You can also set the concurrency programmatically with
Image.put_concurrency/1 and read it back with
Image.get_concurrency/0.
FFmpeg / Xav log noise
If you use Image.Video (which is backed by
Xav / FFmpeg) you may see lines like
[swscaler @ 0x1490a0000] No accelerated colorspace conversion found from yuv420p to rgb24.
written to stderr during frame decoding. These are
informational notices from FFmpeg's libswscale, not
errors. They mean that libswscale does not have a
hand-optimised SIMD path for that particular pixel-format
conversion on your CPU, so it is using its generic C fallback.
Decoded frames are bit-for-bit correct either way.
The messages come from FFmpeg writing directly to stderr at its
default log level (AV_LOG_INFO). Xav does not currently expose
av_log_set_level/1, so the only way to silence them from
application code is to install an FFmpeg build that has the
SIMD path for your architecture (typically an FFmpeg compiled
with --enable-runtime-cpudetect and any of --enable-asm,
--enable-x86asm, or platform ASM flags — most distribution
packages already do this). On Apple Silicon the arm64 optimised
path for yuv420p → rgb24 is not in FFmpeg's swscale as of
FFmpeg 7.x, which is why macOS users on M-series machines see
the notice most often.
If the noise is disruptive during tests or automation, you can
redirect stderr for the command in question, e.g.
mix test 2> /dev/null. Do not do this for production —
suppressing stderr will also hide real FFmpeg errors.
Security considerations
libvipsand the underlying loaders are written in C; a malicious input has the potential to crash the BEAM if libvips itself crashes. In comparison to ImageMagick (638+ CVEs across its history), libvips has had a much smaller attack surface (~8 CVEs, all promptly fixed).The
:imageapplication setsVIPS_BLOCK_UNTRUSTED=TRUEon start unless the user has set it explicitly. This blocks libvips from loading the more dangerous format loaders.When displaying user-supplied images on a web page, sanitise EXIF / XMP metadata before passing it to a browser — embedded HTML in metadata fields is a known vector.
Image processing is CPU-intensive and the default libvips concurrency equals the host core count. For multi-tenant workloads, lower
VIPS_CONCURRENCYto avoid CPU starvation.
License
Apache 2.0. See LICENSE.md for the full text.