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.
-
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.
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 |
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.
{:ok, image} = Image.open("photo.jpg")
{:ok, thumb} = Image.thumbnail(image, 256)
:ok = Image.write(thumb, "thumb.jpg", quality: 85)image
|> Image.resize!(scale: 0.5)
|> Image.crop!(0, 0, 400, 400)
|> Image.rotate!(15)
|> Image.write!("derived.png"){: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 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)"){: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}, ...]{:ok, image} = Image.open("photo.jpg")
{:ok, exif} = Image.exif(image)
exif[:make]
# => "FUJIFILM""photo.jpg"
|> File.stream!([], 64_000)
|> Image.open!()
|> Image.thumbnail!(256)
|> Image.write!(File.stream!("thumb.jpg")){:ok, qrcode} = Image.QRcode.encode("Hello world", size: 256)
{:ok, "Hello world"} = Image.QRcode.decode(qrcode)(Requires the optional :evision dependency.)
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
endStarting 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-develThen set VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS at compile
time and at runtime. See the Vix documentation
for the full list.
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-libsNote: 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.
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.
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.
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.
-
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.
Apache 2.0. See LICENSE.md for the full text.