Packages
phoenix_kit
1.7.162
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/storage/services/image_processor.ex
defmodule PhoenixKit.Modules.Storage.ImageProcessor do
@moduledoc """
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.
"""
require Logger
@doc """
Get the width of an image file using ImageMagick identify.
Returns the width in pixels or nil if extraction fails.
"""
def get_width(file_path) do
case extract_dimensions(file_path) do
{:ok, {width, _height}} -> width
{:error, _reason} -> nil
end
end
@doc """
Get the height of an image file using ImageMagick identify.
Returns the height in pixels or nil if extraction fails.
"""
def get_height(file_path) do
case extract_dimensions(file_path) do
{:ok, {_width, height}} -> height
{:error, _reason} -> nil
end
end
@doc """
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
"""
def extract_dimensions(file_path) do
case System.cmd("identify", ["-format", "%wx%h", file_path], stderr_to_stdout: true) do
{output, 0} ->
case String.split(String.trim(output), "x") do
[width_str, height_str] ->
case {Integer.parse(width_str), Integer.parse(height_str)} do
{{width, ""}, {height, ""}} ->
{:ok, {width, height}}
_ ->
{:error, "Failed to parse dimensions: #{output}"}
end
_ ->
{:error, "Invalid dimension format: #{output}"}
end
{output, exit_code} ->
{:error, "identify failed with exit code #{exit_code}: #{output}"}
end
rescue
e ->
{:error, "Failed to extract dimensions: #{inspect(e)}"}
end
@doc """
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
"""
def resize(input_path, output_path, width, height, opts \\ []) do
quality = Keyword.get(opts, :quality, 85)
format = Keyword.get(opts, :format, nil)
# Extract current dimensions
case extract_dimensions(input_path) do
{:ok, {current_width, current_height}} ->
# Calculate resize parameters
resize_spec = calculate_resize_spec(current_width, current_height, width, height)
# Build ImageMagick convert command
args = build_convert_args(input_path, output_path, resize_spec, quality, format)
Logger.info(
"Resizing image: #{input_path} -> #{output_path}, resize spec: #{resize_spec}"
)
case System.cmd("convert", args, stderr_to_stdout: true) do
{_output, 0} ->
Logger.info("Successfully resized image to #{output_path}")
{:ok, output_path}
{output, exit_code} ->
Logger.error("convert failed with exit code #{exit_code}: #{output}")
{:error, "ImageMagick convert failed: #{output}"}
end
{:error, reason} ->
{:error, "Failed to extract image dimensions: #{reason}"}
end
rescue
e ->
Logger.error("Image resize failed: #{inspect(e)}")
{:error, "Image resize failed: #{inspect(e)}"}
end
@doc """
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
"""
def resize_and_crop_center(input_path, output_path, width, height, opts \\ []) do
quality = Keyword.get(opts, :quality, 85)
format = Keyword.get(opts, :format, nil)
alpha? = has_alpha_channel?(input_path)
background =
if Keyword.has_key?(opts, :background) do
Keyword.get(opts, :background, "white")
else
if alpha?, do: "none", else: "white"
end
format =
if format == "jpg" and alpha? do
Logger.warning(
"Image #{input_path} has alpha channel but format is jpg — overriding to webp to preserve transparency"
)
"webp"
else
format
end
if is_nil(width) or is_nil(height) do
{:error, "Both width and height are required for center-crop resizing"}
else
Logger.info(
"Center-cropping image: #{input_path} -> #{output_path}, target: #{width}x#{height}"
)
# Build ImageMagick convert command for center-crop
args =
build_center_crop_args(
input_path,
output_path,
width,
height,
quality,
format,
background
)
case System.cmd("convert", args, stderr_to_stdout: true) do
{_output, 0} ->
Logger.info("Successfully center-cropped image to #{output_path}")
{:ok, output_path}
{output, exit_code} ->
Logger.error("convert failed with exit code #{exit_code}: #{output}")
{:error, "ImageMagick convert failed: #{output}"}
end
end
rescue
e ->
Logger.error("Image center-crop failed: #{inspect(e)}")
{:error, "Image center-crop failed: #{inspect(e)}"}
end
# Private functions
defp calculate_resize_spec(current_width, current_height, target_width, target_height) do
case {target_width, target_height} do
{w, h} when w != nil and h != nil ->
# Both width and height specified - fit within bounds preserving aspect ratio
# Use ImageMagick's extent notation: scale to fit, then extend to exact size if needed
# The '>' suffix means only shrink, never enlarge
"#{w}x#{h}>"
{w, nil} when w != nil ->
# Only width specified - maintain aspect ratio
"#{w}x"
{nil, h} when h != nil ->
# Only height specified - maintain aspect ratio
"x#{h}"
_ ->
# No dimensions - return original size
"#{current_width}x#{current_height}"
end
end
defp build_convert_args(input_path, output_path, resize_spec, quality, format) do
args = [input_path]
# Add resize operation
args = args ++ ["-resize", resize_spec]
# Add quality setting for JPEG (ImageMagick quality for lossy formats)
args = args ++ ["-quality", to_string(quality)]
# Add format conversion if specified
args =
if format do
format_spec = "#{format}:#{output_path}"
args ++ [format_spec]
else
args ++ [output_path]
end
args
end
defp build_center_crop_args(input_path, output_path, width, height, quality, format, background) do
args = [input_path]
# Set background color for padding/extension (rarely used with ^ resize)
args = args ++ ["-background", background]
# Resize to fill/cover the target dimensions (with the ^ flag)
# The ^ flag means "resize to FILL the box" - scales up to ensure both dimensions
# are at least the target size, creating overflow that gets cropped
resize_spec = "#{width}x#{height}^"
args = args ++ ["-resize", resize_spec]
# Use gravity center to position image at center before cropping
args = args ++ ["-gravity", "center"]
# Crop to exact dimensions from the centered position
args = args ++ ["-extent", "#{width}x#{height}"]
# Add quality setting for JPEG (ImageMagick quality for lossy formats)
args = args ++ ["-quality", to_string(quality)]
# Add format conversion if specified
args =
if format do
format_spec = "#{format}:#{output_path}"
args ++ [format_spec]
else
args ++ [output_path]
end
args
end
defp has_alpha_channel?(file_path) do
case System.cmd("identify", ["-format", "%[channels]", file_path], stderr_to_stdout: true) do
{output, 0} -> String.contains?(String.trim(output), "a")
_ -> false
end
rescue
_ -> false
end
end