PhoenixKit.Modules.Storage.ImageProcessor (phoenix_kit v2.0.1)

Copy Markdown View Source

ImageMagick-based image processing module.

Handles image operations using ImageMagick command-line tools:

  • identify - Extract image metadata (dimensions, format)
  • convert/magick - Resize and format conversion

This replaces Vix with ImageMagick, which is more widely trusted and has better long-term support.

Summary

Functions

Extract both width and height from an image file.

Get the height of an image file using ImageMagick identify.

Get the width of an image file using ImageMagick identify.

Resize an image to fit within specified dimensions.

Resize and center-crop an image to exact dimensions.

Re-encodes an image to known-good bytes, discarding everything else.

Functions

extract_dimensions(file_path)

Extract both width and height from an image file.

Uses ImageMagick's identify command to extract image dimensions.

Returns:

  • {:ok, {width, height}} - Dimensions in pixels
  • {:error, reason} - If extraction fails

get_height(file_path)

Get the height of an image file using ImageMagick identify.

Returns the height in pixels or nil if extraction fails.

get_width(file_path)

Get the width of an image file using ImageMagick identify.

Returns the width in pixels or nil if extraction fails.

resize(input_path, output_path, width, height, opts \\ [])

Resize an image to fit within specified dimensions.

Maintains aspect ratio by scaling to fit within bounds. Optionally converts format based on output_format parameter.

Parameters:

  • input_path - Path to input image file
  • output_path - Path to save resized image
  • width - Target width (nil to use original)
  • height - Target height (nil to use original)
  • opts - Additional options
    • :quality - JPEG quality 1-100 (default: 85)
    • :format - Output format override (jpg, png, webp, etc)

Returns:

  • {:ok, output_path} - Success
  • {:error, reason} - If resize fails

resize_and_crop_center(input_path, output_path, width, height, opts \\ [])

Resize and center-crop an image to exact dimensions.

Zooms into the image to fill the target dimensions completely, then center-crops to extract the exact target size. No padding borders - the entire output is filled with the image content.

This is ideal for thumbnails where you want perfect squares (e.g., 150x150) with the image zoomed in and centered, no white/black borders.

The algorithm:

  1. Resizes image to fill the target box (scales to cover both dimensions)
  2. Centers the image using gravity
  3. Crops from center to exact target dimensions

Parameters:

  • input_path - Path to input image file
  • output_path - Path to save cropped image
  • width - Target width (required)
  • height - Target height (required)
  • opts - Additional options
    • :quality - JPEG quality 1-100 (default: 85)
    • :format - Output format override (jpg, png, webp, etc)
    • :background - Background color (rarely used, default: "white")

Returns:

  • {:ok, output_path} - Success
  • {:error, reason} - If processing fails

sanitize(input_path, output_path, opts \\ [])

@spec sanitize(String.t(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, String.t()}

Re-encodes an image to known-good bytes, discarding everything else.

For uploads from people you do not trust. The point is not to inspect the file — that is a losing game — but to stop serving the uploader's bytes at all: what ends up stored is this encoder's output, produced by decoding the input and writing a fresh image. A polyglot file (valid GIF, valid JavaScript), an EXIF payload, a trailing ZIP, an embedded colour profile — none of them survive being decoded to pixels and written out again.

Also enforces a maximum edge length, because a 30000×30000 PNG is a decompression bomb whatever its byte size, and rejects anything ImageMagick cannot read as an image regardless of what the upload claimed to be.

Returns {:ok, output_path} or {:error, reason}. Never raises.

⚠️ Nothing in core calls this yet

It is a primitive, not a policy: no upload path in PhoenixKit routes through it, so adding it did not change what any existing endpoint stores. A caller that accepts untrusted uploads — a public portal submission, an avatar from an unauthenticated form — has to invoke it itself and store the OUTPUT path, discarding the original. Wiring it into Storage.upload/* wholesale is not a drop-in: it re-encodes, which is right for images from strangers and wrong for a designer uploading a master PNG they expect back byte-for-byte.

Options

  • :max_edge — longest side in pixels (default 2500); larger images are scaled down, never up
  • :quality — output quality (default 82)
  • :format — output format (default "jpeg"; use "png" to keep transparency)

Example

ImageProcessor.sanitize(upload.path, dest, format: "png")