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

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"}
  ]
end

libvips 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
end

Installing 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
:nxImage.to_nx/2, Image.from_nx/1, tensor interop
:scholarImage.k_means/2
:xavImage.Video (FFmpeg-backed frame extraction)
:evisionImage.QRcode, Image.to_evision/2, Image.from_evision/1
:image_detectionImage.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
:kinoImage.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:

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

License

Apache 2.0. See LICENSE.md for the full text.